teksilo_settings/reload.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! [`Reloadable`] — the contract a (separately-built) file watcher uses to
5//! push a peer process's write into live state.
6//!
7//! Every persisted type in this crate is cross-process safe on the *write*
8//! side (see `flush.rs`'s `Patch` design): a write always merges against
9//! whatever is on disk, under a lock. That alone is not enough — a process
10//! that loaded its state once and never looks again will not notice a peer's
11//! write until it happens to mutate something itself. [`Reloadable`] is the
12//! *read* side of the same story: a way for an external watcher (inotify /
13//! FSEvents / ReadDirectoryChangesW, wired up outside this crate) to say
14//! "the file changed, go look," without needing to know anything about the
15//! concrete type it's reloading.
16//!
17//! ## The self-write-suppression contract
18//!
19//! A naive implementation would feed back into itself: this process writes
20//! `general.toml`, the watcher notices *that very write* a few milliseconds
21//! later, and calls `reload_from_disk()` — which had better be a cheap no-op,
22//! not a full re-parse-and-notify cycle (and, worse, must never re-apply our
23//! own value as if it were a peer's newer one, which could bounce a
24//! just-superseded value back into a live `Signal` between the user's edit
25//! and the debounced write landing).
26//!
27//! Every implementation therefore layers two checks, cheapest first:
28//!
29//! 1. **Stamp check.** Each implementor records the `(mtime, len)` of the
30//! file as of the last time it either wrote to it or read it. If the
31//! file's current stamp matches, `reload_from_disk` returns `Ok(false)`
32//! immediately — no read, no parse, nothing touched. This is the common
33//! case for a self-write notification.
34//! 2. **Content backstop.** If the stamp *did* change (a real write happened,
35//! by us or a peer, since a filesystem's mtime resolution can coincide,
36//! or the write path didn't get a chance to update the stamp), the file
37//! is read and parsed, then compared *by value* against what's already
38//! live. Only a genuine difference is pushed into signals / models;
39//! `Ok(false)` is returned — again touching nothing — when the content
40//! is unchanged. This is the actual correctness guarantee; the stamp
41//! check above is purely an optimization to skip the common case cheaply.
42//!
43//! Implementors: [`crate::SettingsFile`], [`crate::SettingsStore`],
44//! [`crate::PersistedListModel`], [`crate::WindowStateService`].
45
46use std::path::Path;
47
48use crate::file::SettingsFileError;
49
50/// A persisted type that can be told "the file may have changed on disk —
51/// go look," and will push any genuinely new content into its live
52/// signals/models.
53///
54/// This is the hook a file-system watcher calls when it observes a write to
55/// one of this crate's managed files. It is deliberately decoupled from any
56/// particular watcher implementation (inotify, kqueue, ReadDirectoryChangesW)
57/// — this crate only defines the contract; wiring an actual watcher onto it
58/// is a separate concern (a file-watcher module built on top of this trait).
59pub trait Reloadable {
60 /// The file this instance reads from and writes to. A watcher uses this
61 /// to know which path to associate with which `Reloadable` handle.
62 fn path(&self) -> &Path;
63
64 /// Re-read the file from disk and push any genuinely new content into
65 /// live signals/models.
66 ///
67 /// Returns `Ok(true)` if the in-memory state changed as a result,
68 /// `Ok(false)` if nothing needed to change (including the common
69 /// self-write-notification case — see the module docs' "self-write
70 /// suppression contract"). `Ok(false)` is a hard guarantee that nothing
71 /// was touched: no signal fired, no model mutated.
72 fn reload_from_disk(&self) -> Result<bool, SettingsFileError>;
73}