sync15/engine/
bridged_engine.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::error::debug;
6use crate::{ServerTimestamp, telemetry};
7use anyhow::Result;
8
9use crate::Guid;
10use crate::bso::{IncomingBso, OutgoingBso};
11
12use super::{CollSyncIds, EngineSyncAssociation, SyncEngine};
13
14/// A BridgedEngine acts as a bridge between application-services, rust
15/// implemented sync engines and sync engines as defined by Desktop Firefox.
16///
17/// [Desktop Firefox has an abstract implementation of a Sync
18/// Engine](https://searchfox.org/mozilla-central/source/services/sync/modules/engines.js)
19/// with a number of functions each engine is expected to override. Engines
20/// implemented in Rust use a different shape (specifically, the
21/// [SyncEngine](crate::SyncEngine) trait), so this BridgedEngine trait adapts
22/// between the 2.
23pub trait BridgedEngine: Send + Sync {
24    /// Returns the last sync time, in milliseconds, for this engine's
25    /// collection. This is called before each sync, to determine the lower
26    /// bound for new records to fetch from the server.
27    fn last_sync(&self) -> Result<i64>;
28
29    /// Sets the last sync time, in milliseconds. This is called throughout
30    /// the sync, to fast-forward the stored last sync time to match the
31    /// timestamp on the uploaded records.
32    fn set_last_sync(&self, last_sync_millis: i64) -> Result<()>;
33
34    /// Returns the sync ID for this engine's collection. This is only used in
35    /// tests.
36    fn sync_id(&self) -> Result<Option<String>>;
37
38    /// Resets the sync ID for this engine's collection, returning the new ID.
39    /// As a side effect, implementations should reset all local Sync state,
40    /// as in `reset`.
41    /// (Note that bridged engines never maintain the "global" guid - that's all managed
42    /// by the bridged_engine consumer (ie, desktop). bridged_engines only care about
43    /// the per-collection one.)
44    fn reset_sync_id(&self) -> Result<String>;
45
46    /// Ensures that the locally stored sync ID for this engine's collection
47    /// matches the `new_sync_id` from the server. If the two don't match,
48    /// implementations should reset all local Sync state, as in `reset`.
49    /// This method returns the assigned sync ID, which can be either the
50    /// `new_sync_id`, or a different one if the engine wants to force other
51    /// devices to reset their Sync state for this collection the next time they
52    /// sync.
53    fn ensure_current_sync_id(&self, new_sync_id: &str) -> Result<String>;
54
55    /// Tells the tabs engine about recent FxA devices. A bit of a leaky abstraction as it only
56    /// makes sense for tabs.
57    /// The arg is a json serialized `ClientData` struct.
58    fn prepare_for_sync(&self, _client_data: &str) -> Result<()> {
59        Ok(())
60    }
61
62    /// Indicates that the engine is about to start syncing. This is called
63    /// once per sync, and always before `store_incoming`.
64    fn sync_started(&self) -> Result<()>;
65
66    /// Stages a batch of incoming Sync records. This is called multiple
67    /// times per sync, once for each batch. Implementations can use the
68    /// signal to check if the operation was aborted, and cancel any
69    /// pending work.
70    fn store_incoming(&self, incoming_records: Vec<IncomingBso>) -> Result<()>;
71
72    /// Applies all staged records, reconciling changes on both sides and
73    /// resolving conflicts. Returns a list of records to upload.
74    fn apply(&self) -> Result<ApplyResults>;
75
76    /// Indicates that the given record IDs were uploaded successfully to the
77    /// server. This is called multiple times per sync, once for each batch
78    /// upload.
79    fn set_uploaded(&self, server_modified_millis: i64, ids: &[Guid]) -> Result<()>;
80
81    /// Indicates that all records have been uploaded. At this point, any record
82    /// IDs marked for upload that haven't been passed to `set_uploaded`, can be
83    /// assumed to have failed: for example, because the server rejected a record
84    /// with an invalid TTL or sort index.
85    fn sync_finished(&self) -> Result<()>;
86
87    /// Resets all local Sync state, including any change flags, mirrors, and
88    /// the last sync time, such that the next sync is treated as a first sync
89    /// with all new local data. Does not erase any local user data.
90    fn reset(&self) -> Result<()>;
91
92    /// Erases all local user data for this collection, and any Sync metadata.
93    /// This method is destructive, and unused for most collections.
94    fn wipe(&self) -> Result<()>;
95}
96
97// This is an adaptor trait - the idea is that engines can implement this
98// trait along with SyncEngine and get a BridgedEngine for free. It's temporary
99// so we can land this trait without needing to update desktop.
100// Longer term, we should remove both this trait and BridgedEngine entirely, sucking up
101// the breaking change for desktop. The main blocker to this is moving desktop away
102// from the explicit timestamp handling and moving closer to the `get_collection_request`
103// model.
104pub trait BridgedEngineAdaptor: Send + Sync {
105    // These are the main mismatches between the 2 engines
106    fn last_sync(&self) -> Result<i64>;
107    fn set_last_sync(&self, last_sync_millis: i64) -> Result<()>;
108    fn sync_started(&self) -> Result<()> {
109        Ok(())
110    }
111
112    fn engine(&self) -> &dyn SyncEngine;
113}
114
115impl<A: BridgedEngineAdaptor> BridgedEngine for A {
116    fn last_sync(&self) -> Result<i64> {
117        self.last_sync()
118    }
119
120    fn set_last_sync(&self, last_sync_millis: i64) -> Result<()> {
121        self.set_last_sync(last_sync_millis)
122    }
123
124    fn sync_id(&self) -> Result<Option<String>> {
125        Ok(match self.engine().get_sync_assoc()? {
126            EngineSyncAssociation::Disconnected => None,
127            EngineSyncAssociation::Connected(c) => Some(c.coll.into()),
128        })
129    }
130
131    fn reset_sync_id(&self) -> Result<String> {
132        // Note that bridged engines never maintain the "global" guid - that's all managed
133        // by desktop. bridged_engines only care about the per-collection one.
134        let global = Guid::empty();
135        let coll = Guid::random();
136        self.engine()
137            .reset(&EngineSyncAssociation::Connected(CollSyncIds {
138                global,
139                coll: coll.clone(),
140            }))?;
141        Ok(coll.to_string())
142    }
143
144    fn ensure_current_sync_id(&self, sync_id: &str) -> Result<String> {
145        let engine = self.engine();
146        let assoc = engine.get_sync_assoc()?;
147        if matches!(assoc, EngineSyncAssociation::Connected(c) if c.coll == sync_id) {
148            debug!("ensure_current_sync_id is current");
149        } else {
150            let new_coll_ids = CollSyncIds {
151                global: Guid::empty(),
152                coll: sync_id.into(),
153            };
154            engine.reset(&EngineSyncAssociation::Connected(new_coll_ids))?;
155        }
156        Ok(sync_id.to_string())
157    }
158
159    fn prepare_for_sync(&self, client_data: &str) -> Result<()> {
160        // unwrap here is unfortunate, but can hopefully go away if we can
161        // start using the ClientData type instead of the string.
162        self.engine()
163            .prepare_for_sync(&|| serde_json::from_str::<crate::ClientData>(client_data).unwrap())
164    }
165
166    fn sync_started(&self) -> Result<()> {
167        A::sync_started(self)
168    }
169
170    fn store_incoming(&self, incoming_records: Vec<IncomingBso>) -> Result<()> {
171        let engine = self.engine();
172        let mut telem = telemetry::Engine::new(engine.collection_name());
173        engine.stage_incoming(incoming_records, &mut telem)
174    }
175
176    fn apply(&self) -> Result<ApplyResults> {
177        let engine = self.engine();
178        let mut telem = telemetry::Engine::new(engine.collection_name());
179        // Desktop tells a bridged engine to apply the records without telling it
180        // the server timestamp, and once applied, explicitly calls `set_last_sync()`
181        // with that timestamp. So this adaptor needs to call apply with an invalid
182        // timestamp, and hope that later call with the correct timestamp does come.
183        // This isn't ideal as it means the timestamp is updated in a different transaction,
184        // but nothing too bad should happen if it doesn't - we'll just end up applying
185        // the same records again next sync.
186        let records = engine.apply(ServerTimestamp::from_millis(0), &mut telem)?;
187        Ok(ApplyResults {
188            records,
189            num_reconciled: telem
190                .get_incoming()
191                .as_ref()
192                .map(|i| i.get_reconciled() as usize),
193        })
194    }
195
196    fn set_uploaded(&self, millis: i64, ids: &[Guid]) -> Result<()> {
197        self.engine()
198            .set_uploaded(ServerTimestamp::from_millis(millis), ids.to_vec())
199    }
200
201    fn sync_finished(&self) -> Result<()> {
202        self.engine().sync_finished()
203    }
204
205    fn reset(&self) -> Result<()> {
206        self.engine().reset(&EngineSyncAssociation::Disconnected)
207    }
208
209    fn wipe(&self) -> Result<()> {
210        self.engine().wipe()
211    }
212}
213
214// TODO: We should see if we can remove this to reduce the number of types engines need to deal
215// with. num_reconciled is only used for telemetry on desktop.
216#[derive(Debug, Default)]
217pub struct ApplyResults {
218    /// List of records
219    pub records: Vec<OutgoingBso>,
220    /// The number of incoming records whose contents were merged because they
221    /// changed on both sides. None indicates we aren't reporting this
222    /// information.
223    pub num_reconciled: Option<usize>,
224}
225
226impl ApplyResults {
227    pub fn new(records: Vec<OutgoingBso>, num_reconciled: impl Into<Option<usize>>) -> Self {
228        Self {
229            records,
230            num_reconciled: num_reconciled.into(),
231        }
232    }
233}
234
235// Shorthand for engines that don't care.
236impl From<Vec<OutgoingBso>> for ApplyResults {
237    fn from(records: Vec<OutgoingBso>) -> Self {
238        Self {
239            records,
240            num_reconciled: None,
241        }
242    }
243}
244
245/// Wraps a `Box<dyn BridgedEngine>` and centralizes the work every consuming
246/// crate's UniFFI-facing bridged engine needs to do: the JSON `String` <-> BSO
247/// marshalling that crosses the FFI boundary, and 1:1 delegation to the wrapped
248/// engine. Rather than each crate hand-writing this (it was ~100 identical lines
249/// per crate), they expose a thin newtype around this via the
250/// [`uniffi_bridged_engine!`] macro.
251///
252/// All methods return [`anyhow::Result`], which each crate maps onto its own
253/// UniFFI error type via an `impl From<anyhow::Error>`.
254///
255/// Note on the longer-term direction: this type, along with [`BridgedEngine`],
256/// [`BridgedEngineAdaptor`] and [`ApplyResults`], only exists because we still
257/// have two sync-engine traits. Once Desktop moves off explicit timestamp
258/// handling to the `get_collection_request` model (see #2841) we can remove
259/// `BridgedEngine` entirely, have Desktop consume [`SyncEngine`] directly, and
260/// this wrapper collapses into a thin `SyncEngine` -> FFI shim (or goes away).
261/// See the note in `engine/mod.rs` for the migration sequencing.
262pub struct BridgedEngineWrapper {
263    inner: Box<dyn BridgedEngine>,
264}
265
266impl BridgedEngineWrapper {
267    pub fn new(inner: Box<dyn BridgedEngine>) -> Self {
268        Self { inner }
269    }
270
271    pub fn last_sync(&self) -> Result<i64> {
272        self.inner.last_sync()
273    }
274
275    pub fn set_last_sync(&self, last_sync: i64) -> Result<()> {
276        self.inner.set_last_sync(last_sync)
277    }
278
279    pub fn sync_id(&self) -> Result<Option<String>> {
280        self.inner.sync_id()
281    }
282
283    pub fn reset_sync_id(&self) -> Result<String> {
284        self.inner.reset_sync_id()
285    }
286
287    pub fn ensure_current_sync_id(&self, sync_id: &str) -> Result<String> {
288        self.inner.ensure_current_sync_id(sync_id)
289    }
290
291    pub fn prepare_for_sync(&self, client_data: &str) -> Result<()> {
292        self.inner.prepare_for_sync(client_data)
293    }
294
295    pub fn sync_started(&self) -> Result<()> {
296        self.inner.sync_started()
297    }
298
299    /// Decode the JSON-encoded `IncomingBso`s that UniFFI passes to us, then
300    /// hand them to the wrapped engine.
301    pub fn store_incoming(&self, incoming: Vec<String>) -> Result<()> {
302        let mut bsos = Vec::with_capacity(incoming.len());
303        for inc in incoming {
304            bsos.push(serde_json::from_str::<IncomingBso>(&inc)?);
305        }
306        self.inner.store_incoming(bsos)
307    }
308
309    /// Apply staged records and encode the outgoing `OutgoingBso`s back into
310    /// JSON for UniFFI.
311    pub fn apply(&self) -> Result<Vec<String>> {
312        let apply_results = self.inner.apply()?;
313        let mut outgoing = Vec::with_capacity(apply_results.records.len());
314        for e in apply_results.records {
315            outgoing.push(serde_json::to_string(&e)?);
316        }
317        Ok(outgoing)
318    }
319
320    /// Accepts anything that turns into a [`Guid`], which reconciles the
321    /// per-crate id representation: logins hands us `Vec<String>`, while
322    /// tabs and webext-storage hand us `Vec<sync_guid::Guid>`. Both `String`
323    /// and `Guid` implement `Into<Guid>`.
324    pub fn set_uploaded<G: Into<Guid>>(
325        &self,
326        server_modified_millis: i64,
327        ids: Vec<G>,
328    ) -> Result<()> {
329        let guids: Vec<Guid> = ids.into_iter().map(Into::into).collect();
330        self.inner.set_uploaded(server_modified_millis, &guids)
331    }
332
333    pub fn sync_finished(&self) -> Result<()> {
334        self.inner.sync_finished()
335    }
336
337    pub fn reset(&self) -> Result<()> {
338        self.inner.reset()
339    }
340
341    pub fn wipe(&self) -> Result<()> {
342        self.inner.wipe()
343    }
344}
345
346/// Generates a UniFFI-exposable bridged engine newtype around
347/// [`BridgedEngineWrapper`], removing the ~100 lines of identical facade
348/// boilerplate each consuming crate used to hand-write.
349///
350/// Usage (invoke in the module the crate's UDL `interface` resolves against):
351/// ```ignore
352/// sync15::uniffi_bridged_engine!(LoginsBridgedEngine, String);
353/// sync15::uniffi_bridged_engine!(TabsBridgedEngine, sync_guid::Guid);
354/// ```
355///
356/// `$guid` is the element type the crate's UDL lowers `set_uploaded`'s ids to
357/// (`String` for logins' `sequence<string>`, `sync_guid::Guid` for the tabs and
358/// webext-storage custom-type sequences). The generated methods return
359/// `anyhow::Result`, which the crate's UDL `[Throws=...]` maps to its error type
360/// via the existing `impl From<anyhow::Error>`.
361///
362/// The macro always emits `prepare_for_sync`; a crate whose UDL doesn't declare
363/// it (logins) simply leaves that inherent method unbound, which is harmless.
364#[macro_export]
365macro_rules! uniffi_bridged_engine {
366    ($name:ident, $guid:ty) => {
367        // This is what UniFFI exposes; it does nothing other than delegate to
368        // the shared `BridgedEngineWrapper`. See
369        // services/interfaces/mozIBridgedSyncEngine.idl for the Desktop contract.
370        pub struct $name($crate::engine::BridgedEngineWrapper);
371
372        impl $name {
373            pub fn new(inner: ::std::boxed::Box<dyn $crate::engine::BridgedEngine>) -> Self {
374                Self($crate::engine::BridgedEngineWrapper::new(inner))
375            }
376
377            pub fn last_sync(&self) -> ::anyhow::Result<i64> {
378                self.0.last_sync()
379            }
380
381            pub fn set_last_sync(&self, last_sync: i64) -> ::anyhow::Result<()> {
382                self.0.set_last_sync(last_sync)
383            }
384
385            pub fn sync_id(&self) -> ::anyhow::Result<Option<String>> {
386                self.0.sync_id()
387            }
388
389            pub fn reset_sync_id(&self) -> ::anyhow::Result<String> {
390                self.0.reset_sync_id()
391            }
392
393            pub fn ensure_current_sync_id(&self, sync_id: &str) -> ::anyhow::Result<String> {
394                self.0.ensure_current_sync_id(sync_id)
395            }
396
397            pub fn prepare_for_sync(&self, client_data: &str) -> ::anyhow::Result<()> {
398                self.0.prepare_for_sync(client_data)
399            }
400
401            pub fn sync_started(&self) -> ::anyhow::Result<()> {
402                self.0.sync_started()
403            }
404
405            pub fn store_incoming(&self, incoming: Vec<String>) -> ::anyhow::Result<()> {
406                self.0.store_incoming(incoming)
407            }
408
409            pub fn apply(&self) -> ::anyhow::Result<Vec<String>> {
410                self.0.apply()
411            }
412
413            pub fn set_uploaded(
414                &self,
415                server_modified_millis: i64,
416                ids: Vec<$guid>,
417            ) -> ::anyhow::Result<()> {
418                self.0.set_uploaded(server_modified_millis, ids)
419            }
420
421            pub fn sync_finished(&self) -> ::anyhow::Result<()> {
422                self.0.sync_finished()
423            }
424
425            pub fn reset(&self) -> ::anyhow::Result<()> {
426                self.0.reset()
427            }
428
429            pub fn wipe(&self) -> ::anyhow::Result<()> {
430                self.0.wipe()
431            }
432        }
433    };
434}
435
436#[cfg(test)]
437mod wrapper_tests {
438    use super::*;
439    use crate::bso::OutgoingBso;
440    use std::sync::Mutex;
441
442    // A minimal BridgedEngine that records the guids passed to `set_uploaded`,
443    // so we can lock in the `Into<Guid>` reconciliation for both `String` and
444    // `Guid` element types.
445    #[derive(Default)]
446    struct RecordingEngine {
447        uploaded: Mutex<Vec<Guid>>,
448    }
449
450    impl BridgedEngine for RecordingEngine {
451        fn last_sync(&self) -> Result<i64> {
452            Ok(0)
453        }
454        fn set_last_sync(&self, _: i64) -> Result<()> {
455            Ok(())
456        }
457        fn sync_id(&self) -> Result<Option<String>> {
458            Ok(None)
459        }
460        fn reset_sync_id(&self) -> Result<String> {
461            Ok(String::new())
462        }
463        fn ensure_current_sync_id(&self, id: &str) -> Result<String> {
464            Ok(id.to_string())
465        }
466        fn sync_started(&self) -> Result<()> {
467            Ok(())
468        }
469        fn store_incoming(&self, _: Vec<IncomingBso>) -> Result<()> {
470            Ok(())
471        }
472        fn apply(&self) -> Result<ApplyResults> {
473            Ok(Vec::<OutgoingBso>::new().into())
474        }
475        fn set_uploaded(&self, _millis: i64, ids: &[Guid]) -> Result<()> {
476            self.uploaded.lock().unwrap().extend_from_slice(ids);
477            Ok(())
478        }
479        fn sync_finished(&self) -> Result<()> {
480            Ok(())
481        }
482        fn reset(&self) -> Result<()> {
483            Ok(())
484        }
485        fn wipe(&self) -> Result<()> {
486            Ok(())
487        }
488    }
489
490    #[test]
491    fn set_uploaded_accepts_strings_and_guids() {
492        let wrapper = BridgedEngineWrapper::new(Box::new(RecordingEngine::default()));
493        // logins-style: Vec<String>
494        wrapper.set_uploaded(1, vec!["aaaa".to_string()]).unwrap();
495        // tabs/webext-style: Vec<Guid>
496        wrapper.set_uploaded(2, vec![Guid::new("bbbb")]).unwrap();
497    }
498}