tabs/sync/
bridge.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::sync::engine::TabsEngine;
6use crate::TabsStore;
7use anyhow::Result;
8use std::sync::Arc;
9use sync15::bso::{IncomingBso, OutgoingBso};
10use sync15::engine::{BridgedEngine, BridgedEngineAdaptor};
11use sync15::ServerTimestamp;
12use sync_guid::Guid as SyncGuid;
13
14impl TabsStore {
15    // Returns a bridged sync engine for Desktop for this store.
16    pub fn bridged_engine(self: Arc<Self>) -> Arc<TabsBridgedEngine> {
17        let engine = TabsEngine::new(self);
18        let bridged_engine = TabsBridgedEngineAdaptor { engine };
19        Arc::new(TabsBridgedEngine::new(Box::new(bridged_engine)))
20    }
21}
22
23/// A bridged engine implements all the methods needed to make the
24/// `storage.sync` store work with Desktop's Sync implementation.
25/// Conceptually it's very similar to our SyncEngine and there's a BridgedEngineAdaptor
26/// trait we can implement to get a `BridgedEngine` from a `SyncEngine`, so that's
27/// what we do. See also #2841, which will finally unify them completely.
28struct TabsBridgedEngineAdaptor {
29    engine: TabsEngine,
30}
31
32impl BridgedEngineAdaptor for TabsBridgedEngineAdaptor {
33    fn last_sync(&self) -> Result<i64> {
34        Ok(self.engine.get_last_sync()?.unwrap_or_default().as_millis())
35    }
36
37    fn set_last_sync(&self, last_sync_millis: i64) -> Result<()> {
38        self.engine
39            .set_last_sync(ServerTimestamp::from_millis(last_sync_millis))
40    }
41
42    fn engine(&self) -> &dyn sync15::engine::SyncEngine {
43        &self.engine
44    }
45}
46
47// This is for uniffi to expose, and does nothing than delegate back to the trait.
48pub struct TabsBridgedEngine {
49    bridge_impl: Box<dyn BridgedEngine>,
50}
51
52impl TabsBridgedEngine {
53    pub fn new(bridge_impl: Box<dyn BridgedEngine>) -> Self {
54        Self { bridge_impl }
55    }
56
57    pub fn last_sync(&self) -> Result<i64> {
58        self.bridge_impl.last_sync()
59    }
60
61    pub fn set_last_sync(&self, last_sync: i64) -> Result<()> {
62        self.bridge_impl.set_last_sync(last_sync)
63    }
64
65    pub fn sync_id(&self) -> Result<Option<String>> {
66        self.bridge_impl.sync_id()
67    }
68
69    pub fn reset_sync_id(&self) -> Result<String> {
70        self.bridge_impl.reset_sync_id()
71    }
72
73    pub fn ensure_current_sync_id(&self, sync_id: &str) -> Result<String> {
74        self.bridge_impl.ensure_current_sync_id(sync_id)
75    }
76
77    pub fn prepare_for_sync(&self, client_data: &str) -> Result<()> {
78        self.bridge_impl.prepare_for_sync(client_data)
79    }
80
81    pub fn sync_started(&self) -> Result<()> {
82        self.bridge_impl.sync_started()
83    }
84
85    // Decode the JSON-encoded IncomingBso's that UniFFI passes to us
86    fn convert_incoming_bsos(&self, incoming: Vec<String>) -> Result<Vec<IncomingBso>> {
87        let mut bsos = Vec::with_capacity(incoming.len());
88        for inc in incoming {
89            bsos.push(serde_json::from_str::<IncomingBso>(&inc)?);
90        }
91        Ok(bsos)
92    }
93
94    // Encode OutgoingBso's into JSON for UniFFI
95    fn convert_outgoing_bsos(&self, outgoing: Vec<OutgoingBso>) -> Result<Vec<String>> {
96        let mut bsos = Vec::with_capacity(outgoing.len());
97        for e in outgoing {
98            bsos.push(serde_json::to_string(&e)?);
99        }
100        Ok(bsos)
101    }
102
103    pub fn store_incoming(&self, incoming: Vec<String>) -> Result<()> {
104        self.bridge_impl
105            .store_incoming(self.convert_incoming_bsos(incoming)?)
106    }
107
108    pub fn apply(&self) -> Result<Vec<String>> {
109        let apply_results = self.bridge_impl.apply()?;
110        self.convert_outgoing_bsos(apply_results.records)
111    }
112
113    pub fn set_uploaded(&self, server_modified_millis: i64, guids: Vec<SyncGuid>) -> Result<()> {
114        self.bridge_impl
115            .set_uploaded(server_modified_millis, &guids)
116    }
117
118    pub fn sync_finished(&self) -> Result<()> {
119        self.bridge_impl.sync_finished()
120    }
121
122    pub fn reset(&self) -> Result<()> {
123        self.bridge_impl.reset()
124    }
125
126    pub fn wipe(&self) -> Result<()> {
127        self.bridge_impl.wipe()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::storage::{RemoteTab, TABS_CLIENT_TTL};
135    use crate::sync::record::TabsRecordTab;
136    use serde_json::json;
137    use std::collections::HashMap;
138    use sync15::{ClientData, DeviceType, RemoteClient};
139
140    // A copy of the normal "engine" tests but which go via the bridge
141    #[test]
142    fn test_sync_via_bridge() {
143        error_support::init_for_tests();
144
145        let store = Arc::new(TabsStore::new_with_mem_path("test-bridge_incoming"));
146
147        // Set some local tabs for our device.
148        let my_tabs = vec![
149            RemoteTab {
150                title: "my first tab".to_string(),
151                url_history: vec!["http://1.com".to_string()],
152                last_used: 2,
153                ..Default::default()
154            },
155            RemoteTab {
156                title: "my second tab".to_string(),
157                url_history: vec!["http://2.com".to_string()],
158                last_used: 1,
159                ..Default::default()
160            },
161        ];
162        store.set_local_tabs(my_tabs.clone());
163
164        let bridge = store.bridged_engine();
165
166        let client_data = ClientData {
167            local_client_id: "my-device".to_string(),
168            recent_clients: HashMap::from([
169                (
170                    "my-device".to_string(),
171                    RemoteClient {
172                        fxa_device_id: None,
173                        device_name: "my device".to_string(),
174                        device_type: sync15::DeviceType::Unknown,
175                    },
176                ),
177                (
178                    "device-no-tabs".to_string(),
179                    RemoteClient {
180                        fxa_device_id: None,
181                        device_name: "device with no tabs".to_string(),
182                        device_type: DeviceType::Unknown,
183                    },
184                ),
185                (
186                    "device-with-a-tab".to_string(),
187                    RemoteClient {
188                        fxa_device_id: None,
189                        device_name: "device with a tab".to_string(),
190                        device_type: DeviceType::Unknown,
191                    },
192                ),
193            ]),
194        };
195        bridge
196            .prepare_for_sync(&serde_json::to_string(&client_data).unwrap())
197            .expect("should work");
198
199        let records = vec![
200            // my-device should be ignored by sync - here it is acting as what our engine last
201            // wrote, but the actual tabs in our store we set above are what should be used.
202            json!({
203                "id": "my-device",
204                "clientName": "my device",
205                "tabs": [{
206                    "title": "the title",
207                    "urlHistory": [
208                        "https://mozilla.org/"
209                    ],
210                    "icon": "https://mozilla.org/icon",
211                    "lastUsed": 1643764207
212                }]
213            }),
214            json!({
215                "id": "device-no-tabs",
216                "clientName": "device with no tabs",
217                "tabs": [],
218            }),
219            json!({
220                "id": "device-with-a-tab",
221                "clientName": "device with a tab",
222                "tabs": [{
223                    "title": "the title",
224                    "urlHistory": [
225                        "https://mozilla.org/"
226                    ],
227                    "icon": "https://mozilla.org/icon",
228                    "lastUsed": 1643764207
229                }]
230            }),
231            // This has the main payload as OK but the tabs part invalid.
232            json!({
233                "id": "device-with-invalid-tab",
234                "clientName": "device with a tab",
235                "tabs": [{
236                    "foo": "bar",
237                }]
238            }),
239            // We want this to be a valid payload but an invalid tab - so it needs an ID.
240            json!({
241                "id": "invalid-tab",
242                "foo": "bar"
243            }),
244        ];
245
246        let mut incoming = Vec::new();
247        for record in records {
248            // Annoyingly we can't use `IncomingEnvelope` directly as it intentionally doesn't
249            // support Serialize - so need to use explicit json.
250            let envelope = json!({
251                "id": record.get("id"),
252                "modified": 0,
253                "payload": serde_json::to_string(&record).unwrap(),
254            });
255            incoming.push(serde_json::to_string(&envelope).unwrap());
256        }
257
258        bridge.store_incoming(incoming).expect("should store");
259
260        let out = bridge.apply().expect("should apply");
261
262        assert_eq!(out.len(), 1);
263        let ours = serde_json::from_str::<serde_json::Value>(&out[0]).unwrap();
264        // As above, can't use `OutgoingEnvelope` as it doesn't Deserialize.
265        // First, convert my_tabs from the local `RemoteTab` to the Sync specific `TabsRecord`
266        let expected_tabs: Vec<TabsRecordTab> =
267            my_tabs.into_iter().map(|t| t.to_record_tab()).collect();
268        let expected = json!({
269            "id": "my-device".to_string(),
270            "payload": json!({
271                "id": "my-device".to_string(),
272                "clientName": "my device",
273                "tabs": serde_json::to_value(expected_tabs).unwrap(),
274            }).to_string(),
275            "ttl": TABS_CLIENT_TTL,
276        });
277
278        assert_eq!(ours, expected);
279        bridge.set_uploaded(1234, vec![]).unwrap();
280        assert_eq!(bridge.last_sync().unwrap(), 1234);
281    }
282
283    #[test]
284    fn test_sync_meta() {
285        error_support::init_for_tests();
286
287        let store = Arc::new(TabsStore::new_with_mem_path("test-meta"));
288        let bridge = store.bridged_engine();
289
290        // Should not error or panic
291        assert_eq!(bridge.last_sync().unwrap(), 0);
292        bridge.set_last_sync(3).unwrap();
293        assert_eq!(bridge.last_sync().unwrap(), 3);
294
295        assert!(bridge.sync_id().unwrap().is_none());
296
297        bridge.ensure_current_sync_id("some_guid").unwrap();
298        assert_eq!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
299        // changing the sync ID should reset the timestamp
300        assert_eq!(bridge.last_sync().unwrap(), 0);
301        bridge.set_last_sync(3).unwrap();
302
303        bridge.reset_sync_id().unwrap();
304        // should now be a random guid.
305        assert_ne!(bridge.sync_id().unwrap(), Some("some_guid".to_string()));
306        // should have reset the last sync timestamp.
307        assert_eq!(bridge.last_sync().unwrap(), 0);
308        bridge.set_last_sync(3).unwrap();
309
310        // `reset` clears the guid and the timestamp
311        bridge.reset().unwrap();
312        assert_eq!(bridge.last_sync().unwrap(), 0);
313        assert!(bridge.sync_id().unwrap().is_none());
314    }
315}