From 03cf4a362408b9caffa6848aae2fcf472a789460 Mon Sep 17 00:00:00 2001 From: lougeniac64 Date: Fri, 1 Nov 2024 18:59:23 -0400 Subject: [PATCH] Exposed webext-storage component calls for desktop --- CHANGELOG.md | 6 ++ components/webext-storage/src/db.rs | 5 +- components/webext-storage/src/error.rs | 8 ++ components/webext-storage/src/lib.rs | 15 +++ components/webext-storage/src/store.rs | 40 +------ components/webext-storage/src/sync/bridge.rs | 100 +++++++++++++++++- components/webext-storage/src/sync/mod.rs | 3 +- .../webext-storage/src/webext-storage.udl | 63 +++++++++++ 8 files changed, 196 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d51887fb2b..52793623ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ ### Glean - Updated to v63.0.0 ([bug 1933939](https://bugzilla.mozilla.org/show_bug.cgi?id=1933939)) +## ⚠️ Breaking Changes ⚠️ + +## Webext-Storage +- Exposed the bridged engine logic for use in desktop ([#6473](https://github.com/mozilla/application-services/pull/6473)). + - This updated the signature of the `bridged_engine` function technically making this PR a breaking change though an imminent desktop patch will remove references to function calls with the old signature. + # v134.0 (_2023-11-25_) ## ✨ What's New ✨ diff --git a/components/webext-storage/src/db.rs b/components/webext-storage/src/db.rs index c8cb38fac0..d716c061d7 100644 --- a/components/webext-storage/src/db.rs +++ b/components/webext-storage/src/db.rs @@ -117,13 +117,10 @@ impl ThreadSafeStorageDb { Arc::clone(&self.interrupt_handle) } + #[allow(dead_code)] pub fn begin_interrupt_scope(&self) -> Result { Ok(self.interrupt_handle.begin_interrupt_scope()?) } - - pub fn into_inner(self) -> StorageDb { - self.db.into_inner() - } } // Deref to a Mutex, which is how we will use ThreadSafeStorageDb most of the time diff --git a/components/webext-storage/src/error.rs b/components/webext-storage/src/error.rs index 11ed250436..7774f15dce 100644 --- a/components/webext-storage/src/error.rs +++ b/components/webext-storage/src/error.rs @@ -143,3 +143,11 @@ impl From for WebExtStorageApiError { } } } + +impl From for WebExtStorageApiError { + fn from(value: anyhow::Error) -> Self { + WebExtStorageApiError::UnexpectedError { + reason: value.to_string(), + } + } +} diff --git a/components/webext-storage/src/lib.rs b/components/webext-storage/src/lib.rs index dd9e72006b..8f3da9d6b9 100644 --- a/components/webext-storage/src/lib.rs +++ b/components/webext-storage/src/lib.rs @@ -25,6 +25,7 @@ pub use api::SYNC_QUOTA_BYTES_PER_ITEM; pub use crate::error::{QuotaReason, WebExtStorageApiError}; pub use crate::store::WebExtStorageStore; +pub use crate::sync::{bridge::WebExtStorageBridgedEngine, SyncedExtensionChange}; pub use api::UsageInfo; pub use api::{StorageChanges, StorageValueChange}; @@ -42,3 +43,17 @@ impl UniffiCustomTypeConverter for JsonValue { obj.to_string() } } + +// Our UDL uses a `Guid` type. +use sync_guid::Guid; +impl UniffiCustomTypeConverter for Guid { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Guid::new(val.as_str())) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.into() + } +} diff --git a/components/webext-storage/src/store.rs b/components/webext-storage/src/store.rs index a1bbff92ab..cee0522b5d 100644 --- a/components/webext-storage/src/store.rs +++ b/components/webext-storage/src/store.rs @@ -29,7 +29,7 @@ use serde_json::Value as JsonValue; /// connection with our sync engines - ie, these engines also hold an Arc<> /// around the same object. pub struct WebExtStorageStore { - db: Arc, + pub(crate) db: Arc, } impl WebExtStorageStore { @@ -124,46 +124,16 @@ impl WebExtStorageStore { /// Returns the bytes in use for the specified items (which can be null, /// a string, or an array) - pub fn get_bytes_in_use(&self, ext_id: &str, keys: JsonValue) -> Result { + pub fn get_bytes_in_use(&self, ext_id: &str, keys: JsonValue) -> Result { let db = &self.db.lock(); let conn = db.get_connection()?; - api::get_bytes_in_use(conn, ext_id, keys) - } - - /// Returns a bridged sync engine for Desktop for this store. - pub fn bridged_engine(&self) -> sync::BridgedEngine { - sync::BridgedEngine::new(&self.db) + Ok(api::get_bytes_in_use(conn, ext_id, keys)? as u64) } /// Closes the store and its database connection. See the docs for /// `StorageDb::close` for more details on when this can fail. - pub fn close(self) -> Result<()> { - // Even though this consumes `self`, the fact we use an Arc<> means - // we can't guarantee we can actually consume the inner DB - so do - // the best we can. - let shared: ThreadSafeStorageDb = match Arc::into_inner(self.db) { - Some(shared) => shared, - _ => { - // The only way this is possible is if the sync engine has an operation - // running - but that shouldn't be possible in practice because desktop - // uses a single "task queue" such that the close operation can't possibly - // be running concurrently with any sync or storage tasks. - - // If this *could* get hit, rusqlite will attempt to close the DB connection - // as it is dropped, and if that close fails, then rusqlite 0.28.0 and earlier - // would panic - but even that only happens if prepared statements are - // not finalized, which ruqlite also does. - - // tl;dr - this should be impossible. If it was possible, rusqlite might panic, - // but we've never seen it panic in practice other places we don't close - // connections, and the next rusqlite version will not panic anyway. - // So this-is-fine.jpg - log::warn!("Attempting to close a store while other DB references exist."); - return Err(Error::OtherConnectionReferencesExist); - } - }; - // consume the mutex and get back the inner. - let mut db = shared.into_inner(); + pub fn close(&self) -> Result<()> { + let mut db = self.db.lock(); db.close() } diff --git a/components/webext-storage/src/sync/bridge.rs b/components/webext-storage/src/sync/bridge.rs index 763dbfea64..4067d6e49e 100644 --- a/components/webext-storage/src/sync/bridge.rs +++ b/components/webext-storage/src/sync/bridge.rs @@ -5,18 +5,30 @@ use anyhow::Result; use rusqlite::Transaction; use std::sync::{Arc, Weak}; -use sync15::bso::IncomingBso; -use sync15::engine::ApplyResults; +use sync15::bso::{IncomingBso, OutgoingBso}; +use sync15::engine::{ApplyResults, BridgedEngine as Sync15BridgedEngine}; use sync_guid::Guid as SyncGuid; use crate::db::{delete_meta, get_meta, put_meta, ThreadSafeStorageDb}; use crate::schema; use crate::sync::incoming::{apply_actions, get_incoming, plan_incoming, stage_incoming}; use crate::sync::outgoing::{get_outgoing, record_uploaded, stage_outgoing}; +use crate::WebExtStorageStore; const LAST_SYNC_META_KEY: &str = "last_sync_time"; const SYNC_ID_META_KEY: &str = "sync_id"; +impl WebExtStorageStore { + // Returns a bridged sync engine for this store. + pub fn bridged_engine(self: Arc) -> Arc { + let engine = Box::new(BridgedEngine::new(&self.db)); + let bridged_engine = WebExtStorageBridgedEngine { + bridge_impl: engine, + }; + Arc::new(bridged_engine) + } +} + /// A bridged engine implements all the methods needed to make the /// `storage.sync` store work with Desktop's Sync implementation. /// Conceptually, it's similar to `sync15::Store`, which we @@ -54,7 +66,7 @@ impl BridgedEngine { } } -impl sync15::engine::BridgedEngine for BridgedEngine { +impl Sync15BridgedEngine for BridgedEngine { fn last_sync(&self) -> Result { let shared_db = self.thread_safe_storage_db()?; let db = shared_db.lock(); @@ -194,6 +206,88 @@ impl sync15::engine::BridgedEngine for BridgedEngine { } } +pub struct WebExtStorageBridgedEngine { + bridge_impl: Box, +} + +impl WebExtStorageBridgedEngine { + pub fn new(bridge_impl: Box) -> Self { + Self { bridge_impl } + } + + pub fn last_sync(&self) -> Result { + self.bridge_impl.last_sync() + } + + pub fn set_last_sync(&self, last_sync: i64) -> Result<()> { + self.bridge_impl.set_last_sync(last_sync) + } + + pub fn sync_id(&self) -> Result> { + self.bridge_impl.sync_id() + } + + pub fn reset_sync_id(&self) -> Result { + self.bridge_impl.reset_sync_id() + } + + pub fn ensure_current_sync_id(&self, sync_id: &str) -> Result { + self.bridge_impl.ensure_current_sync_id(sync_id) + } + + pub fn prepare_for_sync(&self, client_data: &str) -> Result<()> { + self.bridge_impl.prepare_for_sync(client_data) + } + + pub fn store_incoming(&self, incoming: Vec) -> Result<()> { + self.bridge_impl + .store_incoming(self.convert_incoming_bsos(incoming)?) + } + + pub fn apply(&self) -> Result> { + let apply_results = self.bridge_impl.apply()?; + self.convert_outgoing_bsos(apply_results.records) + } + + pub fn set_uploaded(&self, server_modified_millis: i64, guids: Vec) -> Result<()> { + self.bridge_impl + .set_uploaded(server_modified_millis, &guids) + } + + pub fn sync_started(&self) -> Result<()> { + self.bridge_impl.sync_started() + } + + pub fn sync_finished(&self) -> Result<()> { + self.bridge_impl.sync_finished() + } + + pub fn reset(&self) -> Result<()> { + self.bridge_impl.reset() + } + + pub fn wipe(&self) -> Result<()> { + self.bridge_impl.wipe() + } + + fn convert_incoming_bsos(&self, incoming: Vec) -> Result> { + let mut bsos = Vec::with_capacity(incoming.len()); + for inc in incoming { + bsos.push(serde_json::from_str::(&inc)?); + } + Ok(bsos) + } + + // Encode OutgoingBso's into JSON for UniFFI + fn convert_outgoing_bsos(&self, outgoing: Vec) -> Result> { + let mut bsos = Vec::with_capacity(outgoing.len()); + for e in outgoing { + bsos.push(serde_json::to_string(&e)?); + } + Ok(bsos) + } +} + impl From for crate::error::Error { fn from(value: anyhow::Error) -> Self { crate::error::Error::SyncError(value.to_string()) diff --git a/components/webext-storage/src/sync/mod.rs b/components/webext-storage/src/sync/mod.rs index 7d6df456e5..8dd75d761b 100644 --- a/components/webext-storage/src/sync/mod.rs +++ b/components/webext-storage/src/sync/mod.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -mod bridge; +pub(crate) mod bridge; mod incoming; mod outgoing; @@ -17,7 +17,6 @@ use serde_derive::*; use sql_support::ConnExt; use sync_guid::Guid as SyncGuid; -pub use bridge::BridgedEngine; use incoming::IncomingAction; type JsonMap = serde_json::Map; diff --git a/components/webext-storage/src/webext-storage.udl b/components/webext-storage/src/webext-storage.udl index f912861030..41079add7f 100644 --- a/components/webext-storage/src/webext-storage.udl +++ b/components/webext-storage/src/webext-storage.udl @@ -5,6 +5,9 @@ [Custom] typedef string JsonValue; +[Custom] +typedef string Guid; + namespace webextstorage { }; @@ -22,6 +25,11 @@ interface WebExtStorageApiError { QuotaError(QuotaReason reason); }; +dictionary SyncedExtensionChange { + string ext_id; + string changes; +}; + dictionary StorageValueChange { string key; JsonValue? old_value; @@ -42,9 +50,64 @@ interface WebExtStorageStore { [Throws=WebExtStorageApiError] JsonValue get([ByRef] string ext_id, JsonValue keys); + [Throws=WebExtStorageApiError] + u64 get_bytes_in_use([ByRef] string ext_id, JsonValue keys); + + [Throws=WebExtStorageApiError] + void close(); + [Throws=WebExtStorageApiError] StorageChanges remove([ByRef] string ext_id, JsonValue keys); [Throws=WebExtStorageApiError] StorageChanges clear([ByRef] string ext_id); + + [Self=ByArc] + WebExtStorageBridgedEngine bridged_engine(); + + [Throws=WebExtStorageApiError] + sequence get_synced_changes(); +}; + +// Note the canonical docs for this are in https://github.com/mozilla/application-services/blob/main/components/sync15/src/engine/bridged_engine.rs +// NOTE: all timestamps here are milliseconds. +interface WebExtStorageBridgedEngine { + [Throws=WebExtStorageApiError] + i64 last_sync(); + + [Throws=WebExtStorageApiError] + void set_last_sync(i64 last_sync); + + [Throws=WebExtStorageApiError] + string? sync_id(); + + [Throws=WebExtStorageApiError] + string reset_sync_id(); + + [Throws=WebExtStorageApiError] + string ensure_current_sync_id([ByRef]string new_sync_id); + + [Throws=WebExtStorageApiError] + void prepare_for_sync([ByRef]string client_data); + + [Throws=WebExtStorageApiError] + void sync_started(); + + [Throws=WebExtStorageApiError] + void store_incoming(sequence incoming); + + [Throws=WebExtStorageApiError] + sequence apply(); + + [Throws=WebExtStorageApiError] + void set_uploaded(i64 server_modified_millis, sequence guids); + + [Throws=WebExtStorageApiError] + void sync_finished(); + + [Throws=WebExtStorageApiError] + void reset(); + + [Throws=WebExtStorageApiError] + void wipe(); };