From b566f09124ff8de42ae5287c340035c085a69ec9 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 22 Sep 2026 06:03:10 -0300 Subject: [PATCH] refactor(store)!: reset to defaults before merging the on-disk state in reload (#3599) * refactor(store)!: reset to defaults before merging the on-disk state in reload `Store::reload` / `reload()` previously merged the on-disk state into the current in-memory store. It now resets the store to its defaults first, so in-memory keys that are neither in the defaults nor on disk are dropped. `reload_ignore_defaults` / `reload({ ignoreDefaults: true })` is unchanged. * test(store): cover reload resetting to the defaults before merging the on-disk state --------- Co-authored-by: Lucas Nogueira --- .changes/store-reload-defaults.md | 6 +++ plugins/store/Cargo.toml | 3 ++ plugins/store/guest-js/index.ts | 6 +-- plugins/store/src/store.rs | 84 ++++++++++++++++++++++++++++--- 4 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 .changes/store-reload-defaults.md diff --git a/.changes/store-reload-defaults.md b/.changes/store-reload-defaults.md new file mode 100644 index 000000000..cf60dcbe3 --- /dev/null +++ b/.changes/store-reload-defaults.md @@ -0,0 +1,6 @@ +--- +"store": major +"store-js": major +--- + +**Breaking:** `Store::reload` (and `reload()` in JavaScript) now resets the store to its defaults before merging the on-disk state into it, so in-memory keys that are neither in the defaults nor on disk are dropped. Previously the on-disk state was merged into the current in-memory store. Use `reload_ignore_defaults` / `reload({ ignoreDefaults: true })` to fully match the on-disk state. diff --git a/plugins/store/Cargo.toml b/plugins/store/Cargo.toml index e8f4b4b39..47b34845c 100644 --- a/plugins/store/Cargo.toml +++ b/plugins/store/Cargo.toml @@ -35,3 +35,6 @@ tracing = { workspace = true } thiserror = { workspace = true } dunce = { workspace = true } tokio = { version = "1", features = ["sync", "time", "macros"] } + +[dev-dependencies] +tauri = { workspace = true, features = ["test"] } diff --git a/plugins/store/guest-js/index.ts b/plugins/store/guest-js/index.ts index ec8f11657..a634ffe5f 100644 --- a/plugins/store/guest-js/index.ts +++ b/plugins/store/guest-js/index.ts @@ -405,9 +405,9 @@ interface IStore { * This method is useful if the on-disk state was edited by the user and you want to synchronize the changes. * * Note: - * - This method loads the data and merges it with the current store, - * this behavior will be changed to resetting to default first and then merging with the on-disk state in v3, - * to fully match the store with the on-disk state, set {@linkcode ReloadOptions | ignoreDefaults} to `true` + * - This method resets the store to its defaults and then merges the on-disk state into it, + * so keys that are neither in the defaults nor on disk are dropped. + * To fully match the store with the on-disk state (ignoring defaults), set {@linkcode ReloadOptions | ignoreDefaults} to `true` * - This method does not emit change events. * * @returns diff --git a/plugins/store/src/store.rs b/plugins/store/src/store.rs index b597a4369..68e32b8a5 100644 --- a/plugins/store/src/store.rs +++ b/plugins/store/src/store.rs @@ -300,12 +300,13 @@ impl StoreInner { /// Update the store from the on-disk state /// - /// Note: This method loads the data and merges it with the current store + /// Note: This method resets the store to its defaults and then merges the on-disk state into it pub fn load(&mut self) -> crate::Result<()> { let bytes = fs::read(&self.path)?; + let entries = (self.deserialize_fn)(&bytes).map_err(crate::Error::Deserialize)?; - self.cache - .extend((self.deserialize_fn)(&bytes).map_err(crate::Error::Deserialize)?); + self.cache = self.defaults.clone().unwrap_or_default(); + self.cache.extend(entries); Ok(()) } @@ -525,9 +526,9 @@ impl Store { /// Update the store from the on-disk state /// /// Note: - /// - This method loads the data and merges it with the current store, - /// this behavior will be changed to resetting to default first and then merging with the on-disk state in v3, - /// to fully match the store with the on-disk state, + /// - This method resets the store to its defaults and then merges the on-disk state into it, + /// so keys that are neither in the defaults nor on disk are dropped. + /// To fully match the store with the on-disk state (ignoring defaults), /// use [`reload_ignore_defaults`](Self::reload_ignore_defaults) instead /// - This method does not emit change events pub fn reload(&self) -> crate::Result<()> { @@ -613,3 +614,74 @@ impl Drop for Store { self.apply_pending_auto_save(); } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use tauri::{ + test::{mock_app, MockRuntime}, + App, + }; + + fn temp_store_path(name: &str) -> PathBuf { + let dir = + std::env::temp_dir().join(format!("tauri-plugin-store-{}-{name}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + dir.join("store.json") + } + + fn store(app: &App, path: PathBuf) -> StoreInner { + let mut defaults = HashMap::new(); + defaults.insert("default-key".to_string(), json!("default")); + defaults.insert("shared-key".to_string(), json!("default")); + StoreInner::new( + app.handle().clone(), + path, + Some(defaults), + crate::default_serialize, + crate::default_deserialize, + ) + } + + #[test] + fn load_resets_to_defaults_before_merging_the_on_disk_state() { + let app = mock_app(); + let path = temp_store_path("load"); + let mut store = store(&app, path.clone()); + fs::write(&path, r#"{ "disk-key": "disk", "shared-key": "disk" }"#).unwrap(); + // neither in the defaults nor on disk, so it must be dropped + store + .cache + .insert("memory-key".to_string(), json!("memory")); + + store.load().unwrap(); + + assert_eq!(store.get("default-key"), Some(&json!("default"))); + assert_eq!(store.get("disk-key"), Some(&json!("disk"))); + // the on-disk state takes precedence over the defaults + assert_eq!(store.get("shared-key"), Some(&json!("disk"))); + assert_eq!(store.get("memory-key"), None); + + let _ = fs::remove_dir_all(path.parent().unwrap()); + } + + #[test] + fn load_ignore_defaults_matches_the_on_disk_state() { + let app = mock_app(); + let path = temp_store_path("load-ignore-defaults"); + let mut store = store(&app, path.clone()); + fs::write(&path, r#"{ "disk-key": "disk" }"#).unwrap(); + store + .cache + .insert("memory-key".to_string(), json!("memory")); + + store.load_ignore_defaults().unwrap(); + + assert_eq!(store.get("disk-key"), Some(&json!("disk"))); + assert_eq!(store.get("default-key"), None); + assert_eq!(store.get("memory-key"), None); + + let _ = fs::remove_dir_all(path.parent().unwrap()); + } +}