1use std::sync::Arc;
15use std::time::Duration;
16
17use serde::Serialize;
18use serde::de::DeserializeOwned;
19use serde_json::{Map, Value};
20
21use crate::backend::ConfigStore;
22
23pub type SettingsMigration = fn(&mut Map<String, Value>);
25
26pub const MAX_SETTINGS_BYTES: usize = 16 << 20;
28
29pub const DEFAULT_DEBOUNCE: Duration = Duration::from_millis(500);
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum LoadState {
35 Fresh,
37 Loaded { from: u64 },
39 Corrupt,
41 TooNew { found: u64 },
43}
44
45pub type SettingsWake = Arc<dyn Fn(Duration) + Send + Sync>;
47
48const DUE_SLACK: Duration = Duration::from_millis(2);
50
51pub struct Settings {
53 store: Arc<dyn ConfigStore>,
54 name: String,
55 schema_version: u64,
56 values: Map<String, Value>,
57 state: LoadState,
58 debounce: Duration,
59 generation: u64,
61 saved_gen: u64,
62 seen: Option<(u64, Duration)>,
64 last_tick: Option<Duration>,
67 wake: Option<SettingsWake>,
68}
69
70impl std::fmt::Debug for Settings {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("Settings").field("name", &self.name).field("state", &self.state).field("values", &self.values).finish()
73 }
74}
75
76impl Settings {
77 pub fn load(store: Arc<dyn ConfigStore>, name: &str, migrations: &[SettingsMigration]) -> Settings {
80 let schema_version = migrations.len() as u64 + 1;
81 let mut s = Settings {
82 store,
83 name: name.to_string(),
84 schema_version,
85 values: Map::new(),
86 state: LoadState::Fresh,
87 debounce: DEFAULT_DEBOUNCE,
88 generation: 0,
89 saved_gen: 0,
90 seen: None,
91 last_tick: None,
92 wake: None,
93 };
94 let Some(text) = s.store.read(name) else { return s };
95 match parse(&text) {
96 Some((found, values)) if found > schema_version => {
97 log::warn!("settings `{name}` are from a newer schema (v{found} > v{schema_version}); they will not be saved");
98 s.values = values;
99 s.state = LoadState::TooNew { found };
100 }
101 Some((found, mut values)) => {
102 for m in migrations.iter().skip(found.saturating_sub(1) as usize) {
103 m(&mut values);
104 }
105 s.values = values;
106 s.state = LoadState::Loaded { from: found };
107 if found < schema_version {
108 s.generation = 1; }
110 }
111 None => {
112 log::warn!("settings `{name}` are unreadable; moved to `{name}.corrupt`");
113 let _ = s.store.write(&format!("{name}.corrupt"), &text);
114 s.state = LoadState::Corrupt;
115 }
116 }
117 s
118 }
119
120 pub fn with_debounce(mut self, debounce: Duration) -> Settings {
122 self.debounce = debounce;
123 self
124 }
125
126 pub fn set_wake(&mut self, wake: impl Fn(Duration) + Send + Sync + 'static) {
132 self.wake = Some(Arc::new(wake));
133 }
134
135 fn wake(&self, delay: Duration) {
136 if let Some(w) = &self.wake {
137 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| w(delay)));
139 }
140 }
141
142 pub fn state(&self) -> LoadState {
143 self.state
144 }
145
146 pub fn schema_version(&self) -> u64 {
147 self.schema_version
148 }
149
150 pub fn get_value(&self, key: &str) -> Option<&Value> {
154 self.values.get(key)
155 }
156
157 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
159 T::deserialize(self.values.get(key)?).ok()
160 }
161
162 pub fn get_or<T: DeserializeOwned>(&self, key: &str, default: T) -> T {
164 self.get(key).unwrap_or(default)
165 }
166
167 pub fn set_value(&mut self, key: &str, value: Value) {
169 if self.values.get(key) != Some(&value) {
170 self.values.insert(key.to_string(), value);
171 self.changed();
172 }
173 }
174
175 pub fn set<T: Serialize + ?Sized>(&mut self, key: &str, value: &T) -> Result<(), serde_json::Error> {
178 let v = serde_json::to_value(value)?;
179 self.set_value(key, v);
180 Ok(())
181 }
182
183 pub fn remove(&mut self, key: &str) -> Option<Value> {
185 let old = self.values.remove(key);
186 if old.is_some() {
187 self.changed();
188 }
189 old
190 }
191
192 pub fn keys(&self) -> impl Iterator<Item = &str> {
194 self.values.keys().map(String::as_str)
195 }
196
197 fn changed(&mut self) {
198 self.generation = self.generation.wrapping_add(1);
199 self.wake(self.debounce);
200 }
201
202 pub fn is_dirty(&self) -> bool {
206 self.generation != self.saved_gen
207 }
208
209 pub fn tick(&mut self, now: Duration) -> std::io::Result<bool> {
212 let last = self.last_tick.replace(now);
213 if !self.is_dirty() {
214 self.seen = None;
215 return Ok(false);
216 }
217 match self.seen {
218 Some((g, since)) if g == self.generation => {
219 let waited = now.saturating_sub(since).saturating_add(DUE_SLACK);
222 if waited >= self.debounce {
223 self.flush()?;
224 return Ok(true);
225 }
226 }
228 _ => {
230 let since = last.filter(|l| *l <= now).unwrap_or(now);
231 self.seen = Some((self.generation, since));
232 if last.is_none() {
233 self.wake(self.debounce);
234 }
235 }
236 }
237 Ok(false)
238 }
239
240 pub fn flush(&mut self) -> std::io::Result<()> {
242 if !self.is_dirty() {
243 return Ok(());
244 }
245 if matches!(self.state, LoadState::TooNew { .. }) {
246 self.saved_gen = self.generation;
247 return Ok(());
248 }
249 let doc = serde_json::json!({ "schema_version": self.schema_version, "values": self.values });
250 let text = serde_json::to_string_pretty(&doc).map_err(std::io::Error::other)?;
251 self.store.write(&self.name, &text)?;
252 self.saved_gen = self.generation;
253 self.seen = None;
254 Ok(())
255 }
256}
257
258fn parse(text: &str) -> Option<(u64, Map<String, Value>)> {
259 if text.len() > MAX_SETTINGS_BYTES {
260 return None;
261 }
262 let Value::Object(mut o) = serde_json::from_str::<Value>(text).ok()? else { return None };
263 let version = o.get("schema_version")?.as_u64()?;
264 if version == 0 {
265 return None;
266 }
267 let Value::Object(values) = o.remove("values")? else { return None };
268 Some((version, values))
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::backend::MemoryConfig;
275
276 fn mem() -> Arc<dyn ConfigStore> {
277 Arc::new(MemoryConfig::new())
278 }
279
280 const MS: Duration = Duration::from_millis(1);
281
282 #[test]
283 fn typed_get_set_remove_keys() {
284 let mut s = Settings::load(mem(), "settings.json", &[]);
285 assert_eq!(s.state(), LoadState::Fresh);
286 s.set("editor.font_size", &14u32).unwrap();
287 s.set("theme", "dark").unwrap();
288 s.set("list", &vec![1, 2]).unwrap();
289 assert_eq!(s.get::<u32>("editor.font_size"), Some(14));
290 assert_eq!(s.get::<String>("editor.font_size"), None, "wrong type is None");
291 assert_eq!(s.get_or("missing", 7u8), 7);
292 assert_eq!(s.keys().collect::<Vec<_>>(), vec!["editor.font_size", "list", "theme"]);
293 assert_eq!(s.remove("theme"), Some(Value::from("dark")));
294 assert_eq!(s.remove("theme"), None);
295 assert!(s.set("bad", &f64::NAN).is_ok(), "NaN serializes to null");
296 let mut m = std::collections::HashMap::new();
297 m.insert((1, 2), 3);
298 assert!(s.set("bad", &m).is_err());
299 }
300
301 #[test]
302 fn debounced_save_waits_for_quiet() {
303 let store = mem();
304 let mut s = Settings::load(store.clone(), "s.json", &[]).with_debounce(100 * MS);
305 assert!(!s.tick(Duration::ZERO).unwrap());
306 s.set("a", &1).unwrap();
307 assert!(!s.tick(Duration::ZERO).unwrap());
308 assert!(!s.tick(50 * MS).unwrap());
309 s.set("a", &2).unwrap(); assert!(!s.tick(60 * MS).unwrap());
311 assert!(!s.tick(140 * MS).unwrap());
312 assert!(store.read("s.json").is_none());
313 assert!(s.tick(150 * MS).unwrap());
314 assert!(!s.is_dirty());
315 let back = Settings::load(store, "s.json", &[]);
316 assert_eq!(back.get::<i32>("a"), Some(2));
317 assert_eq!(back.state(), LoadState::Loaded { from: 1 });
318 }
319
320 #[test]
321 fn the_wake_hook_says_when_the_next_tick_is_due() {
322 let store = mem();
323 let asked = Arc::new(std::sync::Mutex::new(Vec::new()));
324 let mut s = Settings::load(store.clone(), "s.json", &[]).with_debounce(100 * MS);
325 let a = Arc::clone(&asked);
326 s.set_wake(move |d| a.lock().unwrap().push(d));
327 assert!(!s.tick(Duration::ZERO).unwrap());
328 assert!(asked.lock().unwrap().is_empty(), "nothing pending, nothing asked");
329 s.set("a", &1).unwrap();
330 assert_eq!(*asked.lock().unwrap(), vec![100 * MS], "a change asks for a tick after the debounce");
331 assert!(!s.tick(10 * MS).unwrap());
332 assert!(!s.tick(40 * MS).unwrap());
333 assert_eq!(asked.lock().unwrap().len(), 1, "the save is asked for once, when the change happens, not per tick");
334 assert!(s.tick(110 * MS).unwrap());
336 assert!(store.read("s.json").is_some());
337 let n = asked.lock().unwrap().len();
338 assert!(!s.tick(500 * MS).unwrap());
339 assert_eq!(asked.lock().unwrap().len(), n, "a clean tick asks for nothing");
340 s.set_wake(|_| panic!("hook"));
342 s.set("a", &2).unwrap();
343 assert!(s.is_dirty());
344 }
345
346 #[test]
347 fn unchanged_set_is_not_a_change() {
348 let mut s = Settings::load(mem(), "s.json", &[]);
349 s.set("a", &1).unwrap();
350 s.flush().unwrap();
351 s.set("a", &1).unwrap();
352 assert!(!s.is_dirty());
353 }
354
355 #[test]
356 fn corrupt_file_is_moved_aside() {
357 for bad in [
358 "{",
359 "",
360 "[]",
361 "null",
362 r#"{"values":{}}"#,
363 r#"{"schema_version":0,"values":{}}"#,
364 r#"{"schema_version":"1","values":{}}"#,
365 r#"{"schema_version":1,"values":[]}"#,
366 ] {
367 let store = mem();
368 store.write("s.json", bad).unwrap();
369 let s = Settings::load(store.clone(), "s.json", &[]);
370 assert_eq!(s.state(), LoadState::Corrupt, "{bad}");
371 assert_eq!(store.read("s.json.corrupt").as_deref(), Some(bad));
372 assert_eq!(s.keys().count(), 0);
373 }
374 }
375
376 #[test]
377 fn newer_schema_is_read_but_never_overwritten() {
378 let store = mem();
379 let newer = r#"{"schema_version":18446744073709551615,"values":{"a":1}}"#;
380 store.write("s.json", newer).unwrap();
381 let mut s = Settings::load(store.clone(), "s.json", &[]);
382 assert_eq!(s.state(), LoadState::TooNew { found: u64::MAX });
383 assert_eq!(s.get::<i32>("a"), Some(1));
384 s.set("a", &2).unwrap();
385 s.flush().unwrap();
386 assert_eq!(store.read("s.json").as_deref(), Some(newer));
387 assert!(!s.is_dirty());
388 }
389
390 #[test]
391 fn migrations_run_from_the_file_version() {
392 fn v1_to_v2(m: &mut Map<String, Value>) {
393 if let Some(v) = m.remove("fontSize") {
394 m.insert("font_size".into(), v);
395 }
396 }
397 fn v2_to_v3(m: &mut Map<String, Value>) {
398 m.entry("theme").or_insert(Value::from("light"));
399 }
400 let store = mem();
401 store.write("s.json", r#"{"schema_version":1,"values":{"fontSize":12}}"#).unwrap();
402 let mut s = Settings::load(store.clone(), "s.json", &[v1_to_v2, v2_to_v3]);
403 assert_eq!(s.state(), LoadState::Loaded { from: 1 });
404 assert_eq!(s.get::<i32>("font_size"), Some(12));
405 assert_eq!(s.get::<String>("theme").as_deref(), Some("light"));
406 assert!(s.is_dirty(), "upgraded file is saved");
407 s.flush().unwrap();
408 let v: Value = serde_json::from_str(&store.read("s.json").unwrap()).unwrap();
409 assert_eq!(v["schema_version"], 3);
410
411 store.write("s.json", r#"{"schema_version":2,"values":{"fontSize":9}}"#).unwrap();
412 let s = Settings::load(store, "s.json", &[v1_to_v2, v2_to_v3]);
413 assert_eq!(s.get::<i32>("fontSize"), Some(9), "v1→v2 skipped for a v2 file");
414 }
415}