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 get(&self, key: impl AsRef<str>) -> Option<&Json> {
140 self.state.get(key)
141 }
142
143 pub fn try_get<T>(&self, key: impl AsRef<str>) -> Result<T>
145 where
146 T: DeserializeOwned,
147 {
148 self.state.try_get(key)
149 }
150
151 pub fn try_get_or<T>(&self, key: impl AsRef<str>, default: T) -> T
155 where
156 T: DeserializeOwned,
157 {
158 self.state.try_get_or(key, default)
159 }
160
161 pub fn try_get_or_default<T>(&self, key: impl AsRef<str>) -> T
165 where
166 T: DeserializeOwned + Default,
167 {
168 self.state.try_get_or_default(key)
169 }
170
171 pub fn try_get_or_else<T>(&self, key: impl AsRef<str>, f: impl FnOnce() -> T) -> T
175 where
176 T: DeserializeOwned,
177 {
178 self.state.try_get_or_else(key, f)
179 }
180
181 pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<Json>) -> Result<()> {
183 self.state.set(key, value);
184 self.on_state_change(None::<&str>)
185 }
186
187 #[doc(hidden)]
189 pub fn patch_with_source<S, E>(&mut self, state: S, source: E) -> Result<()>
190 where
191 S: Into<StoreState>,
192 E: Into<EventSource>,
193 {
194 self.state.patch(state);
195 self.on_state_change(source)
196 }
197
198 pub fn patch<S>(&mut self, state: S) -> Result<()>
200 where
201 S: Into<StoreState>,
202 {
203 self.patch_with_source(state, None::<&str>)
204 }
205
206 pub fn has(&self, key: impl AsRef<str>) -> bool {
208 self.state.has(key)
209 }
210
211 pub fn keys(&self) -> impl Iterator<Item = &String> {
213 self.state.keys()
214 }
215
216 pub fn values(&self) -> impl Iterator<Item = &Json> {
218 self.state.values()
219 }
220
221 pub fn entries(&self) -> impl Iterator<Item = (&String, &Json)> {
223 self.state.entries()
224 }
225
226 #[inline]
228 pub fn len(&self) -> usize {
229 self.state.len()
230 }
231
232 #[inline]
234 pub fn is_empty(&self) -> bool {
235 self.state.is_empty()
236 }
237
238 pub fn save(&self) -> Result<()> {
240 match self.save_strategy() {
241 SaveStrategy::Immediate => self.save_now()?,
242 SaveStrategy::Debounce(duration) => {
243 self
244 .debounce_save_handle
245 .get_or_init(|| debounce(self.id.clone(), duration))
246 .call(&self.app);
247 }
248 SaveStrategy::Throttle(duration) => {
249 self
250 .throttle_save_handle
251 .get_or_init(|| throttle(self.id.clone(), duration))
252 .call(&self.app);
253 }
254 }
255
256 Ok(())
257 }
258
259 pub fn save_now(&self) -> Result<()> {
261 let collection = self.app.store_collection();
262 if collection
263 .save_denylist
264 .as_ref()
265 .is_some_and(|it| it.contains(&self.id))
266 {
267 return Ok(());
268 }
269
270 write_file(self.path(), &self.state)
271 .sync(cfg!(feature = "file-sync-all"))
272 .pretty(collection.pretty)
273 .call()?;
274
275 #[cfg(tauri_store_tracing)]
276 debug!("store saved: {}", self.id);
277
278 Ok(())
279 }
280
281 #[inline]
284 pub fn save_on_exit(&mut self, enabled: bool) {
285 self.save_on_exit = enabled;
286 }
287
288 #[inline]
290 pub fn save_on_change(&mut self, enabled: bool) {
291 self.save_on_change = enabled;
292 }
293
294 pub fn save_strategy(&self) -> SaveStrategy {
296 self
297 .save_strategy
298 .unwrap_or_else(|| self.app.store_collection().default_save_strategy)
299 }
300
301 pub fn set_save_strategy(&mut self, strategy: SaveStrategy) {
304 if strategy.is_debounce() {
305 self
306 .debounce_save_handle
307 .take()
308 .inspect(SaveHandle::abort);
309 } else if strategy.is_throttle() {
310 self
311 .throttle_save_handle
312 .take()
313 .inspect(SaveHandle::abort);
314 }
315
316 self.save_strategy = Some(strategy);
317 }
318
319 pub fn watch<F>(&mut self, f: F) -> WatcherId
321 where
322 F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
323 {
324 let (id, listener) = Watcher::new(f);
325 self.watchers.insert(id, listener);
326 id
327 }
328
329 pub fn unwatch(&mut self, id: impl Into<WatcherId>) -> bool {
331 self.watchers.remove(&id.into()).is_some()
332 }
333
334 #[doc(hidden)]
336 pub fn set_options_with_source<E>(&mut self, options: StoreOptions, source: E) -> Result<()>
337 where
338 E: Into<EventSource>,
339 {
340 set_options(self, options);
341 self.on_config_change(source)
342 }
343
344 pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
346 self.set_options_with_source(options, None::<&str>)
347 }
348
349 fn on_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
350 self.emit_state_change(source)?;
351 self.call_watchers();
352
353 if self.save_on_change {
354 self.save()?;
355 }
356
357 Ok(())
358 }
359
360 fn emit_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
361 let source: EventSource = source.into();
362
363 if !source.is_backend()
366 && self
367 .app
368 .store_collection()
369 .sync_denylist
370 .as_ref()
371 .is_some_and(|it| it.contains(&self.id))
372 {
373 return Ok(());
374 }
375
376 emit(
377 &self.app,
378 STORE_STATE_CHANGE_EVENT,
379 &StatePayload::from(self),
380 source,
381 )
382 }
383
384 fn on_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
385 self.emit_config_change(source)
386 }
387
388 fn emit_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
389 emit(
390 &self.app,
391 STORE_CONFIG_CHANGE_EVENT,
392 &ConfigPayload::from(self),
393 source,
394 )
395 }
396
397 fn call_watchers(&self) {
399 if self.watchers.is_empty() {
400 return;
401 }
402
403 for watcher in self.watchers.values() {
404 let app = self.app.clone();
405 let watcher = watcher.clone();
406 spawn_blocking(move || watcher.call(app));
407 }
408 }
409
410 pub(crate) fn abort_pending_save(&self) {
411 self
412 .debounce_save_handle
413 .get()
414 .map(SaveHandle::abort);
415
416 self
417 .throttle_save_handle
418 .get()
419 .map(SaveHandle::abort);
420 }
421}
422
423impl<R: Runtime> fmt::Debug for Store<R> {
424 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 f.debug_struct("Store")
426 .field("id", &self.id)
427 .field("state", &self.state)
428 .field("watchers", &self.watchers.len())
429 .field("save_on_exit", &self.save_on_exit)
430 .field("save_on_change", &self.save_on_change)
431 .field("save_strategy", &self.save_strategy)
432 .finish_non_exhaustive()
433 }
434}
435
436fn store_path<R>(app: &AppHandle<R>, id: &StoreId) -> PathBuf
437where
438 R: Runtime,
439{
440 append_filename(&app.store_collection().path(), id)
441}
442
443pub(crate) fn append_filename(path: &Path, id: &StoreId) -> PathBuf {
445 path.join(format!("{id}.{FILE_EXTENSION}"))
446}