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