tauri_store/
store.rs

1mod id;
2mod options;
3mod resource;
4mod save;
5mod state;
6mod watch;
7
8use crate::collection::CollectionMarker;
9use crate::error::Result;
10use crate::manager::ManagerExt;
11use options::set_options;
12use save::{debounce, throttle, SaveHandle};
13use serde::de::DeserializeOwned;
14use serde_json::{json, Value as Json};
15use std::collections::HashMap;
16use std::fmt;
17use std::marker::PhantomData;
18use std::path::{Path, PathBuf};
19use std::sync::{Arc, OnceLock};
20use tauri::async_runtime::spawn_blocking;
21use tauri::{AppHandle, ResourceId, Runtime};
22use tauri_store_utils::{read_file, write_file};
23use watch::Watcher;
24
25use crate::event::{
26  emit, ConfigPayload, EventSource, StatePayload, STORE_CONFIG_CHANGE_EVENT,
27  STORE_STATE_CHANGE_EVENT,
28};
29
30pub use id::StoreId;
31pub use options::StoreOptions;
32pub(crate) use resource::StoreResource;
33pub use save::SaveStrategy;
34pub use state::StoreState;
35pub use watch::WatcherId;
36
37#[cfg(debug_assertions)]
38const FILE_EXTENSION: &str = "dev.json";
39#[cfg(not(debug_assertions))]
40const FILE_EXTENSION: &str = "json";
41
42type ResourceTuple<R, C> = (ResourceId, Arc<StoreResource<R, C>>);
43
44/// A key-value store that can persist its state to disk.
45pub struct Store<R, C>
46where
47  R: Runtime,
48  C: CollectionMarker,
49{
50  app: AppHandle<R>,
51  pub(crate) id: StoreId,
52  state: StoreState,
53  pub(crate) save_on_exit: bool,
54  save_on_change: bool,
55  save_strategy: Option<SaveStrategy>,
56  debounce_save_handle: OnceLock<SaveHandle<R>>,
57  throttle_save_handle: OnceLock<SaveHandle<R>>,
58  watchers: HashMap<WatcherId, Watcher<R>>,
59  phantom: PhantomData<C>,
60}
61
62impl<R, C> Store<R, C>
63where
64  R: Runtime,
65  C: CollectionMarker,
66{
67  pub(crate) fn load(app: &AppHandle<R>, id: impl AsRef<str>) -> Result<ResourceTuple<R, C>> {
68    let id = StoreId::from(id.as_ref());
69    let path = store_path::<R, C>(app, &id);
70    let state = read_file(&path).call()?;
71
72    #[allow(unused_mut)]
73    let mut store = Self {
74      app: app.clone(),
75      id,
76      state,
77      save_on_change: false,
78      save_on_exit: true,
79      save_strategy: None,
80      debounce_save_handle: OnceLock::new(),
81      throttle_save_handle: OnceLock::new(),
82      watchers: HashMap::new(),
83      phantom: PhantomData,
84    };
85
86    #[cfg(feature = "unstable-migration")]
87    store.run_pending_migrations(app)?;
88
89    Ok(StoreResource::create(app, store))
90  }
91
92  #[cfg(feature = "unstable-migration")]
93  fn run_pending_migrations(&mut self, app: &AppHandle<R>) -> Result<()> {
94    use crate::meta::Meta;
95
96    let collection = app.store_collection_with_marker::<C>();
97    let result = collection
98      .migrator
99      .lock()
100      .expect("migrator is poisoned")
101      .migrate(&self.id, &mut self.state);
102
103    if result.done > 0 {
104      Meta::write(&collection)?;
105    }
106
107    if let Some(err) = result.error {
108      Err(err)
109    } else {
110      Ok(())
111    }
112  }
113
114  /// The id of the store.
115  #[inline]
116  pub fn id(&self) -> StoreId {
117    self.id.clone()
118  }
119
120  /// Path to the store file.
121  pub fn path(&self) -> PathBuf {
122    store_path::<R, C>(&self.app, &self.id)
123  }
124
125  /// Gets a handle to the application instance.
126  pub fn app_handle(&self) -> &AppHandle<R> {
127    &self.app
128  }
129
130  /// Gets a reference to the store state.
131  #[inline]
132  pub fn state(&self) -> &StoreState {
133    &self.state
134  }
135
136  /// Tries to parse the store state as an instance of type `T`.
137  pub fn try_state<T>(&self) -> Result<T>
138  where
139    T: DeserializeOwned,
140  {
141    Ok(serde_json::from_value(json!(self.state))?)
142  }
143
144  /// Tries to parse the store state as an instance of type `T`.
145  ///
146  /// If it cannot be parsed, returns the provided default value.
147  pub fn try_state_or<T>(&self, default: T) -> T
148  where
149    T: DeserializeOwned,
150  {
151    self.try_state().unwrap_or(default)
152  }
153
154  /// Tries to parse the store state as an instance of type `T`.
155  ///
156  /// If it cannot be parsed, returns the default value of `T`.
157  pub fn try_state_or_default<T>(&self) -> T
158  where
159    T: DeserializeOwned + Default,
160  {
161    self.try_state().unwrap_or_default()
162  }
163
164  /// Tries to parse the store state as an instance of type `T`.
165  ///
166  /// If it cannot be parsed, returns the result of the provided closure.
167  pub fn try_state_or_else<T>(&self, f: impl FnOnce() -> T) -> T
168  where
169    T: DeserializeOwned,
170  {
171    self.try_state().unwrap_or_else(|_| f())
172  }
173
174  /// Gets a value from the store.
175  pub fn get(&self, key: impl AsRef<str>) -> Option<&Json> {
176    self.state.get(key)
177  }
178
179  /// Gets a value from the store and tries to parse it as an instance of type `T`.
180  pub fn try_get<T>(&self, key: impl AsRef<str>) -> Result<T>
181  where
182    T: DeserializeOwned,
183  {
184    self.state.try_get(key)
185  }
186
187  /// Gets a value from the store and tries to parse it as an instance of type `T`.
188  ///
189  /// If the key does not exist, returns the provided default value.
190  pub fn try_get_or<T>(&self, key: impl AsRef<str>, default: T) -> T
191  where
192    T: DeserializeOwned,
193  {
194    self.state.try_get_or(key, default)
195  }
196
197  /// Gets a value from the store and tries to parse it as an instance of type `T`.
198  ///
199  /// If the key does not exist, returns the default value of `T`.
200  pub fn try_get_or_default<T>(&self, key: impl AsRef<str>) -> T
201  where
202    T: DeserializeOwned + Default,
203  {
204    self.state.try_get_or_default(key)
205  }
206
207  /// Gets a value from the store and tries to parse it as an instance of type `T`.
208  ///
209  /// If the key does not exist, returns the result of the provided closure.
210  pub fn try_get_or_else<T>(&self, key: impl AsRef<str>, f: impl FnOnce() -> T) -> T
211  where
212    T: DeserializeOwned,
213  {
214    self.state.try_get_or_else(key, f)
215  }
216
217  /// Sets a key-value pair in the store.
218  pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<Json>) -> Result<()> {
219    self.state.set(key, value);
220    self.on_state_change(None::<&str>)
221  }
222
223  /// Patches the store state, optionally having a window as the source.
224  #[doc(hidden)]
225  pub fn patch_with_source<S, E>(&mut self, state: S, source: E) -> Result<()>
226  where
227    S: Into<StoreState>,
228    E: Into<EventSource>,
229  {
230    self.state.patch(state);
231    self.on_state_change(source)
232  }
233
234  /// Patches the store state.
235  pub fn patch<S>(&mut self, state: S) -> Result<()>
236  where
237    S: Into<StoreState>,
238  {
239    self.patch_with_source(state, None::<&str>)
240  }
241
242  /// Whether the store has a key.
243  pub fn has(&self, key: impl AsRef<str>) -> bool {
244    self.state.has(key)
245  }
246
247  /// Creates an iterator over the store keys.
248  pub fn keys(&self) -> impl Iterator<Item = &String> {
249    self.state.keys()
250  }
251
252  /// Creates an iterator over the store values.
253  pub fn values(&self) -> impl Iterator<Item = &Json> {
254    self.state.values()
255  }
256
257  /// Creates an iterator over the store entries.
258  pub fn entries(&self) -> impl Iterator<Item = (&String, &Json)> {
259    self.state.entries()
260  }
261
262  /// Returns the amount of items in the store.
263  #[inline]
264  pub fn len(&self) -> usize {
265    self.state.len()
266  }
267
268  /// Whether the store is empty.
269  #[inline]
270  pub fn is_empty(&self) -> bool {
271    self.state.is_empty()
272  }
273
274  /// Save the store state to the disk.
275  pub fn save(&self) -> Result<()> {
276    match self.save_strategy() {
277      SaveStrategy::Immediate => self.save_now()?,
278      SaveStrategy::Debounce(duration) => {
279        self
280          .debounce_save_handle
281          .get_or_init(|| debounce::<R, C>(self.id.clone(), duration))
282          .call(&self.app);
283      }
284      SaveStrategy::Throttle(duration) => {
285        self
286          .throttle_save_handle
287          .get_or_init(|| throttle::<R, C>(self.id.clone(), duration))
288          .call(&self.app);
289      }
290    }
291
292    Ok(())
293  }
294
295  /// Save the store immediately, ignoring the save strategy.
296  pub fn save_now(&self) -> Result<()> {
297    let collection = self.app.store_collection_with_marker::<C>();
298    if collection
299      .save_denylist
300      .as_ref()
301      .is_some_and(|it| it.contains(&self.id))
302    {
303      return Ok(());
304    }
305
306    write_file(self.path(), &self.state)
307      .sync(cfg!(feature = "file-sync-all"))
308      .pretty(collection.pretty)
309      .call()?;
310
311    Ok(())
312  }
313
314  /// Whether to save the store on exit.
315  /// This is enabled by default.
316  #[inline]
317  pub fn save_on_exit(&mut self, enabled: bool) {
318    self.save_on_exit = enabled;
319  }
320
321  /// Whether to save the store on state change.
322  #[inline]
323  pub fn save_on_change(&mut self, enabled: bool) {
324    self.save_on_change = enabled;
325  }
326
327  /// Current save strategy used by this store.
328  pub fn save_strategy(&self) -> SaveStrategy {
329    self.save_strategy.unwrap_or_else(|| {
330      self
331        .app
332        .store_collection_with_marker::<C>()
333        .default_save_strategy
334    })
335  }
336
337  /// Sets the save strategy for this store.
338  /// Calling this will abort any pending save operation.
339  pub fn set_save_strategy(&mut self, strategy: SaveStrategy) {
340    if strategy.is_debounce() {
341      self
342        .debounce_save_handle
343        .take()
344        .inspect(SaveHandle::abort);
345    } else if strategy.is_throttle() {
346      self
347        .throttle_save_handle
348        .take()
349        .inspect(SaveHandle::abort);
350    }
351
352    self.save_strategy = Some(strategy);
353  }
354
355  /// Watches the store for changes.
356  pub fn watch<F>(&mut self, f: F) -> WatcherId
357  where
358    F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
359  {
360    let (id, listener) = Watcher::new(f);
361    self.watchers.insert(id, listener);
362    id
363  }
364
365  /// Removes a listener from this store.
366  pub fn unwatch(&mut self, id: impl Into<WatcherId>) -> bool {
367    self.watchers.remove(&id.into()).is_some()
368  }
369
370  /// Sets the store options, optionally having a window as the source.
371  #[doc(hidden)]
372  pub fn set_options_with_source<E>(&mut self, options: StoreOptions, source: E) -> Result<()>
373  where
374    E: Into<EventSource>,
375  {
376    set_options(self, options);
377    self.on_config_change(source)
378  }
379
380  /// Sets the store options.
381  pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
382    self.set_options_with_source(options, None::<&str>)
383  }
384
385  fn on_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
386    self.emit_state_change(source)?;
387    self.call_watchers();
388
389    if self.save_on_change {
390      self.save()?;
391    }
392
393    Ok(())
394  }
395
396  fn emit_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
397    let source: EventSource = source.into();
398
399    // If we also skip the store when the source is the backend,
400    // the window where the store resides would never know about the change.
401    if !source.is_backend()
402      && self
403        .app
404        .store_collection_with_marker::<C>()
405        .sync_denylist
406        .as_ref()
407        .is_some_and(|it| it.contains(&self.id))
408    {
409      return Ok(());
410    }
411
412    emit(
413      &self.app,
414      STORE_STATE_CHANGE_EVENT,
415      &StatePayload::from(self),
416      source,
417    )
418  }
419
420  fn on_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
421    self.emit_config_change(source)
422  }
423
424  fn emit_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
425    emit(
426      &self.app,
427      STORE_CONFIG_CHANGE_EVENT,
428      &ConfigPayload::from(self),
429      source,
430    )
431  }
432
433  /// Calls all watchers currently attached to the store.
434  fn call_watchers(&self) {
435    if self.watchers.is_empty() {
436      return;
437    }
438
439    for watcher in self.watchers.values() {
440      let app = self.app.clone();
441      let watcher = watcher.clone();
442      spawn_blocking(move || watcher.call(app));
443    }
444  }
445
446  pub(crate) fn abort_pending_save(&self) {
447    self
448      .debounce_save_handle
449      .get()
450      .map(SaveHandle::abort);
451
452    self
453      .throttle_save_handle
454      .get()
455      .map(SaveHandle::abort);
456  }
457}
458
459impl<R, C> fmt::Debug for Store<R, C>
460where
461  R: Runtime,
462  C: CollectionMarker,
463{
464  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465    f.debug_struct("Store")
466      .field("id", &self.id)
467      .field("state", &self.state)
468      .field("watchers", &self.watchers.len())
469      .field("save_on_exit", &self.save_on_exit)
470      .field("save_on_change", &self.save_on_change)
471      .field("save_strategy", &self.save_strategy)
472      .finish_non_exhaustive()
473  }
474}
475
476fn store_path<R, C>(app: &AppHandle<R>, id: &StoreId) -> PathBuf
477where
478  R: Runtime,
479  C: CollectionMarker,
480{
481  append_filename(&app.store_collection_with_marker::<C>().path(), id)
482}
483
484/// Appends the store filename to the given directory path.
485pub(crate) fn append_filename(path: &Path, id: &StoreId) -> PathBuf {
486  path.join(format!("{id}.{FILE_EXTENSION}"))
487}