Skip to main content

tauri_store/store/
mod.rs

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