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