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::{error::Error, 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)]
90pub enum RestorationKeyError {
91    /// A path segment must identify something explicitly.
92    Empty,
93    /// NUL is rejected so a key remains suitable for portable storage paths.
94    ContainsNul,
95}
96
97impl fmt::Display for RestorationKeyError {
98    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            Self::Empty => formatter.write_str("a restoration key cannot be empty"),
101            Self::ContainsNul => formatter.write_str("a restoration key cannot contain NUL"),
102        }
103    }
104}
105
106impl Error for RestorationKeyError {}
107
108/// Raw JSON storage implemented by the runtime's restoration manager.
109///
110/// Implementations are UI-thread-affine by design. They update in-memory
111/// state synchronously and are responsible for coalescing any asynchronous
112/// persistence work. Paths are always structured key segments, avoiding
113/// ambiguous string concatenation.
114pub trait RestorationBackend {
115    /// Reads a value at the fully-qualified path.
116    fn read_value(&self, path: &[RestorationKey]) -> Option<Value>;
117
118    /// Replaces a value at the fully-qualified path.
119    fn write_value(&self, path: &[RestorationKey], value: Value);
120
121    /// Removes a value at the fully-qualified path.
122    fn remove_value(&self, path: &[RestorationKey]);
123
124    /// Records restoration recovery outcomes observed outside the runtime
125    /// (scroll offsets, type-incompatible values). The default implementation
126    /// ignores them so test backends stay trivial; the runtime counts
127    /// `restored` as recovered positions and `invalid` as stale values.
128    fn note_restoration_outcome(&self, _restored: u64, _invalid: u64) {}
129}
130
131/// A restoration location supplied by the runtime to an opt-in state owner.
132///
133/// Application code normally receives scopes from runtime build APIs rather
134/// than constructing them. `new` remains public so embedded runtimes and
135/// deterministic in-memory tests can provide their own backend.
136#[derive(Clone)]
137pub struct RestorationScope {
138    backend: Rc<dyn RestorationBackend>,
139    path: Vec<RestorationKey>,
140}
141
142impl fmt::Debug for RestorationScope {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        formatter
145            .debug_struct("RestorationScope")
146            .field("path", &self.path)
147            .finish_non_exhaustive()
148    }
149}
150
151impl RestorationScope {
152    /// Creates the application-root restoration scope for a backend.
153    #[must_use]
154    pub fn root(backend: Rc<dyn RestorationBackend>) -> Self {
155        Self {
156            backend,
157            path: Vec::new(),
158        }
159    }
160
161    /// Creates a scope at an already-validated, fully-qualified path.
162    #[must_use]
163    pub fn new(
164        backend: Rc<dyn RestorationBackend>,
165        path: impl IntoIterator<Item = RestorationKey>,
166    ) -> Self {
167        Self {
168            backend,
169            path: path.into_iter().collect(),
170        }
171    }
172
173    /// Extends this scope with one already-registered stable child segment.
174    ///
175    /// Runtime is responsible for registering and checking sibling scope
176    /// identities before it exposes this scope. This method deliberately does
177    /// no implicit child numbering or position-based identity assignment.
178    #[must_use]
179    pub fn child_unchecked(&self, key: RestorationKey) -> Self {
180        let mut path = self.path.clone();
181        path.push(key);
182        Self {
183            backend: self.backend.clone(),
184            path,
185        }
186    }
187
188    /// Extends this scope with one stable child segment.
189    ///
190    /// This is pure namespacing: it performs no duplicate-scope validation.
191    /// The runtime claims window scopes automatically, but application-owned
192    /// dynamic scopes that must be unique while mounted should additionally
193    /// hold a runtime scope claim (see `RestorationHandle::claim_scope`);
194    /// two live writers to one path otherwise collide last-write-wins.
195    #[must_use]
196    pub fn child(&self, key: RestorationKey) -> Self {
197        self.child_unchecked(key)
198    }
199
200    #[must_use]
201    pub fn is_root(&self) -> bool {
202        self.path.is_empty()
203    }
204
205    /// Returns the stable hierarchical path owned by this scope.
206    #[must_use]
207    pub fn path(&self) -> &[RestorationKey] {
208        &self.path
209    }
210
211    /// Reads the JSON value stored under a stable value key.
212    #[must_use]
213    pub fn get_json(&self, key: &RestorationKey) -> Option<Value> {
214        self.backend.read_value(&self.value_path(key))
215    }
216
217    /// Stores a JSON value under a stable value key.
218    ///
219    /// This only updates the runtime manager's in-memory snapshot. Its
220    /// persistence policy determines when an asynchronous disk save occurs.
221    pub fn set_json(&self, key: &RestorationKey, value: Value) {
222        self.backend.write_value(&self.value_path(key), value);
223    }
224
225    /// Removes the JSON value stored under a stable value key.
226    pub fn remove(&self, key: &RestorationKey) {
227        self.backend.remove_value(&self.value_path(key));
228    }
229
230    /// Forwards a recovery outcome to the backend. Scroll restoration and
231    /// typed-value fallbacks report here; backends that do not count outcomes
232    /// ignore the call.
233    pub fn note_restoration_outcome(&self, restored: u64, invalid: u64) {
234        self.backend.note_restoration_outcome(restored, invalid);
235    }
236
237    fn value_path(&self, key: &RestorationKey) -> Vec<RestorationKey> {
238        let mut path = self.path.clone();
239        path.push(key.clone());
240        path
241    }
242}