tauri_store/
store.rs

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