/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub(crate) mod bridge; mod incoming; mod outgoing;
#[cfg(test)] mod sync_tests;
usecrate::api::{StorageChanges, StorageValueChange}; usecrate::db::StorageDb; usecrate::error::*; use serde::Deserialize; use serde_derive::*; use sql_support::ConnExt; use sync_guid::Guid as SyncGuid;
use incoming::IncomingAction;
type JsonMap = serde_json::Map<String, serde_json::Value>;
// Perform a 2-way or 3-way merge, where the incoming value wins on conflict. fn merge(
ext_id: String, mut other: JsonMap, mut ours: JsonMap,
parent: Option<JsonMap>,
) -> IncomingAction { if other == ours { return IncomingAction::Same { ext_id };
} let old_incoming = other.clone(); // worst case is keys in each are unique. letmut changes = StorageChanges::with_capacity(other.len() + ours.len()); iflet Some(parent) = parent { // Perform 3-way merge. First, for every key in parent, // compare the parent value with the incoming value to compute // an implicit "diff". for (key, parent_value) in parent.into_iter() { iflet Some(incoming_value) = other.remove(&key) { if incoming_value != parent_value {
log::trace!( "merge: key {} was updated in incoming - copying value locally",
key
); let old_value = ours.remove(&key); let new_value = Some(incoming_value.clone()); if old_value != new_value {
changes.push(StorageValueChange {
key: key.clone(),
old_value,
new_value,
});
}
ours.insert(key, incoming_value);
}
} else { // Key was not present in incoming value. // Another client must have deleted it.
log::trace!( "merge: key {} no longer present in incoming - removing it locally",
key
); iflet Some(old_value) = ours.remove(&key) {
changes.push(StorageValueChange {
key,
old_value: Some(old_value),
new_value: None,
});
}
}
}
// Then, go through every remaining key in incoming. These are // the ones where a corresponding key does not exist in // parent, so it is a new key, and we need to add it. for (key, incoming_value) in other.into_iter() {
log::trace!( "merge: key {} doesn't occur in parent - copying from incoming",
key
);
changes.push(StorageValueChange {
key: key.clone(),
old_value: None,
new_value: Some(incoming_value.clone()),
});
ours.insert(key, incoming_value);
}
} else { // No parent. Server wins. Overwrite every key in ours with // the corresponding value in other.
log::trace!("merge: no parent - copying all keys from incoming"); for (key, incoming_value) in other.into_iter() { let old_value = ours.remove(&key); let new_value = Some(incoming_value.clone()); if old_value != new_value {
changes.push(StorageValueChange {
key: key.clone(),
old_value,
new_value,
});
}
ours.insert(key, incoming_value);
}
}
/// Holds a JSON-serialized map of all synced changes for an extension. #[derive(Clone, Debug, Eq, PartialEq)] pubstruct SyncedExtensionChange { /// The extension ID. pub ext_id: String, /// The contents of a `StorageChanges` struct, in JSON format. We don't /// deserialize these because they need to be passed back to the browser /// as strings anyway. pub changes: String,
}
// Fetches the applied changes we stashed in the storage_sync_applied table. pubfn get_synced_changes(db: &StorageDb) -> Result<Vec<SyncedExtensionChange>> { let signal = db.begin_interrupt_scope()?; let sql = "SELECT ext_id, changes FROM temp.storage_sync_applied"; let conn = db.get_connection()?;
conn.query_rows_and_then(sql, [], |row| -> Result<_> {
signal.err_if_interrupted()?;
Ok(SyncedExtensionChange {
ext_id: row.get("ext_id")?,
changes: row.get("changes")?,
})
})
}
// Helpers for tests #[cfg(test)] pubmod test { usecrate::db::{test::new_mem_db, StorageDb}; usecrate::schema::create_empty_sync_temp_tables;
pubfn new_syncable_mem_db() -> StorageDb { let _ = env_logger::try_init(); let db = new_mem_db(); let conn = db.get_connection().expect("should retrieve connection");
create_empty_sync_temp_tables(conn).expect("should work");
db
}
}
#[cfg(test)] mod tests { usesuper::test::new_syncable_mem_db; usesuper::*; use serde_json::json;
// a macro for these tests - constructs a serde_json::Value::Object
macro_rules! map {
($($map:tt)+) => {
json!($($map)+).as_object().unwrap().clone()
};
}
#[test] fn test_get_synced_changes() -> Result<()> { let db = new_syncable_mem_db(); let conn = db.get_connection()?;
conn.execute_batch(&format!(
r#"INSERT INTO temp.storage_sync_applied (ext_id, changes)
VALUES
('an-extension', '{change1}'),
('ext"id', '{change2}') "#,
change1 = serde_json::to_string(&changes![change!("key1", "old-val", None)])?,
change2 = serde_json::to_string(&changes![change!("key-for-second", None, "new-val")])?
))?; let changes = get_synced_changes(&db)?;
assert_eq!(changes[0].ext_id, "an-extension"); // sanity check it's valid! let c1: JsonMap =
serde_json::from_str(&changes[0].changes).expect("changes must be an object");
assert_eq!(
c1.get("key1")
.expect("must exist")
.as_object()
.expect("must be an object")
.get("oldValue"),
Some(&json!("old-val"))
);
// phew - do it again to check the string got escaped.
assert_eq!(
changes[1],
SyncedExtensionChange {
ext_id: "ext\"id".into(),
changes: r#"{"key-for-second":{"newValue":"new-val"}}"#.into(),
}
);
assert_eq!(changes[1].ext_id, "ext\"id"); let c2: JsonMap =
serde_json::from_str(&changes[1].changes).expect("changes must be an object");
assert_eq!(
c2.get("key-for-second")
.expect("must exist")
.as_object()
.expect("must be an object")
.get("newValue"),
Some(&json!("new-val"))
);
Ok(())
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-18)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.