1mod options;
2mod resource;
3mod save;
4mod state;
5mod watch;
6
7use crate::error::Result;
8use crate::io_err;
9use crate::manager::ManagerExt;
10use options::set_options;
11use save::{debounce, throttle, SaveHandle};
12use serde::de::DeserializeOwned;
13use serde::{Deserialize, Serialize};
14use serde_json::{json, Value as Json};
15use std::collections::HashMap;
16use std::fmt;
17use std::path::{Path, PathBuf};
18use std::sync::{Arc, OnceLock};
19use tauri::async_runtime::spawn_blocking;
20use tauri::{AppHandle, ResourceId, Runtime};
21use tauri_store_utils::{read_file, write_file};
22use watch::Watcher;
23
24use crate::event::{
25 emit, ConfigPayload, EventSource, StatePayload, STORE_CONFIG_CHANGE_EVENT,
26 STORE_STATE_CHANGE_EVENT,
27};
28
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 pub(crate) state: StoreState,
50 pub(crate) watchers: HashMap<WatcherId, Watcher<R>>,
51 pub(crate) save_on_exit: bool,
52 save_on_change: bool,
53 save_strategy: Option<SaveStrategy>,
54 debounce_save_handle: OnceLock<SaveHandle<R>>,
55 throttle_save_handle: OnceLock<SaveHandle<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 let store = Self {
68 app: app.clone(),
69 id,
70 state,
71 watchers: HashMap::new(),
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 };
78
79 Ok(StoreResource::create(app, store))
80 }
81
82 pub fn id(&self) -> StoreId {
84 self.id.clone()
85 }
86
87 pub fn path(&self) -> PathBuf {
89 store_path(&self.app, &self.id)
90 }
91
92 pub fn app_handle(&self) -> &AppHandle<R> {
94 &self.app
95 }
96
97 pub fn state(&self) -> &StoreState {
99 &self.state
100 }
101
102 pub fn try_state<T: DeserializeOwned>(&self) -> Result<T> {
104 Ok(serde_json::from_value(json!(self.state))?)
105 }
106
107 pub fn get(&self, key: impl AsRef<str>) -> Option<&Json> {
109 self.state.0.get(key.as_ref())
110 }
111
112 pub fn try_get<T: DeserializeOwned>(&self, key: impl AsRef<str>) -> Result<T> {
114 let key = key.as_ref();
115 let Some(value) = self.state.0.get(key).cloned() else {
116 return io_err!(NotFound, "key not found: {key}");
117 };
118
119 Ok(serde_json::from_value(value)?)
120 }
121
122 pub fn try_get_or<T>(&self, key: impl AsRef<str>, default: T) -> T
126 where
127 T: DeserializeOwned,
128 {
129 self.try_get(key).unwrap_or(default)
130 }
131
132 pub fn try_get_or_default<T>(&self, key: impl AsRef<str>) -> T
136 where
137 T: DeserializeOwned + Default,
138 {
139 self.try_get(key).unwrap_or_default()
140 }
141
142 pub fn try_get_or_else<T>(&self, key: impl AsRef<str>, f: impl FnOnce() -> T) -> T
146 where
147 T: DeserializeOwned,
148 {
149 self.try_get(key).unwrap_or_else(|_| f())
150 }
151
152 pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<Json>) -> Result<()> {
154 self
155 .state
156 .0
157 .insert(key.as_ref().to_owned(), value.into());
158
159 self.on_state_change(None::<&str>)
160 }
161
162 #[doc(hidden)]
164 pub fn patch_with_source<S, E>(&mut self, state: S, source: E) -> Result<()>
165 where
166 S: Into<StoreState>,
167 E: Into<EventSource>,
168 {
169 self.state.0.extend(state.into().0);
170 self.on_state_change(source)
171 }
172
173 pub fn patch<S: Into<StoreState>>(&mut self, state: S) -> Result<()> {
175 self.patch_with_source(state, None::<&str>)
176 }
177
178 pub fn has(&self, key: impl AsRef<str>) -> bool {
180 self.state.0.contains_key(key.as_ref())
181 }
182
183 pub fn keys(&self) -> impl Iterator<Item = &String> {
185 self.state.0.keys()
186 }
187
188 pub fn values(&self) -> impl Iterator<Item = &Json> {
190 self.state.0.values()
191 }
192
193 pub fn entries(&self) -> impl Iterator<Item = (&String, &Json)> {
195 self.state.0.iter()
196 }
197
198 pub fn len(&self) -> usize {
200 self.state.0.len()
201 }
202
203 pub fn is_empty(&self) -> bool {
205 self.state.0.is_empty()
206 }
207
208 pub fn save(&self) -> Result<()> {
210 match self.save_strategy() {
211 SaveStrategy::Immediate => self.save_now()?,
212 SaveStrategy::Debounce(duration) => {
213 self
214 .debounce_save_handle
215 .get_or_init(|| debounce(duration, self.id.clone()))
216 .call(&self.app);
217 }
218 SaveStrategy::Throttle(duration) => {
219 self
220 .throttle_save_handle
221 .get_or_init(|| throttle(duration, self.id.clone()))
222 .call(&self.app);
223 }
224 }
225
226 Ok(())
227 }
228
229 pub fn save_now(&self) -> Result<()> {
231 let collection = self.app.store_collection();
232 if collection
233 .save_denylist
234 .as_ref()
235 .is_some_and(|it| it.contains(&self.id))
236 {
237 return Ok(());
238 }
239
240 write_file(self.path(), &self.state)
241 .sync(cfg!(feature = "file-sync-all"))
242 .pretty(collection.pretty)
243 .call()?;
244
245 #[cfg(tauri_store_tracing)]
246 debug!("store saved: {}", self.id);
247
248 Ok(())
249 }
250
251 pub fn save_on_exit(&mut self, enabled: bool) {
254 self.save_on_exit = enabled;
255 }
256
257 pub fn save_on_change(&mut self, enabled: bool) {
259 self.save_on_change = enabled;
260 }
261
262 pub fn save_strategy(&self) -> SaveStrategy {
264 self
265 .save_strategy
266 .unwrap_or_else(|| self.app.store_collection().default_save_strategy)
267 }
268
269 pub fn set_save_strategy(&mut self, strategy: SaveStrategy) {
272 if strategy.is_debounce() {
273 self
274 .debounce_save_handle
275 .take()
276 .inspect(SaveHandle::abort);
277 } else if strategy.is_throttle() {
278 self
279 .throttle_save_handle
280 .take()
281 .inspect(SaveHandle::abort);
282 }
283
284 self.save_strategy = Some(strategy);
285 }
286
287 pub fn watch<F>(&mut self, f: F) -> WatcherId
289 where
290 F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
291 {
292 let listener = Watcher::new(f);
293 let id = listener.id;
294 self.watchers.insert(id, listener);
295 id
296 }
297
298 pub fn unwatch(&mut self, id: impl Into<WatcherId>) -> bool {
300 self.watchers.remove(&id.into()).is_some()
301 }
302
303 #[doc(hidden)]
305 pub fn set_options_with_source<E>(&mut self, options: StoreOptions, source: E) -> Result<()>
306 where
307 E: Into<EventSource>,
308 {
309 set_options(self, options);
310 self.on_config_change(source)
311 }
312
313 pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
315 self.set_options_with_source(options, None::<&str>)
316 }
317
318 fn on_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
319 self.emit_state_change(source)?;
320 self.call_watchers();
321
322 if self.save_on_change {
323 self.save()?;
324 }
325
326 Ok(())
327 }
328
329 fn emit_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
330 let source: EventSource = source.into();
331
332 if !source.is_backend()
335 && self
336 .app
337 .store_collection()
338 .sync_denylist
339 .as_ref()
340 .is_some_and(|it| it.contains(&self.id))
341 {
342 return Ok(());
343 }
344
345 emit(
346 &self.app,
347 STORE_STATE_CHANGE_EVENT,
348 &StatePayload::from(self),
349 source,
350 )
351 }
352
353 fn on_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
354 self.emit_config_change(source)
355 }
356
357 fn emit_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
358 emit(
359 &self.app,
360 STORE_CONFIG_CHANGE_EVENT,
361 &ConfigPayload::from(self),
362 source,
363 )
364 }
365
366 fn call_watchers(&self) {
368 if self.watchers.is_empty() {
369 return;
370 }
371
372 for watcher in self.watchers.values() {
373 let app = self.app.clone();
374 let watcher = watcher.clone();
375 spawn_blocking(move || watcher.call(app));
376 }
377 }
378
379 pub(crate) fn abort_pending_save(&self) {
380 self
381 .debounce_save_handle
382 .get()
383 .map(SaveHandle::abort);
384
385 self
386 .throttle_save_handle
387 .get()
388 .map(SaveHandle::abort);
389 }
390}
391
392impl<R: Runtime> fmt::Debug for Store<R> {
393 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394 f.debug_struct("Store")
395 .field("id", &self.id)
396 .field("state", &self.state)
397 .field("watchers", &self.watchers.len())
398 .field("save_on_exit", &self.save_on_exit)
399 .field("save_on_change", &self.save_on_change)
400 .field("save_strategy", &self.save_strategy)
401 .finish_non_exhaustive()
402 }
403}
404
405#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
407pub struct StoreId(Arc<str>);
408
409impl StoreId {
410 pub fn new(id: &str) -> Self {
411 Self::from(id)
412 }
413}
414
415impl AsRef<str> for StoreId {
416 fn as_ref(&self) -> &str {
417 &self.0
418 }
419}
420
421impl Clone for StoreId {
422 fn clone(&self) -> Self {
423 Self(Arc::clone(&self.0))
424 }
425}
426
427impl From<&str> for StoreId {
428 fn from(id: &str) -> Self {
429 Self(Arc::from(id))
430 }
431}
432
433impl From<String> for StoreId {
434 fn from(id: String) -> Self {
435 Self(Arc::from(id))
436 }
437}
438
439impl From<&String> for StoreId {
440 fn from(id: &String) -> Self {
441 Self(Arc::from(id.as_str()))
442 }
443}
444
445impl fmt::Display for StoreId {
446 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447 write!(f, "{}", self.0)
448 }
449}
450
451fn store_path<R: Runtime>(app: &AppHandle<R>, id: &StoreId) -> PathBuf {
452 append_filename(&app.store_collection().path(), id)
453}
454
455pub(crate) fn append_filename(path: &Path, id: &StoreId) -> PathBuf {
457 path.join(format!("{id}.{FILE_EXTENSION}"))
458}