tauri_store/store/
save.rs

1use super::StoreId;
2use crate::manager::ManagerExt;
3use crate::CollectionMarker;
4use futures::future::BoxFuture;
5use serde::ser::SerializeTuple;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use serde_json::Value;
8use std::fmt;
9use std::result::Result as StdResult;
10use std::time::Duration;
11use tauri::async_runtime::spawn_blocking;
12use tauri::{AppHandle, Runtime};
13use tauri_store_utils::{Debounce, RemoteCallable, Throttle};
14
15type RemoteSaveHandle<R> = Box<dyn RemoteCallable<AppHandle<R>> + Send + Sync>;
16type SaveHandleFn<R> = Box<dyn Fn(AppHandle<R>) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
17
18pub(super) struct SaveHandle<R: Runtime>(RemoteSaveHandle<R>);
19
20impl<R: Runtime> SaveHandle<R> {
21  pub fn call(&self, app: &AppHandle<R>) {
22    self.0.call(app);
23  }
24
25  pub fn abort(&self) {
26    self.0.abort();
27  }
28}
29
30impl<R: Runtime> Drop for SaveHandle<R> {
31  fn drop(&mut self) {
32    self.abort();
33  }
34}
35
36pub(super) fn debounce<R, C>(id: StoreId, duration: Duration) -> SaveHandle<R>
37where
38  R: Runtime,
39  C: CollectionMarker,
40{
41  SaveHandle(Box::new(Debounce::new(duration, save_handle::<R, C>(id))))
42}
43
44pub(super) fn throttle<R, C>(id: StoreId, duration: Duration) -> SaveHandle<R>
45where
46  R: Runtime,
47  C: CollectionMarker,
48{
49  SaveHandle(Box::new(Throttle::new(duration, save_handle::<R, C>(id))))
50}
51
52fn save_handle<R, C>(id: StoreId) -> SaveHandleFn<R>
53where
54  R: Runtime,
55  C: CollectionMarker,
56{
57  Box::new(move |app| {
58    let id = id.clone();
59    Box::pin(async move {
60      let task = spawn_blocking(move || {
61        app
62          .store_collection_with_marker::<C>()
63          .get_resource(&id)?
64          .locked(|store| store.save_now())
65      });
66
67      let _ = task.await;
68    })
69  })
70}
71
72/// The strategy to use when saving a store.
73///
74/// For a detailed explanation of the differences between debouncing and throttling,
75/// take a look at [this article](https://kettanaito.com/blog/debounce-vs-throttle).
76#[non_exhaustive]
77#[derive(Clone, Copy, Debug, Default)]
78pub enum SaveStrategy {
79  #[default]
80  Immediate,
81  Debounce(Duration),
82  Throttle(Duration),
83}
84
85impl SaveStrategy {
86  const IMMEDIATE: &'static str = "immediate";
87  const DEBOUNCE: &'static str = "debounce";
88  const THROTTLE: &'static str = "throttle";
89
90  /// Returns [`Debounce`](SaveStrategy::Debounce) with the given duration, in milliseconds.
91  #[inline]
92  pub const fn debounce_millis(millis: u64) -> Self {
93    Self::Debounce(Duration::from_millis(millis))
94  }
95
96  /// Returns [`Debounce`](SaveStrategy::Debounce) with the given duration, in seconds.
97  #[inline]
98  pub const fn debounce_secs(secs: u64) -> Self {
99    Self::Debounce(Duration::from_secs(secs))
100  }
101
102  /// Returns [`Throttle`](SaveStrategy::Throttle) with the given duration, in milliseconds.
103  #[inline]
104  pub const fn throttle_millis(millis: u64) -> Self {
105    Self::Throttle(Duration::from_millis(millis))
106  }
107
108  /// Returns [`Throttle`](SaveStrategy::Throttle) with the given duration, in seconds.
109  #[inline]
110  pub const fn throttle_secs(secs: u64) -> Self {
111    Self::Throttle(Duration::from_secs(secs))
112  }
113
114  /// Whether the strategy is [`Debounce`](SaveStrategy::Debounce).
115  #[inline]
116  pub const fn is_debounce(&self) -> bool {
117    matches!(self, Self::Debounce(_))
118  }
119
120  /// Whether the strategy is [`Throttle`](SaveStrategy::Throttle).
121  #[inline]
122  pub const fn is_throttle(&self) -> bool {
123    matches!(self, Self::Throttle(_))
124  }
125}
126
127impl fmt::Display for SaveStrategy {
128  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129    match self {
130      Self::Immediate => write!(f, "{}", Self::IMMEDIATE),
131      Self::Debounce(_) => write!(f, "{}", Self::DEBOUNCE),
132      Self::Throttle(_) => write!(f, "{}", Self::THROTTLE),
133    }
134  }
135}
136
137impl Serialize for SaveStrategy {
138  fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>
139  where
140    S: Serializer,
141  {
142    let interval = match self {
143      Self::Immediate => 0,
144      Self::Debounce(duration) | Self::Throttle(duration) => duration.as_millis(),
145    };
146
147    let mut tuple = serializer.serialize_tuple(2)?;
148    tuple.serialize_element(&self.to_string())?;
149    tuple.serialize_element(&interval.to_string())?;
150    tuple.end()
151  }
152}
153
154impl<'de> Deserialize<'de> for SaveStrategy {
155  fn deserialize<D>(deserializer: D) -> StdResult<Self, D::Error>
156  where
157    D: Deserializer<'de>,
158  {
159    let err = || {
160      use serde::de::Error;
161      D::Error::custom("invalid save strategy")
162    };
163
164    let value = Value::deserialize(deserializer)?;
165    if let Value::Array(mut array) = value {
166      if array.len() != 2 {
167        return Err(err());
168      }
169
170      let strategy = array
171        .remove(0)
172        .as_str()
173        .map(ToOwned::to_owned)
174        .ok_or_else(err)?;
175
176      let duration = array
177        .remove(0)
178        .as_str()
179        .map(str::parse)
180        .ok_or_else(err)?
181        .map(Duration::from_millis)
182        .map_err(|_| err())?;
183
184      if duration.is_zero() {
185        return Ok(Self::Immediate);
186      }
187
188      match strategy.as_str() {
189        Self::DEBOUNCE => Ok(Self::Debounce(duration)),
190        Self::THROTTLE => Ok(Self::Throttle(duration)),
191        Self::IMMEDIATE => Ok(Self::Immediate),
192        _ => Err(err()),
193      }
194    } else {
195      Err(err())
196    }
197  }
198}