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