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, 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 fn store(&self, path: impl AsRef<Path>) -> Result<Arc<Store<R>>>;
261 fn store_builder(&self, path: impl AsRef<Path>) -> StoreBuilder<R>;
280 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 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 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 pub fn default_serialize_fn(mut self, serialize_fn: SerializeFn) -> Self {
410 self.default_serialize = serialize_fn;
411 self
412 }
413
414 pub fn default_deserialize_fn(mut self, deserialize_fn: DeserializeFn) -> Self {
416 self.default_deserialize = deserialize_fn;
417 self
418 }
419
420 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}