1use std::{
8 collections::{BTreeMap, BTreeSet, HashSet},
9 fmt,
10 fs::{self, File},
11 io::Read,
12 path::{Path, PathBuf},
13 rc::{Rc, Weak},
14 sync::{Arc, Mutex},
15 time::Duration,
16};
17
18use serde::{Deserialize, Serialize, de::DeserializeOwned};
19use serde_json::Value;
20
21use incular_core::{RestorationBackend, RestorationKey, RestorationScope};
22
23use crate::{Signal, tasks};
24
25pub const FRAMEWORK_RESTORATION_FORMAT_VERSION: u32 = 1;
28
29pub const DEFAULT_RESTORATION_DEBOUNCE: Duration = Duration::from_millis(250);
32
33pub const DEFAULT_RESTORATION_SNAPSHOT_LIMIT: usize = 8 * 1024 * 1024;
36
37pub const MAX_RESTORATION_SAVE_RETRIES: u32 = 3;
41
42pub const RESTORATION_RETRY_BACKOFF: Duration = Duration::from_millis(500);
44
45#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
48#[error("{message}")]
49pub struct RestorationStoreError {
50 message: String,
51}
52
53impl RestorationStoreError {
54 #[must_use]
55 pub fn new(message: impl Into<String>) -> Self {
56 Self {
57 message: message.into(),
58 }
59 }
60}
61
62pub trait RestorationStore: Send + Sync + 'static {
65 fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError>;
66 fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError>;
67 fn remove(&self) -> Result<(), RestorationStoreError>;
68
69 fn location(&self) -> Option<PathBuf> {
72 None
73 }
74}
75
76#[derive(Default)]
80pub struct InMemoryRestorationStore {
81 bytes: Mutex<Option<Vec<u8>>>,
82 fail_next_save: Mutex<Option<String>>,
83}
84
85impl InMemoryRestorationStore {
86 #[must_use]
87 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn fail_next_save(&self, message: impl Into<String>) {
92 *self
93 .fail_next_save
94 .lock()
95 .expect("restoration test-store failure mutex") = Some(message.into());
96 }
97
98 #[must_use]
99 pub fn bytes(&self) -> Option<Vec<u8>> {
100 self.bytes
101 .lock()
102 .expect("restoration test-store bytes mutex")
103 .clone()
104 }
105
106 pub fn set_bytes(&self, bytes: Option<Vec<u8>>) {
107 *self
108 .bytes
109 .lock()
110 .expect("restoration test-store bytes mutex") = bytes;
111 }
112}
113
114impl RestorationStore for InMemoryRestorationStore {
115 fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError> {
116 Ok(self.bytes())
117 }
118
119 fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError> {
120 if let Some(message) = self
121 .fail_next_save
122 .lock()
123 .expect("restoration test-store failure mutex")
124 .take()
125 {
126 return Err(RestorationStoreError::new(message));
127 }
128 *self
129 .bytes
130 .lock()
131 .expect("restoration test-store bytes mutex") = Some(bytes.to_vec());
132 Ok(())
133 }
134
135 fn remove(&self) -> Result<(), RestorationStoreError> {
136 *self
137 .bytes
138 .lock()
139 .expect("restoration test-store bytes mutex") = None;
140 Ok(())
141 }
142}
143
144#[derive(Clone, Debug)]
151pub struct FileRestorationStore {
152 path: PathBuf,
153}
154
155impl FileRestorationStore {
156 #[must_use]
157 pub fn new(path: impl Into<PathBuf>) -> Self {
158 Self { path: path.into() }
159 }
160
161 pub fn for_application(application_id: &str) -> Result<Self, RestorationStoreError> {
167 validate_application_id(application_id)?;
168 let base = incular_platform::application_data_local_directory(application_id).ok_or_else(
169 || {
170 RestorationStoreError::new(
171 "standard application data directory is unavailable for restoration",
172 )
173 },
174 )?;
175 Ok(Self::new(base.join("restoration.json")))
176 }
177
178 #[must_use]
179 pub fn path(&self) -> &Path {
180 &self.path
181 }
182}
183
184impl RestorationStore for FileRestorationStore {
185 fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError> {
186 match File::open(&self.path) {
187 Ok(mut file) => {
188 let mut bytes = Vec::new();
189 file.read_to_end(&mut bytes)
190 .map_err(|error| RestorationStoreError::new(error.to_string()))?;
191 Ok(Some(bytes))
192 }
193 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
194 Err(error) => Err(RestorationStoreError::new(error.to_string())),
195 }
196 }
197
198 #[cfg(target_arch = "wasm32")]
199 fn save(&self, _bytes: &[u8]) -> Result<(), RestorationStoreError> {
200 Err(RestorationStoreError::new(
201 "file restoration snapshots are unavailable in browsers",
202 ))
203 }
204
205 #[cfg(not(target_arch = "wasm32"))]
206 fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError> {
207 let parent = self.path.parent().ok_or_else(|| {
208 RestorationStoreError::new("restoration snapshot path has no parent directory")
209 })?;
210 fs::create_dir_all(parent)
211 .map_err(|error| RestorationStoreError::new(error.to_string()))?;
212 use std::io::Write as _;
213
214 let mut temporary = tempfile::NamedTempFile::new_in(parent)
215 .map_err(|error| RestorationStoreError::new(error.to_string()))?;
216 temporary
217 .write_all(bytes)
218 .and_then(|()| temporary.flush())
219 .and_then(|()| temporary.as_file().sync_all())
220 .map_err(|error| RestorationStoreError::new(error.to_string()))?;
221 temporary
222 .persist(&self.path)
223 .map_err(|error| RestorationStoreError::new(error.error.to_string()))?;
224 Ok(())
227 }
228
229 fn remove(&self) -> Result<(), RestorationStoreError> {
230 match fs::remove_file(&self.path) {
231 Ok(()) => Ok(()),
232 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
233 Err(error) => Err(RestorationStoreError::new(error.to_string())),
234 }
235 }
236
237 fn location(&self) -> Option<PathBuf> {
238 Some(self.path.clone())
239 }
240}
241
242#[cfg(target_arch = "wasm32")]
247#[derive(Clone, Debug)]
248pub struct BrowserRestorationStore {
249 key: String,
250}
251
252#[cfg(target_arch = "wasm32")]
253impl BrowserRestorationStore {
254 pub fn for_application(application_id: &str) -> Result<Self, RestorationStoreError> {
257 validate_application_id(application_id)?;
258 Ok(Self {
259 key: format!("incular.restoration.{application_id}"),
260 })
261 }
262
263 #[must_use]
264 pub fn key(&self) -> &str {
265 &self.key
266 }
267
268 fn storage() -> Result<web_sys::Storage, RestorationStoreError> {
269 web_sys::window()
270 .and_then(|window| window.local_storage().ok().flatten())
271 .ok_or_else(|| RestorationStoreError::new("localStorage is unavailable"))
272 }
273}
274
275#[cfg(target_arch = "wasm32")]
276fn storage_error(error: wasm_bindgen::JsValue) -> RestorationStoreError {
277 RestorationStoreError::new(format!("localStorage: {error:?}"))
278}
279
280#[cfg(target_arch = "wasm32")]
281impl RestorationStore for BrowserRestorationStore {
282 fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError> {
283 Ok(Self::storage()?
284 .get_item(&self.key)
285 .map_err(storage_error)?
286 .map(String::into_bytes))
287 }
288
289 fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError> {
290 let text = std::str::from_utf8(bytes)
291 .map_err(|_| RestorationStoreError::new("restoration snapshot is not UTF-8"))?;
292 Self::storage()?
293 .set_item(&self.key, text)
294 .map_err(storage_error)
295 }
296
297 fn remove(&self) -> Result<(), RestorationStoreError> {
298 Self::storage()?
299 .remove_item(&self.key)
300 .map_err(storage_error)
301 }
302
303 fn location(&self) -> Option<PathBuf> {
304 Some(PathBuf::from(format!("localStorage:{}", self.key)))
305 }
306}
307
308#[derive(Clone)]
310pub struct RestorationConfig {
311 application_id: String,
312 application_schema_version: u32,
313 store: Arc<dyn RestorationStore>,
314 snapshot_limit: usize,
315 debounce: Duration,
316 migration: Option<RestorationMigration>,
317}
318
319impl fmt::Debug for RestorationConfig {
320 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
321 formatter
322 .debug_struct("RestorationConfig")
323 .field("application_id", &self.application_id)
324 .field(
325 "application_schema_version",
326 &self.application_schema_version,
327 )
328 .field("snapshot_limit", &self.snapshot_limit)
329 .field("debounce", &self.debounce)
330 .field("store_location", &self.store.location())
331 .finish_non_exhaustive()
332 }
333}
334
335pub type RestorationMigration = Arc<dyn Fn(u32, &mut Value) -> Result<(), String> + 'static>;
339
340impl RestorationConfig {
341 #[must_use]
342 pub fn new(
343 application_id: impl Into<String>,
344 application_schema_version: u32,
345 store: Arc<dyn RestorationStore>,
346 ) -> Self {
347 Self {
348 application_id: application_id.into(),
349 application_schema_version,
350 store,
351 snapshot_limit: DEFAULT_RESTORATION_SNAPSHOT_LIMIT,
352 debounce: DEFAULT_RESTORATION_DEBOUNCE,
353 migration: None,
354 }
355 }
356
357 pub fn file_backed(
361 application_id: impl Into<String>,
362 application_schema_version: u32,
363 ) -> Result<Self, RestorationStoreError> {
364 let application_id = application_id.into();
365 #[cfg(not(target_arch = "wasm32"))]
366 let store = Arc::new(FileRestorationStore::for_application(&application_id)?);
367 #[cfg(target_arch = "wasm32")]
368 let store = Arc::new(BrowserRestorationStore::for_application(&application_id)?);
369 Ok(Self::new(application_id, application_schema_version, store))
370 }
371
372 #[must_use]
373 pub fn with_snapshot_limit(mut self, bytes: usize) -> Self {
374 self.snapshot_limit = bytes.max(1);
375 self
376 }
377
378 #[must_use]
379 pub fn with_debounce(mut self, debounce: Duration) -> Self {
380 self.debounce = debounce;
381 self
382 }
383
384 #[must_use]
385 pub fn with_migration(mut self, migration: RestorationMigration) -> Self {
386 self.migration = Some(migration);
387 self
388 }
389
390 #[must_use]
391 pub fn application_id(&self) -> &str {
392 &self.application_id
393 }
394
395 #[must_use]
396 pub const fn application_schema_version(&self) -> u32 {
397 self.application_schema_version
398 }
399}
400
401#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
404pub struct RestoredWindow {
405 pub restoration_id: String,
406 pub kind: String,
407 pub logical_width: f32,
408 pub logical_height: f32,
409 pub maximized: bool,
410 pub fullscreen: bool,
411}
412
413#[derive(Clone, Debug, Serialize, Deserialize)]
414struct SnapshotDocument {
415 framework_format_version: u32,
416 application_schema_version: u32,
417 snapshot_generation: u64,
418 application_id: String,
419 #[serde(default)]
420 state: BTreeMap<String, Value>,
421 #[serde(default)]
422 windows: Vec<RestoredWindow>,
423}
424
425impl SnapshotDocument {
426 fn empty(config: &RestorationConfig) -> Self {
427 Self {
428 framework_format_version: FRAMEWORK_RESTORATION_FORMAT_VERSION,
429 application_schema_version: config.application_schema_version,
430 snapshot_generation: 0,
431 application_id: config.application_id.clone(),
432 state: BTreeMap::new(),
433 windows: Vec::new(),
434 }
435 }
436}
437
438#[derive(Clone, Debug, Default, PartialEq, Eq)]
443#[non_exhaustive]
444pub struct RestorationDiagnostics {
445 pub restoration_loads: u64,
446 pub restoration_load_failures: u64,
447 pub restoration_migrations: u64,
448 pub restoration_values: usize,
449 pub restoration_scopes: usize,
450 pub dirty_generation: u64,
451 pub persisted_generation: u64,
452 pub save_requests: u64,
453 pub save_coalesced: u64,
454 pub saves_started: u64,
455 pub saves_completed: u64,
456 pub save_failures: u64,
457 pub snapshot_bytes: usize,
458 pub peak_snapshot_bytes: usize,
459 pub restored_windows: u64,
460 pub skipped_windows: u64,
461 pub restored_routes: u64,
462 pub invalid_routes: u64,
463 pub restored_scroll_positions: u64,
464 pub stale_or_invalid_values: u64,
465 pub last_save_error: Option<String>,
469}
470
471struct ManagerState {
472 snapshot: SnapshotDocument,
473 diagnostics: RestorationDiagnostics,
474 dirty: bool,
475 debounce_pending: bool,
476 save_in_flight: bool,
477 consecutive_save_failures: u32,
478 retry_pending: bool,
479 persist_migrated_snapshot: bool,
480 active_scope_paths: HashSet<String>,
481}
482
483struct RestorationManagerInner {
484 config: RestorationConfig,
485 state: std::cell::RefCell<ManagerState>,
486 scheduler: std::cell::RefCell<Option<Weak<std::cell::RefCell<tasks::TaskScheduler>>>>,
487}
488
489#[derive(Clone)]
492pub(crate) struct RestorationManager {
493 inner: Rc<RestorationManagerInner>,
494}
495
496#[derive(Clone)]
500pub struct RestorationHandle {
501 manager: RestorationManager,
502}
503
504impl RestorationHandle {
505 pub(crate) fn new(manager: RestorationManager) -> Self {
506 Self { manager }
507 }
508
509 pub fn reset(&self) {
510 self.manager.reset();
511 }
512
513 pub fn flush(&self) {
514 self.manager.flush();
515 }
516
517 pub fn remove_scope(&self, scope: &RestorationScope) -> usize {
521 self.manager.remove_scope(scope.path())
522 }
523
524 pub fn note_navigation_restore(&self, restored_routes: u64, invalid_routes: u64) {
526 self.manager
527 .note_navigation_restore(restored_routes, invalid_routes);
528 }
529
530 pub fn claim_scope(&self, scope: &RestorationScope) -> Result<ClaimedRestorationScope, String> {
536 self.manager
537 .acquire_scope(scope.path())
538 .map(|lease| ClaimedRestorationScope { _lease: lease })
539 }
540
541 #[must_use]
542 pub fn diagnostics(&self) -> RestorationDiagnostics {
543 self.manager.diagnostics()
544 }
545
546 #[must_use]
547 pub fn path(&self) -> Option<PathBuf> {
548 self.manager.store_location()
549 }
550}
551
552pub struct ClaimedRestorationScope {
556 _lease: ScopeLease,
557}
558
559impl RestorationManager {
560 pub(crate) fn load(config: RestorationConfig) -> Self {
561 let mut diagnostics = RestorationDiagnostics::default();
562 let mut snapshot = SnapshotDocument::empty(&config);
563 match config.store.load() {
564 Ok(Some(bytes)) => {
565 diagnostics.restoration_loads = 1;
566 if bytes.len() > config.snapshot_limit {
567 diagnostics.restoration_load_failures = 1;
571 } else {
572 diagnostics.snapshot_bytes = bytes.len();
573 diagnostics.peak_snapshot_bytes = bytes.len();
574 match decode_snapshot(&config, &bytes, &mut diagnostics) {
575 Ok(decoded) => snapshot = decoded,
576 Err(()) => diagnostics.restoration_load_failures += 1,
577 }
578 }
579 }
580 Ok(None) => diagnostics.restoration_loads = 1,
581 Err(_) => {
582 diagnostics.restoration_loads = 1;
583 diagnostics.restoration_load_failures = 1;
584 }
585 }
586 diagnostics.restoration_values = snapshot.state.len();
587 diagnostics.restoration_scopes = scope_count(&snapshot.state);
588 diagnostics.dirty_generation = snapshot.snapshot_generation;
589 diagnostics.persisted_generation = snapshot.snapshot_generation;
590 let migrated = diagnostics.restoration_migrations > 0;
591 Self {
592 inner: Rc::new(RestorationManagerInner {
593 config,
594 state: std::cell::RefCell::new(ManagerState {
595 snapshot,
596 diagnostics,
597 dirty: false,
598 debounce_pending: false,
599 save_in_flight: false,
600 consecutive_save_failures: 0,
601 retry_pending: false,
602 persist_migrated_snapshot: migrated,
603 active_scope_paths: HashSet::new(),
604 }),
605 scheduler: std::cell::RefCell::new(None),
606 }),
607 }
608 }
609
610 pub(crate) fn attach_scheduler(
611 &self,
612 scheduler: &Rc<std::cell::RefCell<tasks::TaskScheduler>>,
613 ) {
614 *self.inner.scheduler.borrow_mut() = Some(Rc::downgrade(scheduler));
615 if self.inner.state.borrow().persist_migrated_snapshot {
619 self.inner.state.borrow_mut().persist_migrated_snapshot = false;
620 self.mark_dirty();
621 }
622 }
623
624 pub(crate) fn read(&self, path: &[RestorationKey]) -> Option<Value> {
625 self.inner
626 .state
627 .borrow()
628 .snapshot
629 .state
630 .get(&encode_path(path))
631 .cloned()
632 }
633
634 pub(crate) fn write(&self, path: &[RestorationKey], value: Value) {
635 let encoded = encode_path(path);
636 let mut state = self.inner.state.borrow_mut();
637 if state.snapshot.state.get(&encoded) == Some(&value) {
638 return;
639 }
640 state.snapshot.state.insert(encoded, value);
641 refresh_counts(&mut state);
642 drop(state);
643 self.mark_dirty();
644 }
645
646 pub(crate) fn remove(&self, path: &[RestorationKey]) -> bool {
647 let mut state = self.inner.state.borrow_mut();
648 let removed = state.snapshot.state.remove(&encode_path(path)).is_some();
649 if removed {
650 refresh_counts(&mut state);
651 }
652 drop(state);
653 if removed {
654 self.mark_dirty();
655 }
656 removed
657 }
658
659 pub(crate) fn remove_scope(&self, path: &[RestorationKey]) -> usize {
660 let prefix = encode_path(path);
661 let prefix_with_separator = format!("{prefix}/");
662 let mut state = self.inner.state.borrow_mut();
663 let before = state.snapshot.state.len();
664 state
665 .snapshot
666 .state
667 .retain(|key, _| key != &prefix && !key.starts_with(&prefix_with_separator));
668 let removed = before - state.snapshot.state.len();
669 if removed > 0 {
670 refresh_counts(&mut state);
671 }
672 drop(state);
673 if removed > 0 {
674 self.mark_dirty();
675 }
676 removed
677 }
678
679 pub(crate) fn reset(&self) {
680 let mut state = self.inner.state.borrow_mut();
681 state.snapshot.state.clear();
682 state.snapshot.windows.clear();
683 refresh_counts(&mut state);
684 drop(state);
685 self.mark_dirty();
686 }
687
688 pub(crate) fn replace_windows(&self, windows: Vec<RestoredWindow>) {
689 let mut state = self.inner.state.borrow_mut();
690 if state.snapshot.windows == windows {
691 return;
692 }
693 state.snapshot.windows = windows;
694 drop(state);
695 self.mark_dirty();
696 }
697
698 #[must_use]
699 pub(crate) fn windows(&self) -> Vec<RestoredWindow> {
700 self.inner.state.borrow().snapshot.windows.clone()
701 }
702
703 #[must_use]
704 pub(crate) fn diagnostics(&self) -> RestorationDiagnostics {
705 self.inner.state.borrow().diagnostics.clone()
706 }
707
708 #[must_use]
709 pub(crate) fn store_location(&self) -> Option<PathBuf> {
710 self.inner.config.store.location()
711 }
712
713 pub(crate) fn note_restored_window(&self) {
714 self.inner.state.borrow_mut().diagnostics.restored_windows += 1;
715 }
716
717 pub(crate) fn note_skipped_window(&self) {
718 self.inner.state.borrow_mut().diagnostics.skipped_windows += 1;
719 }
720
721 pub(crate) fn note_navigation_restore(&self, restored_routes: u64, invalid_routes: u64) {
722 let mut state = self.inner.state.borrow_mut();
723 state.diagnostics.restored_routes += restored_routes;
724 state.diagnostics.invalid_routes += invalid_routes;
725 }
726
727 pub(crate) fn record_restoration_outcome(&self, restored: u64, invalid: u64) {
728 let mut state = self.inner.state.borrow_mut();
729 state.diagnostics.restored_scroll_positions += restored;
730 state.diagnostics.stale_or_invalid_values += invalid;
731 }
732
733 pub(crate) fn remove_window_descriptor(&self, restoration_id: &str) -> bool {
737 let mut state = self.inner.state.borrow_mut();
738 let before = state.snapshot.windows.len();
739 state
740 .snapshot
741 .windows
742 .retain(|window| window.restoration_id != restoration_id);
743 let removed = state.snapshot.windows.len() != before;
744 drop(state);
745 if removed {
746 self.mark_dirty();
747 }
748 removed
749 }
750
751 pub(crate) fn scope(&self) -> RestorationScope {
752 RestorationScope::root(Rc::new(self.clone()))
753 }
754
755 pub(crate) fn application_scope(&self) -> RestorationScope {
756 self.scope()
757 .child_unchecked(RestorationKey::new("application").expect("static key"))
758 }
759
760 pub(crate) fn acquire_scope(&self, path: &[RestorationKey]) -> Result<ScopeLease, String> {
764 let path = encode_path(path);
765 let mut state = self.inner.state.borrow_mut();
766 if !state.active_scope_paths.insert(path.clone()) {
767 return Err(format!("duplicate restoration sibling scope `{path}`"));
768 }
769 Ok(ScopeLease {
770 manager: Rc::downgrade(&self.inner),
771 path,
772 })
773 }
774
775 fn mark_dirty(&self) {
776 {
777 let mut state = self.inner.state.borrow_mut();
778 state.dirty = true;
779 state.diagnostics.dirty_generation = state.diagnostics.dirty_generation.wrapping_add(1);
780 state.snapshot.snapshot_generation = state.diagnostics.dirty_generation;
781 state.diagnostics.save_requests += 1;
782 state.retry_pending = false;
783 if state.save_in_flight || state.debounce_pending {
784 state.diagnostics.save_coalesced += 1;
785 return;
786 }
787 state.debounce_pending = true;
788 }
789 self.schedule_debounce();
790 }
791
792 fn schedule_debounce(&self) {
793 let Some(scheduler) = self
794 .inner
795 .scheduler
796 .borrow()
797 .as_ref()
798 .and_then(Weak::upgrade)
799 else {
800 return;
801 };
802 let manager = self.clone();
803 let debounce = self.inner.config.debounce;
804 tasks::TaskScheduler::spawner(&scheduler).spawn_into(
805 crate::executor::sleep(debounce),
806 move |result, _runtime| {
807 if result.is_ok() {
808 manager.start_save();
809 }
810 },
811 );
812 }
813
814 pub(crate) fn flush(&self) {
815 {
816 let mut state = self.inner.state.borrow_mut();
817 state.debounce_pending = false;
818 }
819 self.start_save();
820 }
821
822 #[must_use]
823 #[cfg(not(target_arch = "wasm32"))]
824 pub(crate) fn is_clean(&self) -> bool {
825 let state = self.inner.state.borrow();
826 !state.dirty && !state.save_in_flight && !state.debounce_pending
827 }
828
829 fn start_save(&self) {
830 let (snapshot, generation) = {
831 let mut state = self.inner.state.borrow_mut();
832 state.debounce_pending = false;
833 if !state.dirty || state.save_in_flight {
834 return;
835 }
836 state.save_in_flight = true;
837 state.diagnostics.saves_started += 1;
838 (state.snapshot.clone(), state.diagnostics.dirty_generation)
839 };
840 let Some(scheduler) = self
841 .inner
842 .scheduler
843 .borrow()
844 .as_ref()
845 .and_then(Weak::upgrade)
846 else {
847 self.finish_save(
848 generation,
849 Err(RestorationStoreError::new(
850 "runtime scheduler is unavailable",
851 )),
852 0,
853 );
854 return;
855 };
856 let store = self.inner.config.store.clone();
857 let limit = self.inner.config.snapshot_limit;
858 let manager = self.clone();
859 tasks::TaskScheduler::spawner(&scheduler).spawn_blocking(
860 move || {
861 let bytes = serde_json::to_vec(&snapshot)
862 .map_err(|error| RestorationStoreError::new(error.to_string()))?;
863 if bytes.len() > limit {
864 return Err(RestorationStoreError::new(format!(
865 "restoration snapshot is {} bytes, exceeding configured {limit}-byte limit",
866 bytes.len()
867 )));
868 }
869 store.save(&bytes)?;
870 Ok(bytes.len())
871 },
872 move |result, _runtime| match result {
873 Ok(Ok(bytes)) => manager.finish_save(generation, Ok(()), bytes),
874 Ok(Err(error)) => manager.finish_save(generation, Err(error), 0),
875 Err(_) => manager.finish_save(
876 generation,
877 Err(RestorationStoreError::new(
878 "restoration save task did not complete",
879 )),
880 0,
881 ),
882 },
883 );
884 }
885
886 fn finish_save(
887 &self,
888 generation: u64,
889 result: Result<(), RestorationStoreError>,
890 bytes: usize,
891 ) {
892 let needs_latest;
893 {
894 let mut state = self.inner.state.borrow_mut();
895 state.save_in_flight = false;
896 match result {
897 Ok(()) => {
898 state.diagnostics.saves_completed += 1;
899 state.diagnostics.last_save_error = None;
900 state.consecutive_save_failures = 0;
901 state.retry_pending = false;
902 state.diagnostics.snapshot_bytes = bytes;
903 state.diagnostics.peak_snapshot_bytes =
904 state.diagnostics.peak_snapshot_bytes.max(bytes);
905 state.diagnostics.persisted_generation = generation;
906 state.dirty = state.diagnostics.dirty_generation != generation;
907 needs_latest = state.dirty;
908 }
909 Err(error) => {
910 state.diagnostics.save_failures += 1;
911 state.diagnostics.last_save_error = Some(error.to_string());
912 state.dirty = true;
913 state.consecutive_save_failures += 1;
918 needs_latest = state.consecutive_save_failures <= MAX_RESTORATION_SAVE_RETRIES;
919 }
920 }
921 }
922 if needs_latest {
923 let retry = {
924 let mut state = self.inner.state.borrow_mut();
925 if !state.dirty || state.debounce_pending || state.save_in_flight {
926 return;
927 }
928 let failed = state.consecutive_save_failures > 0;
929 state.retry_pending = failed;
930 failed
931 };
932 if retry {
933 self.schedule_retry();
934 } else {
935 self.schedule_after_in_flight();
936 }
937 }
938 }
939
940 fn schedule_retry(&self) {
941 let attempt = self
942 .inner
943 .state
944 .borrow()
945 .consecutive_save_failures
946 .clamp(1, MAX_RESTORATION_SAVE_RETRIES);
947 let delay = RESTORATION_RETRY_BACKOFF * attempt;
948 let Some(scheduler) = self
949 .inner
950 .scheduler
951 .borrow()
952 .as_ref()
953 .and_then(Weak::upgrade)
954 else {
955 return;
956 };
957 let manager = self.clone();
958 tasks::TaskScheduler::spawner(&scheduler).spawn_into(
959 crate::executor::sleep(delay),
960 move |result, _runtime| {
961 if result.is_ok() {
962 let mut state = manager.inner.state.borrow_mut();
963 state.retry_pending = false;
964 if state.dirty && !state.save_in_flight && !state.debounce_pending {
965 drop(state);
966 manager.start_save();
967 }
968 }
969 },
970 );
971 }
972
973 fn schedule_after_in_flight(&self) {
974 let mut state = self.inner.state.borrow_mut();
975 if state.debounce_pending || state.save_in_flight {
976 return;
977 }
978 state.debounce_pending = true;
979 state.diagnostics.save_coalesced += 1;
980 drop(state);
981 self.schedule_debounce();
982 }
983
984 pub(crate) fn debug_dump(&self) -> String {
985 let state = self.inner.state.borrow();
986 let diagnostics = &state.diagnostics;
987 let mut lines = vec![format!(
988 "Restoration generation={} persisted={} bytes={}",
989 diagnostics.dirty_generation,
990 diagnostics.persisted_generation,
991 diagnostics.snapshot_bytes
992 )];
993 let mut scopes: BTreeMap<String, usize> = BTreeMap::new();
994 for key in state.snapshot.state.keys() {
995 let mut segments = key.split('/').collect::<Vec<_>>();
996 let _ = segments.pop();
997 let scope = if segments.is_empty() {
998 "application".to_owned()
999 } else {
1000 segments.join("/")
1001 };
1002 *scopes.entry(scope).or_default() += 1;
1003 }
1004 for (scope, values) in scopes {
1005 lines.push(format!(" {scope}/ ({values} values)"));
1006 }
1007 lines.join("\n")
1008 }
1009}
1010
1011pub(crate) struct ScopeLease {
1013 manager: Weak<RestorationManagerInner>,
1014 path: String,
1015}
1016
1017impl Drop for ScopeLease {
1018 fn drop(&mut self) {
1019 if let Some(manager) = self.manager.upgrade() {
1020 manager
1021 .state
1022 .borrow_mut()
1023 .active_scope_paths
1024 .remove(&self.path);
1025 }
1026 }
1027}
1028
1029fn validate_application_id(application_id: &str) -> Result<(), RestorationStoreError> {
1030 let rejected = application_id.trim().is_empty()
1031 || application_id.contains(['/', '\\', '\0'])
1032 || application_id.chars().any(char::is_control)
1033 || application_id
1034 .split(['/', '\\'])
1035 .any(|segment| segment == "." || segment == "..");
1036 if rejected {
1037 return Err(RestorationStoreError::new(
1038 "restoration application id must be a stable filesystem-safe identifier",
1039 ));
1040 }
1041 Ok(())
1042}
1043
1044fn decode_snapshot(
1045 config: &RestorationConfig,
1046 bytes: &[u8],
1047 diagnostics: &mut RestorationDiagnostics,
1048) -> Result<SnapshotDocument, ()> {
1049 let mut value: Value = serde_json::from_slice(bytes).map_err(|_| ())?;
1050 let document: SnapshotDocument = serde_json::from_value(value.clone()).map_err(|_| ())?;
1051 if document.framework_format_version != FRAMEWORK_RESTORATION_FORMAT_VERSION
1052 || document.application_id != config.application_id
1053 {
1054 return Err(());
1055 }
1056 if document.application_schema_version != config.application_schema_version {
1057 let Some(migration) = &config.migration else {
1058 return Err(());
1059 };
1060 migration(document.application_schema_version, &mut value).map_err(|_| ())?;
1061 let mut migrated: SnapshotDocument = serde_json::from_value(value).map_err(|_| ())?;
1062 migrated.framework_format_version = FRAMEWORK_RESTORATION_FORMAT_VERSION;
1063 migrated.application_schema_version = config.application_schema_version;
1064 migrated.application_id = config.application_id.clone();
1065 diagnostics.restoration_migrations += 1;
1066 Ok(migrated)
1067 } else {
1068 Ok(document)
1069 }
1070}
1071
1072fn encode_path(path: &[RestorationKey]) -> String {
1073 path.iter()
1074 .map(|segment| segment.as_str().replace('~', "~0").replace('/', "~1"))
1075 .collect::<Vec<_>>()
1076 .join("/")
1077}
1078
1079fn scope_count(values: &BTreeMap<String, Value>) -> usize {
1080 values
1081 .keys()
1082 .filter_map(|key| {
1083 key.rsplit_once('/')
1084 .map(|(scope, _)| scope)
1085 .or(Some("application"))
1086 })
1087 .collect::<BTreeSet<_>>()
1088 .len()
1089}
1090
1091fn refresh_counts(state: &mut ManagerState) {
1092 state.diagnostics.restoration_values = state.snapshot.state.len();
1093 state.diagnostics.restoration_scopes = scope_count(&state.snapshot.state);
1094}
1095
1096pub type RestorableInt = Restorable<i64>;
1098pub type RestorableIntN = Restorable<Option<i64>>;
1100pub type RestorableDouble = Restorable<f64>;
1102pub type RestorableDoubleN = Restorable<Option<f64>>;
1104pub type RestorableBool = Restorable<bool>;
1106pub type RestorableBoolN = Restorable<Option<bool>>;
1108pub type RestorableString = Restorable<String>;
1110pub type RestorableStringN = Restorable<Option<String>>;
1112
1113#[derive(Clone)]
1117pub struct Restorable<T> {
1118 signal: Signal<T>,
1119 write: Rc<dyn Fn(&T)>,
1120 remove: Rc<dyn Fn()>,
1121}
1122
1123impl<T> Restorable<T>
1124where
1125 T: Clone + PartialEq + Serialize + DeserializeOwned + 'static,
1126{
1127 pub(crate) fn new(
1128 initial: T,
1129 write: impl Fn(&T) + 'static,
1130 remove: impl Fn() + 'static,
1131 ) -> Self {
1132 Self {
1133 signal: Signal::new(initial),
1134 write: Rc::new(write),
1135 remove: Rc::new(remove),
1136 }
1137 }
1138
1139 #[must_use]
1144 pub fn from_scope(scope: RestorationScope, key: RestorationKey, default: T) -> Self {
1145 let stored = scope.get_json(&key);
1146 let initial = match stored {
1147 None => default,
1148 Some(value) => match serde_json::from_value(value) {
1149 Ok(initial) => initial,
1150 Err(_) => {
1151 scope.note_restoration_outcome(0, 1);
1152 default
1153 }
1154 },
1155 };
1156 let write_scope = scope.clone();
1157 let write_key = key.clone();
1158 let remove_scope = scope;
1159 Self::new(
1160 initial,
1161 move |value| {
1162 if let Ok(value) = serde_json::to_value(value) {
1163 write_scope.set_json(&write_key, value);
1164 }
1165 },
1166 move || remove_scope.remove(&key),
1167 )
1168 }
1169
1170 #[must_use]
1171 pub fn get(&self) -> T {
1172 self.signal.get()
1173 }
1174
1175 pub fn set(&self, value: T) -> bool {
1176 if !self.signal.set(value.clone()) {
1177 return false;
1178 }
1179 (self.write)(&value);
1180 true
1181 }
1182
1183 pub fn update(&self, update: impl FnOnce(&mut T)) {
1184 let mut value = self.signal.get();
1185 update(&mut value);
1186 let _ = self.set(value);
1187 }
1188
1189 pub fn remove(&self) {
1191 (self.remove)();
1192 }
1193
1194 #[must_use]
1197 pub fn signal(&self) -> Signal<T> {
1198 self.signal.clone()
1199 }
1200}
1201
1202impl RestorationBackend for RestorationManager {
1203 fn read_value(&self, path: &[RestorationKey]) -> Option<Value> {
1204 self.read(path)
1205 }
1206
1207 fn write_value(&self, path: &[RestorationKey], value: Value) {
1208 self.write(path, value);
1209 }
1210
1211 fn remove_value(&self, path: &[RestorationKey]) {
1212 let _ = self.remove(path);
1213 }
1214
1215 fn note_restoration_outcome(&self, restored: u64, invalid: u64) {
1216 self.record_restoration_outcome(restored, invalid);
1217 }
1218}