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  /// Gets a value from the store.
139  pub fn get(&self, key: impl AsRef<str>) -> Option<&Json> {
140    self.state.get(key)
141  }
142
143  /// Gets a value from the store and tries to parse it as an instance of type `T`.
144  pub fn try_get<T>(&self, key: impl AsRef<str>) -> Result<T>
145  where
146    T: DeserializeOwned,
147  {
148    self.state.try_get(key)
149  }
150
151  /// Gets a value from the store and tries to parse it as an instance of type `T`.
152  ///
153  /// If the key does not exist, returns the provided default value.
154  pub fn try_get_or<T>(&self, key: impl AsRef<str>, default: T) -> T
155  where
156    T: DeserializeOwned,
157  {
158    self.state.try_get_or(key, default)
159  }
160
161  /// Gets a value from the store and tries to parse it as an instance of type `T`.
162  ///
163  /// If the key does not exist, returns the default value of `T`.
164  pub fn try_get_or_default<T>(&self, key: impl AsRef<str>) -> T
165  where
166    T: DeserializeOwned + Default,
167  {
168    self.state.try_get_or_default(key)
169  }
170
171  /// Gets a value from the store and tries to parse it as an instance of type `T`.
172  ///
173  /// If the key does not exist, returns the result of the provided closure.
174  pub fn try_get_or_else<T>(&self, key: impl AsRef<str>, f: impl FnOnce() -> T) -> T
175  where
176    T: DeserializeOwned,
177  {
178    self.state.try_get_or_else(key, f)
179  }
180
181  /// Sets a key-value pair in the store.
182  pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<Json>) -> Result<()> {
183    self.state.set(key, value);
184    self.on_state_change(None::<&str>)
185  }
186
187  /// Patches the store state, optionally having a window as the source.
188  #[doc(hidden)]
189  pub fn patch_with_source<S, E>(&mut self, state: S, source: E) -> Result<()>
190  where
191    S: Into<StoreState>,
192    E: Into<EventSource>,
193  {
194    self.state.patch(state);
195    self.on_state_change(source)
196  }
197
198  /// Patches the store state.
199  pub fn patch<S>(&mut self, state: S) -> Result<()>
200  where
201    S: Into<StoreState>,
202  {
203    self.patch_with_source(state, None::<&str>)
204  }
205
206  /// Whether the store has a key.
207  pub fn has(&self, key: impl AsRef<str>) -> bool {
208    self.state.has(key)
209  }
210
211  /// Creates an iterator over the store keys.
212  pub fn keys(&self) -> impl Iterator<Item = &String> {
213    self.state.keys()
214  }
215
216  /// Creates an iterator over the store values.
217  pub fn values(&self) -> impl Iterator<Item = &Json> {
218    self.state.values()
219  }
220
221  /// Creates an iterator over the store entries.
222  pub fn entries(&self) -> impl Iterator<Item = (&String, &Json)> {
223    self.state.entries()
224  }
225
226  /// Returns the amount of items in the store.
227  #[inline]
228  pub fn len(&self) -> usize {
229    self.state.len()
230  }
231
232  /// Whether the store is empty.
233  #[inline]
234  pub fn is_empty(&self) -> bool {
235    self.state.is_empty()
236  }
237
238  /// Save the store state to the disk.
239  pub fn save(&self) -> Result<()> {
240    match self.save_strategy() {
241      SaveStrategy::Immediate => self.save_now()?,
242      SaveStrategy::Debounce(duration) => {
243        self
244          .debounce_save_handle
245          .get_or_init(|| debounce(self.id.clone(), duration))
246          .call(&self.app);
247      }
248      SaveStrategy::Throttle(duration) => {
249        self
250          .throttle_save_handle
251          .get_or_init(|| throttle(self.id.clone(), duration))
252          .call(&self.app);
253      }
254    }
255
256    Ok(())
257  }
258
259  /// Save the store immediately, ignoring the save strategy.
260  pub fn save_now(&self) -> Result<()> {
261    let collection = self.app.store_collection();
262    if collection
263      .save_denylist
264      .as_ref()
265      .is_some_and(|it| it.contains(&self.id))
266    {
267      return Ok(());
268    }
269
270    write_file(self.path(), &self.state)
271      .sync(cfg!(feature = "file-sync-all"))
272      .pretty(collection.pretty)
273      .call()?;
274
275    #[cfg(tauri_store_tracing)]
276    debug!("store saved: {}", self.id);
277
278    Ok(())
279  }
280
281  /// Whether to save the store on exit.
282  /// This is enabled by default.
283  #[inline]
284  pub fn save_on_exit(&mut self, enabled: bool) {
285    self.save_on_exit = enabled;
286  }
287
288  /// Whether to save the store on state change.
289  #[inline]
290  pub fn save_on_change(&mut self, enabled: bool) {
291    self.save_on_change = enabled;
292  }
293
294  /// Current save strategy used by this store.
295  pub fn save_strategy(&self) -> SaveStrategy {
296    self
297      .save_strategy
298      .unwrap_or_else(|| self.app.store_collection().default_save_strategy)
299  }
300
301  /// Sets the save strategy for this store.
302  /// Calling this will abort any pending save operation.
303  pub fn set_save_strategy(&mut self, strategy: SaveStrategy) {
304    if strategy.is_debounce() {
305      self
306        .debounce_save_handle
307        .take()
308        .inspect(SaveHandle::abort);
309    } else if strategy.is_throttle() {
310      self
311        .throttle_save_handle
312        .take()
313        .inspect(SaveHandle::abort);
314    }
315
316    self.save_strategy = Some(strategy);
317  }
318
319  /// Watches the store for changes.
320  pub fn watch<F>(&mut self, f: F) -> WatcherId
321  where
322    F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
323  {
324    let (id, listener) = Watcher::new(f);
325    self.watchers.insert(id, listener);
326    id
327  }
328
329  /// Removes a listener from this store.
330  pub fn unwatch(&mut self, id: impl Into<WatcherId>) -> bool {
331    self.watchers.remove(&id.into()).is_some()
332  }
333
334  /// Sets the store options, optionally having a window as the source.
335  #[doc(hidden)]
336  pub fn set_options_with_source<E>(&mut self, options: StoreOptions, source: E) -> Result<()>
337  where
338    E: Into<EventSource>,
339  {
340    set_options(self, options);
341    self.on_config_change(source)
342  }
343
344  /// Sets the store options.
345  pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
346    self.set_options_with_source(options, None::<&str>)
347  }
348
349  fn on_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
350    self.emit_state_change(source)?;
351    self.call_watchers();
352
353    if self.save_on_change {
354      self.save()?;
355    }
356
357    Ok(())
358  }
359
360  fn emit_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
361    let source: EventSource = source.into();
362
363    // If we also skip the store when the source is the backend,
364    // the window where the store resides would never know about the change.
365    if !source.is_backend()
366      && self
367        .app
368        .store_collection()
369        .sync_denylist
370        .as_ref()
371        .is_some_and(|it| it.contains(&self.id))
372    {
373      return Ok(());
374    }
375
376    emit(
377      &self.app,
378      STORE_STATE_CHANGE_EVENT,
379      &StatePayload::from(self),
380      source,
381    )
382  }
383
384  fn on_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
385    self.emit_config_change(source)
386  }
387
388  fn emit_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
389    emit(
390      &self.app,
391      STORE_CONFIG_CHANGE_EVENT,
392      &ConfigPayload::from(self),
393      source,
394    )
395  }
396
397  /// Calls all watchers currently attached to the store.
398  fn call_watchers(&self) {
399    if self.watchers.is_empty() {
400      return;
401    }
402
403    for watcher in self.watchers.values() {
404      let app = self.app.clone();
405      let watcher = watcher.clone();
406      spawn_blocking(move || watcher.call(app));
407    }
408  }
409
410  pub(crate) fn abort_pending_save(&self) {
411    self
412      .debounce_save_handle
413      .get()
414      .map(SaveHandle::abort);
415
416    self
417      .throttle_save_handle
418      .get()
419      .map(SaveHandle::abort);
420  }
421}
422
423impl<R: Runtime> fmt::Debug for Store<R> {
424  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425    f.debug_struct("Store")
426      .field("id", &self.id)
427      .field("state", &self.state)
428      .field("watchers", &self.watchers.len())
429      .field("save_on_exit", &self.save_on_exit)
430      .field("save_on_change", &self.save_on_change)
431      .field("save_strategy", &self.save_strategy)
432      .finish_non_exhaustive()
433  }
434}
435
436fn store_path<R>(app: &AppHandle<R>, id: &StoreId) -> PathBuf
437where
438  R: Runtime,
439{
440  append_filename(&app.store_collection().path(), id)
441}
442
443/// Appends the store filename to the given directory path.
444pub(crate) fn append_filename(path: &Path, id: &StoreId) -> PathBuf {
445  path.join(format!("{id}.{FILE_EXTENSION}"))
446}