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