tauri_store/
store.rs

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