From 9811756e6cd7891bd8f243091f5bce6e96523f17 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 2 Oct 2024 18:02:44 +0800 Subject: [PATCH] refactor(store): more reworks --- plugins/store/api-iife.js | 2 +- plugins/store/build.rs | 1 + .../AppSettingsManager/src-tauri/src/main.rs | 13 +- .../src-tauri/tauri.conf.json | 1 + plugins/store/guest-js/index.ts | 10 +- .../autogenerated/commands/get_store.toml | 13 ++ .../permissions/autogenerated/reference.md | 26 +++ plugins/store/permissions/schemas/schema.json | 10 + plugins/store/src/error.rs | 6 +- plugins/store/src/lib.rs | 205 ++++++++---------- plugins/store/src/store.rs | 74 ++++--- 11 files changed, 204 insertions(+), 157 deletions(-) create mode 100644 plugins/store/permissions/autogenerated/commands/get_store.toml diff --git a/plugins/store/api-iife.js b/plugins/store/api-iife.js index 77295d7f..3582aa3b 100644 --- a/plugins/store/api-iife.js +++ b/plugins/store/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_STORE__=function(e){"use strict";var t,r;function a(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}async function i(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;class n{get rid(){return function(e,t,r,a){if("a"===r&&!a)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?a:"a"===r?a.call(e):a?a.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,r,a,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");t.set(e,r)}(this,t,e)}async close(){return i("plugin:resources|close",{rid:this.rid})}}async function s(e,t,r){const n={kind:"Any"};return i("plugin:event|listen",{event:e,target:n,handler:a(t)}).then((t=>async()=>async function(e,t){await i("plugin:event|unlisten",{event:e,eventId:t})}(e,t)))}t=new WeakMap,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(r||(r={}));class o extends n{constructor(e,t){super(e),this.path=t}async set(e,t){await i("plugin:store|set",{rid:this.rid,key:e,value:t})}async get(e){return await i("plugin:store|get",{rid:this.rid,key:e})}async has(e){return await i("plugin:store|has",{rid:this.rid,key:e})}async delete(e){return await i("plugin:store|delete",{rid:this.rid,key:e})}async clear(){await i("plugin:store|clear",{rid:this.rid})}async reset(){await i("plugin:store|reset",{rid:this.rid})}async keys(){return await i("plugin:store|keys",{rid:this.rid})}async values(){return await i("plugin:store|values",{rid:this.rid})}async entries(){return await i("plugin:store|entries",{rid:this.rid})}async length(){return await i("plugin:store|length",{rid:this.rid})}async load(){await i("plugin:store|load",{rid:this.rid})}async save(){await i("plugin:store|save",{rid:this.rid})}async onKeyChange(e,t){return await s("store://change",(r=>{r.payload.path===this.path&&r.payload.key===e&&t(r.payload.value)}))}async onChange(e){return await s("store://change",(t=>{t.payload.path===this.path&&e(t.payload.key,t.payload.value)}))}}return e.Store=o,e.createStore=async function(e,t){const r=await i("plugin:store|create_store",{path:e,...t});return new o(r,e)},e}({});Object.defineProperty(window.__TAURI__,"store",{value:__TAURI_PLUGIN_STORE__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_STORE__=function(e){"use strict";var t,r;function a(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}async function i(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;class n{get rid(){return function(e,t,r,a){if("a"===r&&!a)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?a:"a"===r?a.call(e):a?a.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,r,a,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");t.set(e,r)}(this,t,e)}async close(){return i("plugin:resources|close",{rid:this.rid})}}async function s(e,t,r){const n={kind:"Any"};return i("plugin:event|listen",{event:e,target:n,handler:a(t)}).then((t=>async()=>async function(e,t){await i("plugin:event|unlisten",{event:e,eventId:t})}(e,t)))}t=new WeakMap,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(r||(r={}));class o extends n{constructor(e,t){super(e),this.path=t}async set(e,t){await i("plugin:store|set",{rid:this.rid,key:e,value:t})}async get(e){return await i("plugin:store|get",{rid:this.rid,key:e})}async has(e){return await i("plugin:store|has",{rid:this.rid,key:e})}async delete(e){return await i("plugin:store|delete",{rid:this.rid,key:e})}async clear(){await i("plugin:store|clear",{rid:this.rid})}async reset(){await i("plugin:store|reset",{rid:this.rid})}async keys(){return await i("plugin:store|keys",{rid:this.rid})}async values(){return await i("plugin:store|values",{rid:this.rid})}async entries(){return await i("plugin:store|entries",{rid:this.rid})}async length(){return await i("plugin:store|length",{rid:this.rid})}async load(){await i("plugin:store|load",{rid:this.rid})}async save(){await i("plugin:store|save",{rid:this.rid})}async onKeyChange(e,t){return await s("store://change",(r=>{r.payload.path===this.path&&r.payload.key===e&&t(r.payload.value)}))}async onChange(e){return await s("store://change",(t=>{t.payload.path===this.path&&e(t.payload.key,t.payload.value)}))}}return e.Store=o,e.createStore=async function(e,t){const r=await i("plugin:store|create_store",{path:e,...t});return new o(r,e)},e.getStore=async function(e){const t=await i("plugin:store|get_store");return new o(t,e)},e}({});Object.defineProperty(window.__TAURI__,"store",{value:__TAURI_PLUGIN_STORE__})} diff --git a/plugins/store/build.rs b/plugins/store/build.rs index 3c9fee01..a144df29 100644 --- a/plugins/store/build.rs +++ b/plugins/store/build.rs @@ -4,6 +4,7 @@ const COMMANDS: &[&str] = &[ "create_store", + "get_store", "set", "get", "has", diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs b/plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs index 0dd4e0bc..45ec5989 100644 --- a/plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs +++ b/plugins/store/examples/AppSettingsManager/src-tauri/src/main.rs @@ -8,6 +8,7 @@ use std::time::Duration; use serde_json::json; +use tauri::Listener; use tauri_plugin_store::StoreExt; mod app; @@ -22,13 +23,13 @@ fn main() { .handle() .store_builder("settings.json") .auto_save(Duration::from_millis(100)) - .build(); - - // If there are no saved settings yet, this will return an error so we ignore the return value. - let _ = store.load(); - + .build() + .unwrap(); + app.share_store(store.clone()); + app.listen("store://change", |event| { + dbg!(event); + }); let app_settings = AppSettings::load_from_store(&store); - match app_settings { Ok(app_settings) => { let theme = app_settings.theme; diff --git a/plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json b/plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json index 5a67883d..d3f60daa 100644 --- a/plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json +++ b/plugins/store/examples/AppSettingsManager/src-tauri/tauri.conf.json @@ -3,6 +3,7 @@ "version": "0.1.0", "identifier": "com.tauri.app-settings-manager", "build": { + "devUrl": "http://localhost:1420", "frontendDist": "../dist" }, "app": { diff --git a/plugins/store/guest-js/index.ts b/plugins/store/guest-js/index.ts index 259b0662..d173c3f4 100644 --- a/plugins/store/guest-js/index.ts +++ b/plugins/store/guest-js/index.ts @@ -35,7 +35,15 @@ export async function createStore(path: string, options?: StoreOptions) { } /** - * A lazy loaded key-value store persisted by the backend layer. + * @param path: Path of the store in the rust side + */ +export async function getStore(path: string) { + const resourceId = await invoke('plugin:store|get_store') + return new Store(resourceId, path) +} + +/** + * A key-value store persisted by the backend layer. */ export class Store extends Resource { constructor( diff --git a/plugins/store/permissions/autogenerated/commands/get_store.toml b/plugins/store/permissions/autogenerated/commands/get_store.toml new file mode 100644 index 00000000..7c19173a --- /dev/null +++ b/plugins/store/permissions/autogenerated/commands/get_store.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-store" +description = "Enables the get_store command without any pre-configured scope." +commands.allow = ["get_store"] + +[[permission]] +identifier = "deny-get-store" +description = "Denies the get_store command without any pre-configured scope." +commands.deny = ["get_store"] diff --git a/plugins/store/permissions/autogenerated/reference.md b/plugins/store/permissions/autogenerated/reference.md index 4e9bf2cc..ce857346 100644 --- a/plugins/store/permissions/autogenerated/reference.md +++ b/plugins/store/permissions/autogenerated/reference.md @@ -165,6 +165,32 @@ Denies the get command without any pre-configured scope. +`store:allow-get-store` + + + + +Enables the get_store command without any pre-configured scope. + + + + + + + +`store:deny-get-store` + + + + +Denies the get_store command without any pre-configured scope. + + + + + + + `store:allow-has` diff --git a/plugins/store/permissions/schemas/schema.json b/plugins/store/permissions/schemas/schema.json index 6ebf788e..bfd50f9c 100644 --- a/plugins/store/permissions/schemas/schema.json +++ b/plugins/store/permissions/schemas/schema.json @@ -344,6 +344,16 @@ "type": "string", "const": "deny-get" }, + { + "description": "Enables the get_store command without any pre-configured scope.", + "type": "string", + "const": "allow-get-store" + }, + { + "description": "Denies the get_store command without any pre-configured scope.", + "type": "string", + "const": "deny-get-store" + }, { "description": "Enables the has command without any pre-configured scope.", "type": "string", diff --git a/plugins/store/src/error.rs b/plugins/store/src/error.rs index afd43add..612edcfd 100644 --- a/plugins/store/src/error.rs +++ b/plugins/store/src/error.rs @@ -21,9 +21,9 @@ pub enum Error { /// IO error. #[error(transparent)] Io(#[from] std::io::Error), - /// Store not found - #[error("Store \"{0}\" not found")] - NotFound(PathBuf), + /// Store already exists + #[error("Store at \"{0}\" already exists")] + AlreadyExists(PathBuf), /// Some Tauri API failed #[error(transparent)] Tauri(#[from] tauri::Error), diff --git a/plugins/store/src/lib.rs b/plugins/store/src/lib.rs index b05bf4b4..53c71ea1 100644 --- a/plugins/store/src/lib.rs +++ b/plugins/store/src/lib.rs @@ -18,13 +18,13 @@ pub use serde_json::Value as JsonValue; use std::{ collections::HashMap, path::{Path, PathBuf}, - sync::{Mutex, Weak}, + sync::{Arc, Mutex, Weak}, time::Duration, }; pub use store::{Store, StoreBuilder, StoreInner}; use tauri::{ plugin::{self, TauriPlugin}, - AppHandle, Manager, ResourceId, RunEvent, Runtime, Webview, + AppHandle, Manager, ResourceId, RunEvent, Runtime, }; mod error; @@ -38,138 +38,185 @@ struct ChangePayload<'a> { } pub struct StoreCollection { - stores: Mutex>>>>, - // frozen: bool, + /// This weak pointer is always pointing to a real reference since we will remove it on drop + stores: Mutex>, Option)>>, } #[tauri::command] async fn create_store( app: AppHandle, - webview: Webview, path: PathBuf, auto_save: Option, ) -> Result { - let mut builder = app.store_builder(path); + let mut builder = app.store_builder(path.clone()); if let Some(auto_save) = auto_save { builder = builder.auto_save(Duration::from_millis(auto_save)); } - let store = builder.build(); - Ok(webview.resources_table().add(store)) + let store = builder.build()?; + let rid = app.resources_table().add_arc(store); + let collection = app.state::>(); + let mut stores = collection.stores.lock().unwrap(); + if let Some((_, resource_id)) = stores.get_mut(&path) { + resource_id.replace(rid); + } + Ok(rid) +} + +#[tauri::command] +async fn get_store(app: AppHandle, path: PathBuf) -> Option { + let collection = app.state::>(); + let mut stores = collection.stores.lock().unwrap(); + if let Some((store, resource_id)) = stores.get_mut(&path) { + let rid = if let Some(resource_id) = resource_id { + *resource_id + } else { + let rid = app.resources_table().add_arc(store.upgrade().unwrap()); + resource_id.replace(rid); + rid + }; + Some(rid) + } else { + None + } } #[tauri::command] async fn set( - webview: Webview, + app: AppHandle, rid: ResourceId, key: String, value: JsonValue, ) -> Result<()> { - let store = webview.resources_table().get::>(rid)?; + let store = app.resources_table().get::>(rid)?; store.set(key, value); Ok(()) } #[tauri::command] async fn get( - webview: Webview, + app: AppHandle, rid: ResourceId, key: String, ) -> Result> { - let store = webview.resources_table().get::>(rid)?; + let store = app.resources_table().get::>(rid)?; Ok(store.get(key)) } #[tauri::command] -async fn has(webview: Webview, rid: ResourceId, key: String) -> Result { - let store = webview.resources_table().get::>(rid)?; +async fn has(app: AppHandle, rid: ResourceId, key: String) -> Result { + let store = app.resources_table().get::>(rid)?; Ok(store.has(key)) } #[tauri::command] -async fn delete(webview: Webview, rid: ResourceId, key: String) -> Result { - let store = webview.resources_table().get::>(rid)?; +async fn delete(app: AppHandle, rid: ResourceId, key: String) -> Result { + let store = app.resources_table().get::>(rid)?; Ok(store.delete(key)) } #[tauri::command] -async fn clear(webview: Webview, rid: ResourceId) -> Result<()> { - let store = webview.resources_table().get::>(rid)?; +async fn clear(app: AppHandle, rid: ResourceId) -> Result<()> { + let store = app.resources_table().get::>(rid)?; store.clear(); Ok(()) } #[tauri::command] -async fn reset(webview: Webview, rid: ResourceId) -> Result<()> { - let store = webview.resources_table().get::>(rid)?; +async fn reset(app: AppHandle, rid: ResourceId) -> Result<()> { + let store = app.resources_table().get::>(rid)?; store.reset(); Ok(()) } #[tauri::command] -async fn keys(webview: Webview, rid: ResourceId) -> Result> { - let store = webview.resources_table().get::>(rid)?; +async fn keys(app: AppHandle, rid: ResourceId) -> Result> { + let store = app.resources_table().get::>(rid)?; Ok(store.keys()) } #[tauri::command] -async fn values(webview: Webview, rid: ResourceId) -> Result> { - let store = webview.resources_table().get::>(rid)?; +async fn values(app: AppHandle, rid: ResourceId) -> Result> { + let store = app.resources_table().get::>(rid)?; Ok(store.values()) } #[tauri::command] async fn entries( - webview: Webview, + app: AppHandle, rid: ResourceId, ) -> Result> { - let store = webview.resources_table().get::>(rid)?; + let store = app.resources_table().get::>(rid)?; Ok(store.entries()) } #[tauri::command] -async fn length(webview: Webview, rid: ResourceId) -> Result { - let store = webview.resources_table().get::>(rid)?; +async fn length(app: AppHandle, rid: ResourceId) -> Result { + let store = app.resources_table().get::>(rid)?; Ok(store.length()) } #[tauri::command] -async fn load(webview: Webview, rid: ResourceId) -> Result<()> { - let store = webview.resources_table().get::>(rid)?; +async fn load(app: AppHandle, rid: ResourceId) -> Result<()> { + let store = app.resources_table().get::>(rid)?; store.load() } #[tauri::command] -async fn save(webview: Webview, rid: ResourceId) -> Result<()> { - let store = webview.resources_table().get::>(rid)?; +async fn save(app: AppHandle, rid: ResourceId) -> Result<()> { + let store = app.resources_table().get::>(rid)?; store.save() } pub trait StoreExt { - fn store(&self, path: impl AsRef) -> Store; + fn create_store(&self, path: impl AsRef) -> Result>>; fn store_builder(&self, path: impl AsRef) -> StoreBuilder; + fn share_store(&self, store: Arc>); + fn get_store(&self, path: impl AsRef) -> Option>>; } impl> StoreExt for T { - fn store(&self, path: impl AsRef) -> Store { + fn create_store(&self, path: impl AsRef) -> Result>> { StoreBuilder::new(self.app_handle(), path).build() } fn store_builder(&self, path: impl AsRef) -> StoreBuilder { StoreBuilder::new(self.app_handle(), path) } + + fn share_store(&self, store: Arc>) { + let collection = self.state::>(); + let mut stores = collection.stores.lock().unwrap(); + if let Some(path) = store.with_store(|inner_store| { + if stores.contains_key(&inner_store.path) { + None + } else { + Some(inner_store.path.clone()) + } + }) { + let weak_store = Arc::downgrade(&store); + let rid = self.resources_table().add_arc(store); + stores.insert(path, (weak_store, Some(rid))); + } + } + + fn get_store(&self, path: impl AsRef) -> Option>> { + let collection = self.state::>(); + let stores = collection.stores.lock().unwrap(); + stores + .get(path.as_ref()) + .and_then(|(store, _)| store.upgrade()) + } } // #[derive(Default)] pub struct Builder { stores: HashMap>, - // frozen: bool, } impl Default for Builder { fn default() -> Self { Self { stores: Default::default(), - // frozen: false, } } } @@ -179,68 +226,6 @@ impl Builder { Self::default() } - // /// Registers a store with the plugin. - // /// - // /// # Examples - // /// - // /// ``` - // /// use tauri_plugin_store::{StoreBuilder, Builder}; - // /// - // /// tauri::Builder::default() - // /// .setup(|app| { - // /// let store = StoreBuilder::new("store.bin").build(app.handle().clone()); - // /// let builder = Builder::default().store(store); - // /// Ok(()) - // /// }); - // /// ``` - // pub fn store(mut self, store: Store) -> Self { - // self.stores.insert(store.path.clone(), store); - // self - // } - - // /// Registers multiple stores with the plugin. - // /// - // /// # Examples - // /// - // /// ``` - // /// use tauri_plugin_store::{StoreBuilder, Builder}; - // /// - // /// tauri::Builder::default() - // /// .setup(|app| { - // /// let store = StoreBuilder::new("store.bin").build(app.handle().clone()); - // /// let builder = Builder::default().stores([store]); - // /// Ok(()) - // /// }); - // /// ``` - // pub fn stores>>(mut self, stores: T) -> Self { - // self.stores = stores - // .into_iter() - // .map(|store| (store.path.clone(), store)) - // .collect(); - // self - // } - - // /// Freezes the collection. - // /// - // /// This causes requests for plugins that haven't been registered to fail - // /// - // /// # Examples - // /// - // /// ``` - // /// use tauri_plugin_store::{StoreBuilder, Builder}; - // /// - // /// tauri::Builder::default() - // /// .setup(|app| { - // /// let store = StoreBuilder::new("store.bin").build(app.handle().clone()); - // /// app.handle().plugin(Builder::default().freeze().build()); - // /// Ok(()) - // /// }); - // /// ``` - // pub fn freeze(mut self) -> Self { - // self.frozen = true; - // self - // } - /// Builds the plugin. /// /// # Examples @@ -249,7 +234,7 @@ impl Builder { /// tauri::Builder::default() /// .plugin(tauri_plugin_store::Builder::default().build()) /// .setup(|app| { - /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin").build(); + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin").build()?; /// Ok(()) /// }); /// ``` @@ -257,6 +242,7 @@ impl Builder { plugin::Builder::new("store") .invoke_handler(tauri::generate_handler![ create_store, + get_store, set, get, has, @@ -282,23 +268,20 @@ impl Builder { app_handle.manage(StoreCollection:: { stores: Mutex::new(HashMap::new()), - // frozen: self.frozen, }); Ok(()) }) - .on_event(|_app_handle, event| { + .on_event(|app_handle, event| { if let RunEvent::Exit = event { - // let collection = app_handle.state::>(); - - // for store in collection.stores.lock().expect("mutex poisoned").values_mut() { - // if let Some(sender) = store.auto_save_debounce_sender.take() { - // let _ = sender.send(AutoSaveMessage::Cancel); - // } - // if let Err(err) = store.save() { - // eprintln!("failed to save store {:?} with error {:?}", store.path, err); - // } - // } + let collection = app_handle.state::>(); + let stores = collection.stores.lock().unwrap(); + for (path, (store, _)) in stores.iter() { + let store = store.upgrade().unwrap(); + if let Err(err) = store.save() { + eprintln!("failed to save store {path:?} with error {err:?}"); + } + } } }) .build() diff --git a/plugins/store/src/store.rs b/plugins/store/src/store.rs index d610525d..542a52de 100644 --- a/plugins/store/src/store.rs +++ b/plugins/store/src/store.rs @@ -41,7 +41,6 @@ pub struct StoreBuilder { app: AppHandle, path: PathBuf, defaults: Option>, - cache: HashMap, serialize: SerializeFn, deserialize: DeserializeFn, auto_save: Option, @@ -65,7 +64,6 @@ impl StoreBuilder { // Since Store.path is only exposed to the user in emit calls we may as well simplify it here already. path: dunce::simplified(path.as_ref()).to_path_buf(), defaults: None, - cache: Default::default(), serialize: default_serialize, deserialize: default_deserialize, auto_save: None, @@ -84,12 +82,11 @@ impl StoreBuilder { /// /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin") /// .defaults(defaults) - /// .build(); + /// .build()?; /// Ok(()) /// }); /// ``` pub fn defaults(mut self, defaults: HashMap) -> Self { - self.cache.clone_from(&defaults); self.defaults = Some(defaults); self } @@ -103,14 +100,13 @@ impl StoreBuilder { /// .setup(|app| { /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin") /// .default("foo".to_string(), "bar") - /// .build(); + /// .build()?; /// Ok(()) /// }); /// ``` pub fn default(mut self, key: impl Into, value: impl Into) -> Self { let key = key.into(); let value = value.into(); - self.cache.insert(key.clone(), value.clone()); self.defaults .get_or_insert(HashMap::new()) .insert(key, value); @@ -126,7 +122,7 @@ impl StoreBuilder { /// .setup(|app| { /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json") /// .serialize(|cache| serde_json::to_vec(&cache).map_err(Into::into)) - /// .build(); + /// .build()?; /// Ok(()) /// }); /// ``` @@ -144,7 +140,7 @@ impl StoreBuilder { /// .setup(|app| { /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json") /// .deserialize(|bytes| serde_json::from_slice(&bytes).map_err(Into::into)) - /// .build(); + /// .build()?; /// Ok(()) /// }); /// ``` @@ -155,17 +151,14 @@ impl StoreBuilder { /// Auto save on modified with a debounce duration /// - /// Note: only works if this store is managed by the plugin (e.g. made using [`crate::with_store`] or inserted into [`crate::Builder`]) - /// /// # Examples /// ``` - /// /// tauri::Builder::default() /// .plugin(tauri_plugin_store::Builder::default().build()) /// .setup(|app| { /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json") /// .auto_save(std::time::Duration::from_millis(100)) - /// .build(); + /// .build()?; /// Ok(()) /// }); /// ``` @@ -181,35 +174,35 @@ impl StoreBuilder { /// tauri::Builder::default() /// .plugin(tauri_plugin_store::Builder::default().build()) /// .setup(|app| { - /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json").build(); + /// let store = tauri_plugin_store::StoreBuilder::new(app, "store.json").build()?; /// Ok(()) /// }); /// ``` - pub fn build(self) -> Store { + pub fn build(self) -> crate::Result>> { let collection = self.app.state::>(); let mut stores = collection.stores.lock().unwrap(); - let store = stores - .get(&self.path) - .and_then(|store| store.upgrade()) - .unwrap_or_else(|| { - let mut store = StoreInner::new(self.app.clone(), self.path.clone()); - let _ = store.load(self.deserialize); - let store = Arc::new(Mutex::new(store)); - stores.insert( - self.path.clone(), - Arc::>>::downgrade(&store), - ); - store - }); - drop(stores); - Store { + + if stores.contains_key(&self.path) { + return Err(crate::Error::AlreadyExists(self.path)); + } + + let mut store_inner = StoreInner::new(self.app.clone(), self.path.clone()); + if let Some(defaults) = &self.defaults { + store_inner.cache.clone_from(defaults); + } + let _ = store_inner.load(self.deserialize); + + let store = Store { defaults: self.defaults, serialize: self.serialize, deserialize: self.deserialize, auto_save: self.auto_save, auto_save_debounce_sender: Arc::new(Mutex::new(None)), - store, - } + store: Arc::new(Mutex::new(store_inner)), + }; + let store = Arc::new(store); + stores.insert(self.path, (Arc::downgrade(&store), None)); + Ok(store) } } @@ -372,10 +365,7 @@ pub struct Store { impl Resource for Store {} impl Store { - pub fn with_store( - &self, - f: impl FnOnce(&mut StoreInner) -> crate::Result, - ) -> crate::Result { + pub fn with_store(&self, f: impl FnOnce(&mut StoreInner) -> T) -> T { let mut store = self.store.lock().unwrap(); f(&mut store) } @@ -480,3 +470,17 @@ impl Store { Ok(()) } } + +impl Drop for Store { + fn drop(&mut self) { + let store = self.store.lock().unwrap(); + // Cancel and save if auto save is pending + if let Some(sender) = self.auto_save_debounce_sender.lock().unwrap().take() { + let _ = sender.send(AutoSaveMessage::Cancel); + let _ = store.save(self.serialize); + }; + let collection = store.app.state::>(); + let mut stores = collection.stores.lock().unwrap(); + stores.remove(&store.path); + } +}