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
45pub 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 #[inline]
110 pub fn id(&self) -> StoreId {
111 self.id.clone()
112 }
113
114 pub fn path(&self) -> PathBuf {
116 store_path(&self.app, &self.id)
117 }
118
119 pub fn app_handle(&self) -> &AppHandle<R> {
121 &self.app
122 }
123
124 #[inline]
126 pub fn state(&self) -> &StoreState {
127 &self.state
128 }
129
130 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 pub fn try_state_or<T>(&self, default: T) -> T
142 where
143 T: DeserializeOwned,
144 {
145 self.try_state().unwrap_or(default)
146 }
147
148 pub fn try_state_or_default<T>(&self) -> T
152 where
153 T: DeserializeOwned + Default,
154 {
155 self.try_state().unwrap_or_default()
156 }
157
158 pub fn try_state_or_else<T>(&self, f: impl FnOnce() -> T) -> T
162 where
163 T: DeserializeOwned,
164 {
165 self.try_state().unwrap_or_else(|_| f())
166 }
167
168 pub fn get(&self, key: impl AsRef<str>) -> Option<&Json> {
170 self.state.get(key)
171 }
172
173 pub fn try_get<T>(&self, key: impl AsRef<str>) -> Result<T>
175 where
176 T: DeserializeOwned,
177 {
178 self.state.try_get(key)
179 }
180
181 pub fn try_get_or<T>(&self, key: impl AsRef<str>, default: T) -> T
185 where
186 T: DeserializeOwned,
187 {
188 self.state.try_get_or(key, default)
189 }
190
191 pub fn try_get_or_default<T>(&self, key: impl AsRef<str>) -> T
195 where
196 T: DeserializeOwned + Default,
197 {
198 self.state.try_get_or_default(key)
199 }
200
201 pub fn try_get_or_else<T>(&self, key: impl AsRef<str>, f: impl FnOnce() -> T) -> T
205 where
206 T: DeserializeOwned,
207 {
208 self.state.try_get_or_else(key, f)
209 }
210
211 pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<Json>) -> Result<()> {
213 self.state.set(key, value);
214 self.on_state_change(None::<&str>)
215 }
216
217 #[doc(hidden)]
219 pub fn patch_with_source<S, E>(&mut self, state: S, source: E) -> Result<()>
220 where
221 S: Into<StoreState>,
222 E: Into<EventSource>,
223 {
224 self.state.patch(state);
225 self.on_state_change(source)
226 }
227
228 pub fn patch<S>(&mut self, state: S) -> Result<()>
230 where
231 S: Into<StoreState>,
232 {
233 self.patch_with_source(state, None::<&str>)
234 }
235
236 pub fn has(&self, key: impl AsRef<str>) -> bool {
238 self.state.has(key)
239 }
240
241 pub fn keys(&self) -> impl Iterator<Item = &String> {
243 self.state.keys()
244 }
245
246 pub fn values(&self) -> impl Iterator<Item = &Json> {
248 self.state.values()
249 }
250
251 pub fn entries(&self) -> impl Iterator<Item = (&String, &Json)> {
253 self.state.entries()
254 }
255
256 #[inline]
258 pub fn len(&self) -> usize {
259 self.state.len()
260 }
261
262 #[inline]
264 pub fn is_empty(&self) -> bool {
265 self.state.is_empty()
266 }
267
268 pub fn save(&self) -> Result<()> {
270 match self.save_strategy() {
271 SaveStrategy::Immediate => self.save_now()?,
272 SaveStrategy::Debounce(duration) => {
273 self
274 .debounce_save_handle
275 .get_or_init(|| debounce(self.id.clone(), duration))
276 .call(&self.app);
277 }
278 SaveStrategy::Throttle(duration) => {
279 self
280 .throttle_save_handle
281 .get_or_init(|| throttle(self.id.clone(), duration))
282 .call(&self.app);
283 }
284 }
285
286 Ok(())
287 }
288
289 pub fn save_now(&self) -> Result<()> {
291 let collection = self.app.store_collection();
292 if collection
293 .save_denylist
294 .as_ref()
295 .is_some_and(|it| it.contains(&self.id))
296 {
297 return Ok(());
298 }
299
300 write_file(self.path(), &self.state)
301 .sync(cfg!(feature = "file-sync-all"))
302 .pretty(collection.pretty)
303 .call()?;
304
305 #[cfg(tauri_store_tracing)]
306 debug!("store saved: {}", self.id);
307
308 Ok(())
309 }
310
311 #[inline]
314 pub fn save_on_exit(&mut self, enabled: bool) {
315 self.save_on_exit = enabled;
316 }
317
318 #[inline]
320 pub fn save_on_change(&mut self, enabled: bool) {
321 self.save_on_change = enabled;
322 }
323
324 pub fn save_strategy(&self) -> SaveStrategy {
326 self
327 .save_strategy
328 .unwrap_or_else(|| self.app.store_collection().default_save_strategy)
329 }
330
331 pub fn set_save_strategy(&mut self, strategy: SaveStrategy) {
334 if strategy.is_debounce() {
335 self
336 .debounce_save_handle
337 .take()
338 .inspect(SaveHandle::abort);
339 } else if strategy.is_throttle() {
340 self
341 .throttle_save_handle
342 .take()
343 .inspect(SaveHandle::abort);
344 }
345
346 self.save_strategy = Some(strategy);
347 }
348
349 pub fn watch<F>(&mut self, f: F) -> WatcherId
351 where
352 F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
353 {
354 let (id, listener) = Watcher::new(f);
355 self.watchers.insert(id, listener);
356 id
357 }
358
359 pub fn unwatch(&mut self, id: impl Into<WatcherId>) -> bool {
361 self.watchers.remove(&id.into()).is_some()
362 }
363
364 #[doc(hidden)]
366 pub fn set_options_with_source<E>(&mut self, options: StoreOptions, source: E) -> Result<()>
367 where
368 E: Into<EventSource>,
369 {
370 set_options(self, options);
371 self.on_config_change(source)
372 }
373
374 pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
376 self.set_options_with_source(options, None::<&str>)
377 }
378
379 fn on_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
380 self.emit_state_change(source)?;
381 self.call_watchers();
382
383 if self.save_on_change {
384 self.save()?;
385 }
386
387 Ok(())
388 }
389
390 fn emit_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
391 let source: EventSource = source.into();
392
393 if !source.is_backend()
396 && self
397 .app
398 .store_collection()
399 .sync_denylist
400 .as_ref()
401 .is_some_and(|it| it.contains(&self.id))
402 {
403 return Ok(());
404 }
405
406 emit(
407 &self.app,
408 STORE_STATE_CHANGE_EVENT,
409 &StatePayload::from(self),
410 source,
411 )
412 }
413
414 fn on_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
415 self.emit_config_change(source)
416 }
417
418 fn emit_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
419 emit(
420 &self.app,
421 STORE_CONFIG_CHANGE_EVENT,
422 &ConfigPayload::from(self),
423 source,
424 )
425 }
426
427 fn call_watchers(&self) {
429 if self.watchers.is_empty() {
430 return;
431 }
432
433 for watcher in self.watchers.values() {
434 let app = self.app.clone();
435 let watcher = watcher.clone();
436 spawn_blocking(move || watcher.call(app));
437 }
438 }
439
440 pub(crate) fn abort_pending_save(&self) {
441 self
442 .debounce_save_handle
443 .get()
444 .map(SaveHandle::abort);
445
446 self
447 .throttle_save_handle
448 .get()
449 .map(SaveHandle::abort);
450 }
451}
452
453impl<R: Runtime> fmt::Debug for Store<R> {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455 f.debug_struct("Store")
456 .field("id", &self.id)
457 .field("state", &self.state)
458 .field("watchers", &self.watchers.len())
459 .field("save_on_exit", &self.save_on_exit)
460 .field("save_on_change", &self.save_on_change)
461 .field("save_strategy", &self.save_strategy)
462 .finish_non_exhaustive()
463 }
464}
465
466fn store_path<R>(app: &AppHandle<R>, id: &StoreId) -> PathBuf
467where
468 R: Runtime,
469{
470 append_filename(&app.store_collection().path(), id)
471}
472
473pub(crate) fn append_filename(path: &Path, id: &StoreId) -> PathBuf {
475 path.join(format!("{id}.{FILE_EXTENSION}"))
476}