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 <lucas@crabnebula.dev>
This commit is contained in:
Lucas Fernandes Nogueira
2026-09-22 06:03:10 -03:00
committed by GitHub
co-authored by Lucas Nogueira
parent b7897cab5d
commit b566f09124
4 changed files with 90 additions and 9 deletions
+6
View File
@@ -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.
+3
View File
@@ -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"] }
+3 -3
View File
@@ -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
+78 -6
View File
@@ -300,12 +300,13 @@ impl<R: Runtime> StoreInner<R> {
/// 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<R: Runtime> Store<R> {
/// 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<R: Runtime> Drop for Store<R> {
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<MockRuntime>, path: PathBuf) -> StoreInner<MockRuntime> {
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());
}
}