tauri_store/
collection.rs

1mod autosave;
2mod builder;
3mod path;
4
5use crate::error::Result;
6use crate::event::{emit, STORE_UNLOAD_EVENT};
7use crate::meta::Meta;
8use crate::store::{SaveStrategy, Store, StoreId, StoreResource, StoreState, WatcherId};
9use autosave::Autosave;
10use dashmap::DashMap;
11use serde::de::DeserializeOwned;
12use serde_json::Value as Json;
13use std::collections::HashSet;
14use std::fmt;
15use std::path::PathBuf;
16use std::sync::{Arc, Mutex, OnceLock};
17use std::time::Duration;
18use tauri::{AppHandle, Resource, ResourceId, Runtime};
19
20pub use builder::StoreCollectionBuilder;
21
22#[cfg(feature = "unstable-migration")]
23use crate::migration::Migrator;
24
25pub(crate) static RESOURCE_ID: OnceLock<ResourceId> = OnceLock::new();
26
27/// Closure to be called when a store is loaded.
28pub type OnLoadFn<R> = dyn Fn(&Store<R>) -> Result<()> + Send + Sync;
29
30/// A collection of stores.
31/// This is the core component for store plugins.
32pub struct StoreCollection<R: Runtime> {
33  pub(crate) app: AppHandle<R>,
34  pub(crate) name: Box<str>,
35  pub(crate) path: Mutex<PathBuf>,
36  pub(crate) stores: DashMap<StoreId, ResourceId>,
37  pub(crate) on_load: Option<Box<OnLoadFn<R>>>,
38  pub(crate) autosave: Mutex<Autosave>,
39  pub(crate) default_save_strategy: SaveStrategy,
40  pub(crate) save_denylist: Option<HashSet<StoreId>>,
41  pub(crate) sync_denylist: Option<HashSet<StoreId>>,
42  pub(crate) pretty: bool,
43
44  #[cfg(feature = "unstable-migration")]
45  pub(crate) migrator: Mutex<Migrator>,
46}
47
48impl<R: Runtime> StoreCollection<R> {
49  /// Builds a new store collection.
50  pub fn builder() -> StoreCollectionBuilder<R> {
51    StoreCollectionBuilder::new()
52  }
53
54  pub(crate) fn get_resource(&self, id: impl AsRef<str>) -> Result<Arc<StoreResource<R>>> {
55    let id = StoreId::from(id.as_ref());
56    let rid = match self.rid(&id) {
57      Some(rid) => rid,
58      None => self.load_store(id)?,
59    };
60
61    StoreResource::get(&self.app, rid)
62  }
63
64  fn load_store(&self, id: StoreId) -> Result<ResourceId> {
65    let (rid, resource) = Store::load(&self.app, &id)?;
66    if let Some(on_load) = &self.on_load {
67      resource.locked(|store| on_load(store))?;
68    }
69
70    self.stores.insert(id, rid);
71    Ok(rid)
72  }
73
74  /// Gets the resource id for a store.
75  fn rid(&self, store_id: &StoreId) -> Option<ResourceId> {
76    self.stores.get(store_id).map(|it| *it.value())
77  }
78
79  /// Gets the resource ids for all the stores.
80  fn rids(&self) -> Vec<ResourceId> {
81    self.stores.iter().map(|it| *it.value()).collect()
82  }
83
84  /// Lists all the store ids.
85  pub fn ids(&self) -> Vec<StoreId> {
86    self
87      .stores
88      .iter()
89      .map(|it| it.key().clone())
90      .collect()
91  }
92
93  /// Calls a closure with a mutable reference to the store with the given id.
94  pub fn with_store<F, T>(&self, store_id: impl AsRef<str>, f: F) -> Result<T>
95  where
96    F: FnOnce(&mut Store<R>) -> T,
97  {
98    Ok(self.get_resource(store_id)?.locked(f))
99  }
100
101  /// Gets a clone of the store state.
102  pub fn state(&self, store_id: impl AsRef<str>) -> Result<StoreState> {
103    self
104      .get_resource(store_id)?
105      .locked(|store| Ok(store.state().clone()))
106  }
107
108  /// Gets the store state, then tries to parse it as an instance of type `T`.
109  pub fn try_state<T>(&self, store_id: impl AsRef<str>) -> Result<T>
110  where
111    T: DeserializeOwned,
112  {
113    self
114      .get_resource(store_id)?
115      .locked(|store| store.try_state())
116  }
117
118  /// Gets the store state, then tries to parse it as an instance of type `T`.
119  ///
120  /// If it cannot be parsed, returns the provided default value.
121  pub fn try_state_or<T>(&self, store_id: impl AsRef<str>, default: T) -> Result<T>
122  where
123    T: DeserializeOwned,
124  {
125    self
126      .get_resource(store_id)?
127      .locked(move |store| Ok(store.try_state_or(default)))
128  }
129
130  /// Gets the store state, then tries to parse it as an instance of type `T`.
131  ///
132  /// If it cannot be parsed, returns the default value of `T`.
133  pub fn try_state_or_default<T>(&self, store_id: impl AsRef<str>) -> Result<T>
134  where
135    T: DeserializeOwned + Default,
136  {
137    self
138      .get_resource(store_id)?
139      .locked(|store| Ok(store.try_state_or_default()))
140  }
141
142  /// Gets the store state, then tries to parse it as an instance of type `T`.
143  ///
144  /// If it cannot be parsed, returns the result of the provided closure.
145  pub fn try_state_or_else<T>(&self, store_id: impl AsRef<str>, f: impl FnOnce() -> T) -> Result<T>
146  where
147    T: DeserializeOwned,
148  {
149    self
150      .get_resource(store_id)?
151      .locked(|store| Ok(store.try_state_or_else(f)))
152  }
153
154  /// Gets a value from a store.
155  pub fn get(&self, store_id: impl AsRef<str>, key: impl AsRef<str>) -> Option<Json> {
156    self
157      .get_resource(store_id)
158      .ok()?
159      .locked(|store| store.get(key).cloned())
160  }
161
162  /// Gets a value from a store and tries to parse it as an instance of type `T`.
163  pub fn try_get<T>(&self, store_id: impl AsRef<str>, key: impl AsRef<str>) -> Result<T>
164  where
165    T: DeserializeOwned,
166  {
167    self
168      .get_resource(store_id)?
169      .locked(|store| store.try_get(key))
170  }
171
172  /// Gets a value from a store and tries to parse it as an instance of type `T`.
173  ///
174  /// If the key does not exist, returns the provided default value.
175  pub fn try_get_or<T>(&self, store_id: impl AsRef<str>, key: impl AsRef<str>, default: T) -> T
176  where
177    T: DeserializeOwned,
178  {
179    self.try_get(store_id, key).unwrap_or(default)
180  }
181
182  /// Gets a value from a store and tries to parse it as an instance of type `T`.
183  ///
184  /// If the key does not exist, returns the default value of `T`.
185  pub fn try_get_or_default<T>(&self, store_id: impl AsRef<str>, key: impl AsRef<str>) -> T
186  where
187    T: Default + DeserializeOwned,
188  {
189    self.try_get(store_id, key).unwrap_or_default()
190  }
191
192  /// Gets a value from a store and tries to parse it as an instance of type `T`.
193  ///
194  /// If the key does not exist, returns the result of the provided closure.
195  pub fn try_get_or_else<T>(
196    &self,
197    store_id: impl AsRef<str>,
198    key: impl AsRef<str>,
199    f: impl FnOnce() -> T,
200  ) -> T
201  where
202    T: DeserializeOwned,
203  {
204    self
205      .try_get(store_id, key)
206      .unwrap_or_else(|_| f())
207  }
208
209  /// Sets a key-value pair in a store.
210  pub fn set<K, V>(&self, store_id: impl AsRef<str>, key: K, value: V) -> Result<()>
211  where
212    K: AsRef<str>,
213    V: Into<Json>,
214  {
215    self
216      .get_resource(store_id)?
217      .locked(|store| store.set(key, value))
218  }
219
220  /// Patches a store state.
221  pub fn patch<S>(&self, store_id: impl AsRef<str>, state: S) -> Result<()>
222  where
223    S: Into<StoreState>,
224  {
225    self
226      .get_resource(store_id)?
227      .locked(|store| store.patch(state))
228  }
229
230  /// Saves a store to the disk.
231  pub fn save(&self, store_id: impl AsRef<str>) -> Result<()> {
232    self
233      .get_resource(store_id)?
234      .locked(|store| store.save())
235  }
236
237  /// Saves a store to the disk immediately, ignoring the save strategy.
238  pub fn save_now(&self, store_id: impl AsRef<str>) -> Result<()> {
239    self.get_resource(store_id)?.locked(|store| {
240      store.abort_pending_save();
241      store.save_now()
242    })
243  }
244
245  /// Saves some stores to the disk.
246  pub fn save_some(&self, ids: &[impl AsRef<str>]) -> Result<()> {
247    ids.iter().try_for_each(|id| self.save(id))
248  }
249
250  /// Saves some stores to the disk immediately, ignoring the save strategy.
251  pub fn save_some_now(&self, ids: &[impl AsRef<str>]) -> Result<()> {
252    ids.iter().try_for_each(|id| self.save_now(id))
253  }
254
255  /// Saves all the stores to the disk.
256  pub fn save_all(&self) -> Result<()> {
257    // I suppose going through the rids is better than through the store ids.
258    // This way, we don't need to hold references into the dashmap nor clone its keys.
259    // The downside (?) is that we need to use the StoreResource directly.
260    self
261      .rids()
262      .into_iter()
263      .try_for_each(|rid| StoreResource::save(&self.app, rid))
264  }
265
266  /// Saves all the stores to the disk immediately, ignoring the save strategy.
267  pub fn save_all_now(&self) -> Result<()> {
268    self
269      .rids()
270      .into_iter()
271      .try_for_each(|rid| StoreResource::save_now(&self.app, rid))
272  }
273
274  /// Default save strategy for the stores.
275  /// This can be overridden on a per-store basis.
276  #[inline]
277  pub fn default_save_strategy(&self) -> SaveStrategy {
278    self.default_save_strategy
279  }
280
281  /// Saves the stores periodically.
282  pub fn set_autosave(&self, duration: Duration) {
283    if let Ok(mut autosave) = self.autosave.lock() {
284      autosave.set_duration(duration);
285      autosave.start(&self.app);
286    }
287  }
288
289  /// Stops the autosave.
290  pub fn clear_autosave(&self) {
291    if let Ok(mut autosave) = self.autosave.lock() {
292      autosave.stop();
293    }
294  }
295
296  /// Watches a store for changes.
297  pub fn watch<F>(&self, store_id: impl AsRef<str>, f: F) -> Result<WatcherId>
298  where
299    F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
300  {
301    self
302      .get_resource(store_id)?
303      .locked(|store| Ok(store.watch(f)))
304  }
305
306  /// Removes a watcher from a store.
307  pub fn unwatch(
308    &self,
309    store_id: impl AsRef<str>,
310    watcher_id: impl Into<WatcherId>,
311  ) -> Result<bool> {
312    self
313      .get_resource(store_id)?
314      .locked(|store| Ok(store.unwatch(watcher_id)))
315  }
316
317  /// Removes the store from the collection.
318  #[doc(hidden)]
319  pub fn unload_store(&self, id: &StoreId) -> Result<()> {
320    if let Some((_, rid)) = self.stores.remove(id) {
321      // The store needs to be saved immediately here.
322      // Otherwise, the plugin might try to load it again if `StoreCollection::get_resource` is called.
323      // This scenario will happen whenever the save strategy is not `Immediate`.
324      let resource = StoreResource::take(&self.app, rid)?;
325      resource.locked(|store| store.save_now())?;
326
327      emit(&self.app, STORE_UNLOAD_EVENT, id, None::<&str>)?;
328    }
329
330    Ok(())
331  }
332
333  /// Runs any necessary tasks before the application exits.
334  #[doc(hidden)]
335  pub fn on_exit(&self) -> Result<()> {
336    self.clear_autosave();
337
338    for rid in self.rids() {
339      if let Ok(resource) = StoreResource::take(&self.app, rid) {
340        resource.locked(|store| {
341          store.abort_pending_save();
342          if store.save_on_exit {
343            let _ = store.save_now();
344          }
345        });
346      }
347    }
348
349    Meta::write(self)?;
350
351    Ok(())
352  }
353}
354
355impl<R: Runtime> Resource for StoreCollection<R> {}
356
357impl<R: Runtime> fmt::Debug for StoreCollection<R> {
358  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359    f.debug_struct("StoreCollection")
360      .field("default_save_strategy", &self.default_save_strategy)
361      .field("pretty", &self.pretty)
362      .finish_non_exhaustive()
363  }
364}