1#![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
243pub trait StoreExt<R: Runtime> {
249 fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>>;
266 fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R>;
285 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
344pub 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 pub fn new() -> Self {
379 Self::default()
380 }
381
382 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 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 pub fn default_serialize_fn(mut self, serialize_fn: SerializeFn) -> Self {
430 self.default_serialize = serialize_fn;
431 self
432 }
433
434 pub fn default_deserialize_fn(mut self, deserialize_fn: DeserializeFn) -> Self {
436 self.default_deserialize = deserialize_fn;
437 self
438 }
439
440 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}