Skip to main content

incular_core/
restoration.rs

1//! The renderer-independent boundary used by opt-in state restoration.
2//!
3//! `incular-runtime` owns restoration hierarchy, persistence, diagnostics,
4//! and duplicate-scope detection. Lower-level crates use this small boundary
5//! to restore and update JSON values without depending on the runtime.
6
7use std::{fmt, rc::Rc};
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
10use serde_json::Value;
11
12/// A stable, application-provided segment in a restoration path.
13///
14/// A key is a segment, not a pre-concatenated path. In particular, a slash is
15/// ordinary key content rather than an implicit hierarchy separator; runtime
16/// stores paths structurally and applies its deterministic escaping only when
17/// it needs a textual representation.
18#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct RestorationKey(String);
20
21impl RestorationKey {
22    /// Creates one stable restoration path segment.
23    pub fn new(segment: impl Into<String>) -> Result<Self, RestorationKeyError> {
24        let segment = segment.into();
25        if segment.is_empty() {
26            return Err(RestorationKeyError::Empty);
27        }
28        if segment.contains('\0') {
29            return Err(RestorationKeyError::ContainsNul);
30        }
31        Ok(Self(segment))
32    }
33
34    /// Borrows the application-provided segment.
35    #[must_use]
36    pub fn as_str(&self) -> &str {
37        &self.0
38    }
39
40    /// Consumes this key into its application-provided segment.
41    #[must_use]
42    pub fn into_inner(self) -> String {
43        self.0
44    }
45}
46
47impl TryFrom<String> for RestorationKey {
48    type Error = RestorationKeyError;
49
50    fn try_from(value: String) -> Result<Self, Self::Error> {
51        Self::new(value)
52    }
53}
54
55impl TryFrom<&str> for RestorationKey {
56    type Error = RestorationKeyError;
57
58    fn try_from(value: &str) -> Result<Self, Self::Error> {
59        Self::new(value)
60    }
61}
62
63impl fmt::Display for RestorationKey {
64    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65        formatter.write_str(self.as_str())
66    }
67}
68
69impl Serialize for RestorationKey {
70    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
71    where
72        S: Serializer,
73    {
74        self.0.serialize(serializer)
75    }
76}
77
78impl<'de> Deserialize<'de> for RestorationKey {
79    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80    where
81        D: Deserializer<'de>,
82    {
83        String::deserialize(deserializer)
84            .and_then(|segment| Self::new(segment).map_err(de::Error::custom))
85    }
86}
87
88/// Why a [`RestorationKey`] could not be created.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
90pub enum RestorationKeyError {
91    /// A path segment must identify something explicitly.
92    #[error("a restoration key cannot be empty")]
93    Empty,
94    /// NUL is rejected so a key remains suitable for portable storage paths.
95    #[error("a restoration key cannot contain NUL")]
96    ContainsNul,
97}
98
99/// Raw JSON storage implemented by the runtime's restoration manager.
100///
101/// Implementations are UI-thread-affine by design. They update in-memory
102/// state synchronously and are responsible for coalescing any asynchronous
103/// persistence work. Paths are always structured key segments, avoiding
104/// ambiguous string concatenation.
105pub trait RestorationBackend {
106    /// Reads a value at the fully-qualified path.
107    fn read_value(&self, path: &[RestorationKey]) -> Option<Value>;
108
109    /// Replaces a value at the fully-qualified path.
110    fn write_value(&self, path: &[RestorationKey], value: Value);
111
112    /// Removes a value at the fully-qualified path.
113    fn remove_value(&self, path: &[RestorationKey]);
114
115    /// Records restoration recovery outcomes observed outside the runtime
116    /// (scroll offsets, type-incompatible values). The default implementation
117    /// ignores them so test backends stay trivial; the runtime counts
118    /// `restored` as recovered positions and `invalid` as stale values.
119    fn note_restoration_outcome(&self, _restored: u64, _invalid: u64) {}
120}
121
122/// A restoration location supplied by the runtime to an opt-in state owner.
123///
124/// Application code normally receives scopes from runtime build APIs rather
125/// than constructing them. `new` remains public so embedded runtimes and
126/// deterministic in-memory tests can provide their own backend.
127#[derive(Clone)]
128pub struct RestorationScope {
129    backend: Rc<dyn RestorationBackend>,
130    path: Vec<RestorationKey>,
131}
132
133impl fmt::Debug for RestorationScope {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        formatter
136            .debug_struct("RestorationScope")
137            .field("path", &self.path)
138            .finish_non_exhaustive()
139    }
140}
141
142impl RestorationScope {
143    /// Creates the application-root restoration scope for a backend.
144    #[must_use]
145    pub fn root(backend: Rc<dyn RestorationBackend>) -> Self {
146        Self {
147            backend,
148            path: Vec::new(),
149        }
150    }
151
152    /// Creates a scope at an already-validated, fully-qualified path.
153    #[must_use]
154    pub fn new(
155        backend: Rc<dyn RestorationBackend>,
156        path: impl IntoIterator<Item = RestorationKey>,
157    ) -> Self {
158        Self {
159            backend,
160            path: path.into_iter().collect(),
161        }
162    }
163
164    /// Extends this scope with one already-registered stable child segment.
165    ///
166    /// Runtime is responsible for registering and checking sibling scope
167    /// identities before it exposes this scope. This method deliberately does
168    /// no implicit child numbering or position-based identity assignment.
169    #[must_use]
170    pub fn child_unchecked(&self, key: RestorationKey) -> Self {
171        let mut path = self.path.clone();
172        path.push(key);
173        Self {
174            backend: self.backend.clone(),
175            path,
176        }
177    }
178
179    /// Extends this scope with one stable child segment.
180    ///
181    /// This is pure namespacing: it performs no duplicate-scope validation.
182    /// The runtime claims window scopes automatically, but application-owned
183    /// dynamic scopes that must be unique while mounted should additionally
184    /// hold a runtime scope claim (see `RestorationHandle::claim_scope`);
185    /// two live writers to one path otherwise collide last-write-wins.
186    #[must_use]
187    pub fn child(&self, key: RestorationKey) -> Self {
188        self.child_unchecked(key)
189    }
190
191    #[must_use]
192    pub fn is_root(&self) -> bool {
193        self.path.is_empty()
194    }
195
196    /// Returns the stable hierarchical path owned by this scope.
197    #[must_use]
198    pub fn path(&self) -> &[RestorationKey] {
199        &self.path
200    }
201
202    /// Reads the JSON value stored under a stable value key.
203    #[must_use]
204    pub fn get_json(&self, key: &RestorationKey) -> Option<Value> {
205        self.backend.read_value(&self.value_path(key))
206    }
207
208    /// Stores a JSON value under a stable value key.
209    ///
210    /// This only updates the runtime manager's in-memory snapshot. Its
211    /// persistence policy determines when an asynchronous disk save occurs.
212    pub fn set_json(&self, key: &RestorationKey, value: Value) {
213        self.backend.write_value(&self.value_path(key), value);
214    }
215
216    /// Removes the JSON value stored under a stable value key.
217    pub fn remove(&self, key: &RestorationKey) {
218        self.backend.remove_value(&self.value_path(key));
219    }
220
221    /// Forwards a recovery outcome to the backend. Scroll restoration and
222    /// typed-value fallbacks report here; backends that do not count outcomes
223    /// ignore the call.
224    pub fn note_restoration_outcome(&self, restored: u64, invalid: u64) {
225        self.backend.note_restoration_outcome(restored, invalid);
226    }
227
228    fn value_path(&self, key: &RestorationKey) -> Vec<RestorationKey> {
229        let mut path = self.path.clone();
230        path.push(key.clone());
231        path
232    }
233}