Skip to main content

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, RwLock},
19    time::Duration,
20};
21pub use store::{DeserializeFn, SerializeFn, Store, StoreBuilder, resolve_store_path};
22use tauri::{
23    AppHandle, Manager, ResourceId, RunEvent, Runtime, State,
24    plugin::{self, TauriPlugin},
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<RwLock<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.read().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
243/// Extension trait to access the store APIs on a [`Manager`] such as `App`, `AppHandle`,
244/// `WebviewWindow` or `Window`.
245///
246/// The plugin must be registered with [`Builder::build`] for these methods to work,
247/// as they rely on the state it manages.
248pub trait StoreExt<R: Runtime> {
249    /// Create a store or load an existing store with default settings at the given path.
250    ///
251    /// If the store is already loaded, its instance is automatically returned.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use tauri_plugin_store::StoreExt;
257    ///
258    /// tauri::Builder::default()
259    ///   .plugin(tauri_plugin_store::Builder::default().build())
260    ///   .setup(|app| {
261    ///     let store = app.store("my-store")?;
262    ///     Ok(())
263    ///   });
264    /// ```
265    fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>>;
266    /// Get a store builder.
267    ///
268    /// The builder can be used to configure the store.
269    /// To use the default settings see [`Self::store`].
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use tauri_plugin_store::StoreExt;
275    /// use std::time::Duration;
276    ///
277    /// tauri::Builder::default()
278    ///   .plugin(tauri_plugin_store::Builder::default().build())
279    ///   .setup(|app| {
280    ///     let store = app.store_builder("users.json").auto_save(Duration::from_secs(1)).build()?;
281    ///     Ok(())
282    ///   });
283    /// ```
284    fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R>;
285    /// Get a handle of an already loaded store.
286    ///
287    /// If the store is not loaded or does not exist, it returns `None`.
288    ///
289    /// Note that using this function can cause race conditions if you fallback to creating or loading the store,
290    /// so you should consider using [`Self::store`] if you are not sure if the store is loaded or not.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use tauri_plugin_store::StoreExt;
296    ///
297    /// tauri::Builder::default()
298    ///   .plugin(tauri_plugin_store::Builder::default().build())
299    ///   .setup(|app| {
300    ///     let store = if let Some(s) = app.get_store("store.json") {
301    ///       s
302    ///     } else {
303    ///       // this is not thread safe; if another thread is doing the same load/create,
304    ///       // there will be a race condition; in this case we could remove the get_store
305    ///       // and only run app.store() as it will return the existing store if it has been loaded
306    ///       app.store("store.json")?
307    ///     };
308    ///     Ok(())
309    ///   });
310    /// ```
311    fn get_store(&self, path: impl AsRef<Path>) -> Option<Arc<Store<R>>>;
312}
313
314impl<R: Runtime, T: Manager<R>> StoreExt<R> for T {
315    fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>> {
316        StoreBuilder::new(self.app_handle(), path).build()
317    }
318
319    fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R> {
320        StoreBuilder::new(self.app_handle(), path)
321    }
322
323    fn get_store(&self, path: impl AsRef<Path>) -> Option<Arc<Store<R>>> {
324        let collection = self.state::<StoreState>();
325        let stores = collection.stores.read().unwrap();
326        stores
327            .get(&resolve_store_path(self.app_handle(), path.as_ref()).ok()?)
328            .and_then(|rid| self.resources_table().get(*rid).ok())
329    }
330}
331
332fn default_serialize(
333    cache: &HashMap<String, JsonValue>,
334) -> std::result::Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
335    Ok(serde_json::to_vec_pretty(&cache)?)
336}
337
338fn default_deserialize(
339    bytes: &[u8],
340) -> std::result::Result<HashMap<String, JsonValue>, Box<dyn std::error::Error + Send + Sync>> {
341    serde_json::from_slice(bytes).map_err(Into::into)
342}
343
344/// Builder for the store plugin.
345///
346/// It is used to register custom serialize and deserialize functions the frontend can select by
347/// name when loading a store, and to change the functions used by default (pretty printed JSON).
348///
349/// # Examples
350///
351/// ```
352/// tauri::Builder::default()
353///   .plugin(tauri_plugin_store::Builder::default().build());
354/// ```
355pub struct Builder {
356    serialize_fns: HashMap<String, SerializeFn>,
357    deserialize_fns: HashMap<String, DeserializeFn>,
358    default_serialize: SerializeFn,
359    default_deserialize: DeserializeFn,
360}
361
362impl Default for Builder {
363    fn default() -> Self {
364        Self {
365            serialize_fns: Default::default(),
366            deserialize_fns: Default::default(),
367            default_serialize,
368            default_deserialize,
369        }
370    }
371}
372
373impl Builder {
374    /// Creates a new builder using the default serialize and deserialize functions,
375    /// which read and write pretty printed JSON.
376    ///
377    /// This is the same as [`Builder::default`].
378    pub fn new() -> Self {
379        Self::default()
380    }
381
382    /// Register a serialize function to access it from the JavaScript side
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// fn no_pretty_json(
388    ///     cache: &std::collections::HashMap<String, serde_json::Value>,
389    /// ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
390    ///     Ok(serde_json::to_vec(&cache)?)
391    /// }
392    ///
393    /// tauri::Builder::default()
394    ///     .plugin(
395    ///         tauri_plugin_store::Builder::default()
396    ///             .register_serialize_fn("no-pretty-json".to_owned(), no_pretty_json)
397    ///             .build(),
398    ///     );
399    /// ```
400    pub fn register_serialize_fn(mut self, name: String, serialize_fn: SerializeFn) -> Self {
401        self.serialize_fns.insert(name, serialize_fn);
402        self
403    }
404
405    /// Register a deserialize function to access it from the JavaScript side
406    pub fn register_deserialize_fn(mut self, name: String, deserialize_fn: DeserializeFn) -> Self {
407        self.deserialize_fns.insert(name, deserialize_fn);
408        self
409    }
410
411    /// Use this serialize function for stores by default
412    ///
413    /// # Examples
414    ///
415    /// ```
416    /// fn no_pretty_json(
417    ///     cache: &std::collections::HashMap<String, serde_json::Value>,
418    /// ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
419    ///     Ok(serde_json::to_vec(&cache)?)
420    /// }
421    ///
422    /// tauri::Builder::default()
423    ///     .plugin(
424    ///         tauri_plugin_store::Builder::default()
425    ///             .default_serialize_fn(no_pretty_json)
426    ///             .build(),
427    ///     );
428    /// ```
429    pub fn default_serialize_fn(mut self, serialize_fn: SerializeFn) -> Self {
430        self.default_serialize = serialize_fn;
431        self
432    }
433
434    /// Use this deserialize function for stores by default
435    pub fn default_deserialize_fn(mut self, deserialize_fn: DeserializeFn) -> Self {
436        self.default_deserialize = deserialize_fn;
437        self
438    }
439
440    /// Builds the plugin.
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// tauri::Builder::default()
446    ///   .plugin(tauri_plugin_store::Builder::default().build())
447    ///   .setup(|app| {
448    ///     let store = tauri_plugin_store::StoreBuilder::new(app, "store.bin").build()?;
449    ///     Ok(())
450    ///   });
451    /// ```
452    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
453        plugin::Builder::new("store")
454            .invoke_handler(tauri::generate_handler![
455                load, get_store, set, get, has, delete, clear, reset, keys, values, length,
456                entries, reload, save,
457            ])
458            .setup(move |app_handle, _api| {
459                app_handle.manage(StoreState {
460                    stores: Arc::new(RwLock::new(HashMap::new())),
461                    serialize_fns: self.serialize_fns,
462                    deserialize_fns: self.deserialize_fns,
463                    default_serialize: self.default_serialize,
464                    default_deserialize: self.default_deserialize,
465                });
466                Ok(())
467            })
468            .on_event(|app_handle, event| {
469                if let RunEvent::Exit = event {
470                    let collection = app_handle.state::<StoreState>();
471                    let stores = collection.stores.read().unwrap();
472                    for (path, rid) in stores.iter() {
473                        if let Ok(store) = app_handle.resources_table().get::<Store<R>>(*rid)
474                            && let Err(err) = store.save()
475                        {
476                            tracing::error!("failed to save store {path:?} with error {err:?}");
477                        }
478                    }
479                }
480            })
481            .build()
482    }
483}