tauri_plugin_store/
lib.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Simple, persistent key-value store.
6
7#![doc(
8    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
9    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
10)]
11
12pub use error::{Error, Result};
13use serde::{Deserialize, Serialize};
14pub use serde_json::Value as JsonValue;
15use std::{
16    collections::HashMap,
17    path::{Path, PathBuf},
18    sync::{Arc, Mutex},
19    time::Duration,
20};
21pub use store::{resolve_store_path, DeserializeFn, SerializeFn, Store, StoreBuilder};
22use tauri::{
23    plugin::{self, TauriPlugin},
24    AppHandle, Manager, ResourceId, RunEvent, Runtime, State,
25};
26
27mod error;
28mod store;
29
30#[derive(Serialize, Clone)]
31#[serde(rename_all = "camelCase")]
32struct ChangePayload<'a> {
33    path: &'a Path,
34    resource_id: Option<u32>,
35    key: &'a str,
36    value: Option<&'a JsonValue>,
37    exists: bool,
38}
39
40#[derive(Debug)]
41struct StoreState {
42    stores: Arc<Mutex<HashMap<PathBuf, ResourceId>>>,
43    serialize_fns: HashMap<String, SerializeFn>,
44    deserialize_fns: HashMap<String, DeserializeFn>,
45    default_serialize: SerializeFn,
46    default_deserialize: DeserializeFn,
47}
48
49#[derive(Serialize, Deserialize)]
50#[serde(untagged)]
51enum AutoSave {
52    DebounceDuration(u64),
53    Bool(bool),
54}
55
56#[derive(Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58struct LoadStoreOptions {
59    defaults: Option<HashMap<String, JsonValue>>,
60    auto_save: Option<AutoSave>,
61    serialize_fn_name: Option<String>,
62    deserialize_fn_name: Option<String>,
63    #[serde(default)]
64    create_new: bool,
65    #[serde(default)]
66    override_defaults: bool,
67}
68
69fn builder<R: Runtime>(
70    app: AppHandle<R>,
71    store_state: State<'_, StoreState>,
72    path: PathBuf,
73    options: Option<LoadStoreOptions>,
74) -> Result<StoreBuilder<R>> {
75    let mut builder = app.store_builder(path);
76
77    let Some(options) = options else {
78        return Ok(builder);
79    };
80
81    if let Some(defaults) = options.defaults {
82        builder = builder.defaults(defaults);
83    }
84
85    if let Some(auto_save) = options.auto_save {
86        match auto_save {
87            AutoSave::DebounceDuration(duration) => {
88                builder = builder.auto_save(Duration::from_millis(duration));
89            }
90            AutoSave::Bool(false) => {
91                builder = builder.disable_auto_save();
92            }
93            _ => {}
94        }
95    }
96
97    if let Some(serialize_fn_name) = options.serialize_fn_name {
98        let serialize_fn = store_state
99            .serialize_fns
100            .get(&serialize_fn_name)
101            .ok_or_else(|| crate::Error::SerializeFunctionNotFound(serialize_fn_name))?;
102        builder = builder.serialize(*serialize_fn);
103    }
104
105    if let Some(deserialize_fn_name) = options.deserialize_fn_name {
106        let deserialize_fn = store_state
107            .deserialize_fns
108            .get(&deserialize_fn_name)
109            .ok_or_else(|| crate::Error::DeserializeFunctionNotFound(deserialize_fn_name))?;
110        builder = builder.deserialize(*deserialize_fn);
111    }
112
113    if options.create_new {
114        builder = builder.create_new();
115    }
116
117    if options.override_defaults {
118        builder = builder.override_defaults();
119    }
120
121    Ok(builder)
122}
123
124#[tauri::command]
125async fn load<R: Runtime>(
126    app: AppHandle<R>,
127    store_state: State<'_, StoreState>,
128    path: PathBuf,
129    options: Option<LoadStoreOptions>,
130) -> Result<ResourceId> {
131    let builder = builder(app, store_state, path, options)?;
132    let (_, rid) = builder.build_inner()?;
133    Ok(rid)
134}
135
136#[tauri::command]
137async fn get_store<R: Runtime>(
138    app: AppHandle<R>,
139    store_state: State<'_, StoreState>,
140    path: PathBuf,
141) -> Result<Option<ResourceId>> {
142    let stores = store_state.stores.lock().unwrap();
143    Ok(stores.get(&resolve_store_path(&app, path)?).copied())
144}
145
146#[tauri::command]
147async fn set<R: Runtime>(
148    app: AppHandle<R>,
149    rid: ResourceId,
150    key: String,
151    value: JsonValue,
152) -> Result<()> {
153    let store = app.resources_table().get::<Store<R>>(rid)?;
154    store.set(key, value);
155    Ok(())
156}
157
158#[tauri::command]
159async fn get<R: Runtime>(
160    app: AppHandle<R>,
161    rid: ResourceId,
162    key: String,
163) -> Result<(Option<JsonValue>, bool)> {
164    let store = app.resources_table().get::<Store<R>>(rid)?;
165    let value = store.get(key);
166    let exists = value.is_some();
167    Ok((value, exists))
168}
169
170#[tauri::command]
171async fn has<R: Runtime>(app: AppHandle<R>, rid: ResourceId, key: String) -> Result<bool> {
172    let store = app.resources_table().get::<Store<R>>(rid)?;
173    Ok(store.has(key))
174}
175
176#[tauri::command]
177async fn delete<R: Runtime>(app: AppHandle<R>, rid: ResourceId, key: String) -> Result<bool> {
178    let store = app.resources_table().get::<Store<R>>(rid)?;
179    Ok(store.delete(key))
180}
181
182#[tauri::command]
183async fn clear<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
184    let store = app.resources_table().get::<Store<R>>(rid)?;
185    store.clear();
186    Ok(())
187}
188
189#[tauri::command]
190async fn reset<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
191    let store = app.resources_table().get::<Store<R>>(rid)?;
192    store.reset();
193    Ok(())
194}
195
196#[tauri::command]
197async fn keys<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<Vec<String>> {
198    let store = app.resources_table().get::<Store<R>>(rid)?;
199    Ok(store.keys())
200}
201
202#[tauri::command]
203async fn values<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<Vec<JsonValue>> {
204    let store = app.resources_table().get::<Store<R>>(rid)?;
205    Ok(store.values())
206}
207
208#[tauri::command]
209async fn entries<R: Runtime>(
210    app: AppHandle<R>,
211    rid: ResourceId,
212) -> Result<Vec<(String, JsonValue)>> {
213    let store = app.resources_table().get::<Store<R>>(rid)?;
214    Ok(store.entries())
215}
216
217#[tauri::command]
218async fn length<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<usize> {
219    let store = app.resources_table().get::<Store<R>>(rid)?;
220    Ok(store.length())
221}
222
223#[tauri::command]
224async fn reload<R: Runtime>(
225    app: AppHandle<R>,
226    rid: ResourceId,
227    ignore_defaults: Option<bool>,
228) -> Result<()> {
229    let store = app.resources_table().get::<Store<R>>(rid)?;
230    if ignore_defaults.unwrap_or_default() {
231        store.reload_ignore_defaults()
232    } else {
233        store.reload()
234    }
235}
236
237#[tauri::command]
238async fn save<R: Runtime>(app: AppHandle<R>, rid: ResourceId) -> Result<()> {
239    let store = app.resources_table().get::<Store<R>>(rid)?;
240    store.save()
241}
242
243pub trait StoreExt<R: Runtime> {
244    /// Create a store or load an existing store with default settings at the given path.
245    ///
246    /// If the store is already loaded, its instance is automatically returned.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use tauri_plugin_store::StoreExt;
252    ///
253    /// tauri::Builder::default()
254    ///   .plugin(tauri_plugin_store::Builder::default().build())
255    ///   .setup(|app| {
256    ///     let store = app.store("my-store")?;
257    ///     Ok(())
258    ///   });
259    /// ```
260    fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>>;
261    /// Get a store builder.
262    ///
263    /// The builder can be used to configure the store.
264    /// To use the default settings see [`Self::store`].
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use tauri_plugin_store::StoreExt;
270    /// use std::time::Duration;
271    ///
272    /// tauri::Builder::default()
273    ///   .plugin(tauri_plugin_store::Builder::default().build())
274    ///   .setup(|app| {
275    ///     let store = app.store_builder("users.json").auto_save(Duration::from_secs(1)).build()?;
276    ///     Ok(())
277    ///   });
278    /// ```
279    fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R>;
280    /// Get a handle of an already loaded store.
281    ///
282    /// If the store is not loaded or does not exist, it returns `None`.
283    ///
284    /// Note that using this function can cause race conditions if you fallback to creating or loading the store,
285    /// so you should consider using [`Self::store`] if you are not sure if the store is loaded or not.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use tauri_plugin_store::StoreExt;
291    ///
292    /// tauri::Builder::default()
293    ///   .plugin(tauri_plugin_store::Builder::default().build())
294    ///   .setup(|app| {
295    ///     let store = if let Some(s) = app.get_store("store.json") {
296    ///       s
297    ///     } else {
298    ///       // this is not thread safe; if another thread is doing the same load/create,
299    ///       // there will be a race condition; in this case we could remove the get_store
300    ///       // and only run app.store() as it will return the existing store if it has been loaded
301    ///       app.store("store.json")?
302    ///     };
303    ///     Ok(())
304    ///   });
305    /// ```
306    fn get_store(&self, path: impl AsRef<Path>) -> Option<Arc<Store<R>>>;
307}
308
309impl<R: Runtime, T: Manager<R>> StoreExt<R> for T {
310    fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>> {
311        StoreBuilder::new(self.app_handle(), path).build()
312    }
313
314    fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R> {
315        StoreBuilder::new(self.app_handle(), path)
316    }
317
318    fn get_store(&self, path: impl AsRef<Path>) -> Option<Arc<Store<R>>> {
319        let collection = self.state::<StoreState>();
320        let stores = collection.stores.lock().unwrap();
321        stores
322            .get(&resolve_store_path(self.app_handle(), path.as_ref()).ok()?)
323            .and_then(|rid| self.resources_table().get(*rid).ok())
324    }
325}
326
327fn default_serialize(
328    cache: &HashMap<String, JsonValue>,
329) -> std::result::Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
330    Ok(serde_json::to_vec_pretty(&cache)?)
331}
332
333fn default_deserialize(
334    bytes: &[u8],
335) -> std::result::Result<HashMap<String, JsonValue>, Box<dyn std::error::Error + Send + Sync>> {
336    serde_json::from_slice(bytes).map_err(Into::into)
337}
338
339pub struct Builder {
340    serialize_fns: HashMap<String, SerializeFn>,
341    deserialize_fns: HashMap<String, DeserializeFn>,
342    default_serialize: SerializeFn,
343    default_deserialize: DeserializeFn,
344}
345
346impl Default for Builder {
347    fn default() -> Self {
348        Self {
349            serialize_fns: Default::default(),
350            deserialize_fns: Default::default(),
351            default_serialize,
352            default_deserialize,
353        }
354    }
355}
356
357impl Builder {
358    pub fn new() -> Self {
359        Self::default()
360    }
361
362    /// Register a serialize function to access it from the JavaScript side
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// fn no_pretty_json(
368    ///     cache: &std::collections::HashMap<String, serde_json::Value>,
369    /// ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
370    ///     Ok(serde_json::to_vec(&cache)?)
371    /// }
372    ///
373    /// tauri::Builder::default()
374    ///     .plugin(
375    ///         tauri_plugin_store::Builder::default()
376    ///             .register_serialize_fn("no-pretty-json".to_owned(), no_pretty_json)
377    ///             .build(),
378    ///     );
379    /// ```
380    pub fn register_serialize_fn(mut self, name: String, serialize_fn: SerializeFn) -> Self {
381        self.serialize_fns.insert(name, serialize_fn);
382        self
383    }
384
385    /// Register a deserialize function to access it from the JavaScript side
386    pub fn register_deserialize_fn(mut self, name: String, deserialize_fn: DeserializeFn) -> Self {
387        self.deserialize_fns.insert(name, deserialize_fn);
388        self
389    }
390
391    /// Use this serialize function for stores by default
392    ///
393    /// # Examples
394    ///
395    /// ```
396    /// fn no_pretty_json(
397    ///     cache: &std::collections::HashMap<String, serde_json::Value>,
398    /// ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
399    ///     Ok(serde_json::to_vec(&cache)?)
400    /// }
401    ///
402    /// tauri::Builder::default()
403    ///     .plugin(
404    ///         tauri_plugin_store::Builder::default()
405    ///             .default_serialize_fn(no_pretty_json)
406    ///             .build(),
407    ///     );
408    /// ```
409    pub fn default_serialize_fn(mut self, serialize_fn: SerializeFn) -> Self {
410        self.default_serialize = serialize_fn;
411        self
412    }
413
414    /// Use this deserialize function for stores by default
415    pub fn default_deserialize_fn(mut self, deserialize_fn: DeserializeFn) -> Self {
416        self.default_deserialize = deserialize_fn;
417        self
418    }
419
420    /// Builds the plugin.
421    ///
422    /// # Examples
423    ///
424    /// ```
425    /// tauri::Builder::default()
426    ///   .plugin(tauri_plugin_store::Builder::default().build())
427    ///   .setup(|app| {
428    ///     let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin").build()?;
429    ///     Ok(())
430    ///   });
431    /// ```
432    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
433        plugin::Builder::new("store")
434            .invoke_handler(tauri::generate_handler![
435                load, get_store, set, get, has, delete, clear, reset, keys, values, length,
436                entries, reload, save,
437            ])
438            .setup(move |app_handle, _api| {
439                app_handle.manage(StoreState {
440                    stores: Arc::new(Mutex::new(HashMap::new())),
441                    serialize_fns: self.serialize_fns,
442                    deserialize_fns: self.deserialize_fns,
443                    default_serialize: self.default_serialize,
444                    default_deserialize: self.default_deserialize,
445                });
446                Ok(())
447            })
448            .on_event(|app_handle, event| {
449                if let RunEvent::Exit = event {
450                    let collection = app_handle.state::<StoreState>();
451                    let stores = collection.stores.lock().unwrap();
452                    for (path, rid) in stores.iter() {
453                        if let Ok(store) = app_handle.resources_table().get::<Store<R>>(*rid) {
454                            if let Err(err) = store.save() {
455                                tracing::error!("failed to save store {path:?} with error {err:?}");
456                            }
457                        }
458                    }
459                }
460            })
461            .build()
462    }
463}