use std::{
collections::{BTreeMap, BTreeSet, HashSet},
fmt,
fs::{self, File},
io::Read,
path::{Path, PathBuf},
rc::{Rc, Weak},
sync::{Arc, Mutex},
time::Duration,
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::Value;
use incular_core::{RestorationBackend, RestorationKey, RestorationScope};
use crate::{Signal, tasks};
pub const FRAMEWORK_RESTORATION_FORMAT_VERSION: u32 = 1;
pub const DEFAULT_RESTORATION_DEBOUNCE: Duration = Duration::from_millis(250);
pub const DEFAULT_RESTORATION_SNAPSHOT_LIMIT: usize = 8 * 1024 * 1024;
pub const MAX_RESTORATION_SAVE_RETRIES: u32 = 3;
pub const RESTORATION_RETRY_BACKOFF: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct RestorationStoreError {
message: String,
}
impl RestorationStoreError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
pub trait RestorationStore: Send + Sync + 'static {
fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError>;
fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError>;
fn remove(&self) -> Result<(), RestorationStoreError>;
fn location(&self) -> Option<PathBuf> {
None
}
}
#[derive(Default)]
pub struct InMemoryRestorationStore {
bytes: Mutex<Option<Vec<u8>>>,
fail_next_save: Mutex<Option<String>>,
}
impl InMemoryRestorationStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn fail_next_save(&self, message: impl Into<String>) {
*self
.fail_next_save
.lock()
.expect("restoration test-store failure mutex") = Some(message.into());
}
#[must_use]
pub fn bytes(&self) -> Option<Vec<u8>> {
self.bytes
.lock()
.expect("restoration test-store bytes mutex")
.clone()
}
pub fn set_bytes(&self, bytes: Option<Vec<u8>>) {
*self
.bytes
.lock()
.expect("restoration test-store bytes mutex") = bytes;
}
}
impl RestorationStore for InMemoryRestorationStore {
fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError> {
Ok(self.bytes())
}
fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError> {
if let Some(message) = self
.fail_next_save
.lock()
.expect("restoration test-store failure mutex")
.take()
{
return Err(RestorationStoreError::new(message));
}
*self
.bytes
.lock()
.expect("restoration test-store bytes mutex") = Some(bytes.to_vec());
Ok(())
}
fn remove(&self) -> Result<(), RestorationStoreError> {
*self
.bytes
.lock()
.expect("restoration test-store bytes mutex") = None;
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct FileRestorationStore {
path: PathBuf,
}
impl FileRestorationStore {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn for_application(application_id: &str) -> Result<Self, RestorationStoreError> {
validate_application_id(application_id)?;
let base = incular_platform::application_data_local_directory(application_id).ok_or_else(
|| {
RestorationStoreError::new(
"standard application data directory is unavailable for restoration",
)
},
)?;
Ok(Self::new(base.join("restoration.json")))
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl RestorationStore for FileRestorationStore {
fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError> {
match File::open(&self.path) {
Ok(mut file) => {
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|error| RestorationStoreError::new(error.to_string()))?;
Ok(Some(bytes))
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(RestorationStoreError::new(error.to_string())),
}
}
#[cfg(target_arch = "wasm32")]
fn save(&self, _bytes: &[u8]) -> Result<(), RestorationStoreError> {
Err(RestorationStoreError::new(
"file restoration snapshots are unavailable in browsers",
))
}
#[cfg(not(target_arch = "wasm32"))]
fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError> {
let parent = self.path.parent().ok_or_else(|| {
RestorationStoreError::new("restoration snapshot path has no parent directory")
})?;
fs::create_dir_all(parent)
.map_err(|error| RestorationStoreError::new(error.to_string()))?;
use std::io::Write as _;
let mut temporary = tempfile::NamedTempFile::new_in(parent)
.map_err(|error| RestorationStoreError::new(error.to_string()))?;
temporary
.write_all(bytes)
.and_then(|()| temporary.flush())
.and_then(|()| temporary.as_file().sync_all())
.map_err(|error| RestorationStoreError::new(error.to_string()))?;
temporary
.persist(&self.path)
.map_err(|error| RestorationStoreError::new(error.error.to_string()))?;
Ok(())
}
fn remove(&self) -> Result<(), RestorationStoreError> {
match fs::remove_file(&self.path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(RestorationStoreError::new(error.to_string())),
}
}
fn location(&self) -> Option<PathBuf> {
Some(self.path.clone())
}
}
#[cfg(target_arch = "wasm32")]
#[derive(Clone, Debug)]
pub struct BrowserRestorationStore {
key: String,
}
#[cfg(target_arch = "wasm32")]
impl BrowserRestorationStore {
pub fn for_application(application_id: &str) -> Result<Self, RestorationStoreError> {
validate_application_id(application_id)?;
Ok(Self {
key: format!("incular.restoration.{application_id}"),
})
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
fn storage() -> Result<web_sys::Storage, RestorationStoreError> {
web_sys::window()
.and_then(|window| window.local_storage().ok().flatten())
.ok_or_else(|| RestorationStoreError::new("localStorage is unavailable"))
}
}
#[cfg(target_arch = "wasm32")]
fn storage_error(error: wasm_bindgen::JsValue) -> RestorationStoreError {
RestorationStoreError::new(format!("localStorage: {error:?}"))
}
#[cfg(target_arch = "wasm32")]
impl RestorationStore for BrowserRestorationStore {
fn load(&self) -> Result<Option<Vec<u8>>, RestorationStoreError> {
Ok(Self::storage()?
.get_item(&self.key)
.map_err(storage_error)?
.map(String::into_bytes))
}
fn save(&self, bytes: &[u8]) -> Result<(), RestorationStoreError> {
let text = std::str::from_utf8(bytes)
.map_err(|_| RestorationStoreError::new("restoration snapshot is not UTF-8"))?;
Self::storage()?
.set_item(&self.key, text)
.map_err(storage_error)
}
fn remove(&self) -> Result<(), RestorationStoreError> {
Self::storage()?
.remove_item(&self.key)
.map_err(storage_error)
}
fn location(&self) -> Option<PathBuf> {
Some(PathBuf::from(format!("localStorage:{}", self.key)))
}
}
#[derive(Clone)]
pub struct RestorationConfig {
application_id: String,
application_schema_version: u32,
store: Arc<dyn RestorationStore>,
snapshot_limit: usize,
debounce: Duration,
migration: Option<RestorationMigration>,
}
impl fmt::Debug for RestorationConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("RestorationConfig")
.field("application_id", &self.application_id)
.field(
"application_schema_version",
&self.application_schema_version,
)
.field("snapshot_limit", &self.snapshot_limit)
.field("debounce", &self.debounce)
.field("store_location", &self.store.location())
.finish_non_exhaustive()
}
}
pub type RestorationMigration = Arc<dyn Fn(u32, &mut Value) -> Result<(), String> + 'static>;
impl RestorationConfig {
#[must_use]
pub fn new(
application_id: impl Into<String>,
application_schema_version: u32,
store: Arc<dyn RestorationStore>,
) -> Self {
Self {
application_id: application_id.into(),
application_schema_version,
store,
snapshot_limit: DEFAULT_RESTORATION_SNAPSHOT_LIMIT,
debounce: DEFAULT_RESTORATION_DEBOUNCE,
migration: None,
}
}
pub fn file_backed(
application_id: impl Into<String>,
application_schema_version: u32,
) -> Result<Self, RestorationStoreError> {
let application_id = application_id.into();
#[cfg(not(target_arch = "wasm32"))]
let store = Arc::new(FileRestorationStore::for_application(&application_id)?);
#[cfg(target_arch = "wasm32")]
let store = Arc::new(BrowserRestorationStore::for_application(&application_id)?);
Ok(Self::new(application_id, application_schema_version, store))
}
#[must_use]
pub fn with_snapshot_limit(mut self, bytes: usize) -> Self {
self.snapshot_limit = bytes.max(1);
self
}
#[must_use]
pub fn with_debounce(mut self, debounce: Duration) -> Self {
self.debounce = debounce;
self
}
#[must_use]
pub fn with_migration(mut self, migration: RestorationMigration) -> Self {
self.migration = Some(migration);
self
}
#[must_use]
pub fn application_id(&self) -> &str {
&self.application_id
}
#[must_use]
pub const fn application_schema_version(&self) -> u32 {
self.application_schema_version
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RestoredWindow {
pub restoration_id: String,
pub kind: String,
pub logical_width: f32,
pub logical_height: f32,
pub maximized: bool,
pub fullscreen: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct SnapshotDocument {
framework_format_version: u32,
application_schema_version: u32,
snapshot_generation: u64,
application_id: String,
#[serde(default)]
state: BTreeMap<String, Value>,
#[serde(default)]
windows: Vec<RestoredWindow>,
}
impl SnapshotDocument {
fn empty(config: &RestorationConfig) -> Self {
Self {
framework_format_version: FRAMEWORK_RESTORATION_FORMAT_VERSION,
application_schema_version: config.application_schema_version,
snapshot_generation: 0,
application_id: config.application_id.clone(),
state: BTreeMap::new(),
windows: Vec::new(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RestorationDiagnostics {
pub restoration_loads: u64,
pub restoration_load_failures: u64,
pub restoration_migrations: u64,
pub restoration_values: usize,
pub restoration_scopes: usize,
pub dirty_generation: u64,
pub persisted_generation: u64,
pub save_requests: u64,
pub save_coalesced: u64,
pub saves_started: u64,
pub saves_completed: u64,
pub save_failures: u64,
pub snapshot_bytes: usize,
pub peak_snapshot_bytes: usize,
pub restored_windows: u64,
pub skipped_windows: u64,
pub restored_routes: u64,
pub invalid_routes: u64,
pub restored_scroll_positions: u64,
pub stale_or_invalid_values: u64,
pub last_save_error: Option<String>,
}
struct ManagerState {
snapshot: SnapshotDocument,
diagnostics: RestorationDiagnostics,
dirty: bool,
debounce_pending: bool,
save_in_flight: bool,
consecutive_save_failures: u32,
retry_pending: bool,
persist_migrated_snapshot: bool,
active_scope_paths: HashSet<String>,
}
struct RestorationManagerInner {
config: RestorationConfig,
state: std::cell::RefCell<ManagerState>,
scheduler: std::cell::RefCell<Option<Weak<std::cell::RefCell<tasks::TaskScheduler>>>>,
}
#[derive(Clone)]
pub(crate) struct RestorationManager {
inner: Rc<RestorationManagerInner>,
}
#[derive(Clone)]
pub struct RestorationHandle {
manager: RestorationManager,
}
impl RestorationHandle {
pub(crate) fn new(manager: RestorationManager) -> Self {
Self { manager }
}
pub fn reset(&self) {
self.manager.reset();
}
pub fn flush(&self) {
self.manager.flush();
}
pub fn remove_scope(&self, scope: &RestorationScope) -> usize {
self.manager.remove_scope(scope.path())
}
pub fn note_navigation_restore(&self, restored_routes: u64, invalid_routes: u64) {
self.manager
.note_navigation_restore(restored_routes, invalid_routes);
}
pub fn claim_scope(&self, scope: &RestorationScope) -> Result<ClaimedRestorationScope, String> {
self.manager
.acquire_scope(scope.path())
.map(|lease| ClaimedRestorationScope { _lease: lease })
}
#[must_use]
pub fn diagnostics(&self) -> RestorationDiagnostics {
self.manager.diagnostics()
}
#[must_use]
pub fn path(&self) -> Option<PathBuf> {
self.manager.store_location()
}
}
pub struct ClaimedRestorationScope {
_lease: ScopeLease,
}
impl RestorationManager {
pub(crate) fn load(config: RestorationConfig) -> Self {
let mut diagnostics = RestorationDiagnostics::default();
let mut snapshot = SnapshotDocument::empty(&config);
match config.store.load() {
Ok(Some(bytes)) => {
diagnostics.restoration_loads = 1;
if bytes.len() > config.snapshot_limit {
diagnostics.restoration_load_failures = 1;
} else {
diagnostics.snapshot_bytes = bytes.len();
diagnostics.peak_snapshot_bytes = bytes.len();
match decode_snapshot(&config, &bytes, &mut diagnostics) {
Ok(decoded) => snapshot = decoded,
Err(()) => diagnostics.restoration_load_failures += 1,
}
}
}
Ok(None) => diagnostics.restoration_loads = 1,
Err(_) => {
diagnostics.restoration_loads = 1;
diagnostics.restoration_load_failures = 1;
}
}
diagnostics.restoration_values = snapshot.state.len();
diagnostics.restoration_scopes = scope_count(&snapshot.state);
diagnostics.dirty_generation = snapshot.snapshot_generation;
diagnostics.persisted_generation = snapshot.snapshot_generation;
let migrated = diagnostics.restoration_migrations > 0;
Self {
inner: Rc::new(RestorationManagerInner {
config,
state: std::cell::RefCell::new(ManagerState {
snapshot,
diagnostics,
dirty: false,
debounce_pending: false,
save_in_flight: false,
consecutive_save_failures: 0,
retry_pending: false,
persist_migrated_snapshot: migrated,
active_scope_paths: HashSet::new(),
}),
scheduler: std::cell::RefCell::new(None),
}),
}
}
pub(crate) fn attach_scheduler(
&self,
scheduler: &Rc<std::cell::RefCell<tasks::TaskScheduler>>,
) {
*self.inner.scheduler.borrow_mut() = Some(Rc::downgrade(scheduler));
if self.inner.state.borrow().persist_migrated_snapshot {
self.inner.state.borrow_mut().persist_migrated_snapshot = false;
self.mark_dirty();
}
}
pub(crate) fn read(&self, path: &[RestorationKey]) -> Option<Value> {
self.inner
.state
.borrow()
.snapshot
.state
.get(&encode_path(path))
.cloned()
}
pub(crate) fn write(&self, path: &[RestorationKey], value: Value) {
let encoded = encode_path(path);
let mut state = self.inner.state.borrow_mut();
if state.snapshot.state.get(&encoded) == Some(&value) {
return;
}
state.snapshot.state.insert(encoded, value);
refresh_counts(&mut state);
drop(state);
self.mark_dirty();
}
pub(crate) fn remove(&self, path: &[RestorationKey]) -> bool {
let mut state = self.inner.state.borrow_mut();
let removed = state.snapshot.state.remove(&encode_path(path)).is_some();
if removed {
refresh_counts(&mut state);
}
drop(state);
if removed {
self.mark_dirty();
}
removed
}
pub(crate) fn remove_scope(&self, path: &[RestorationKey]) -> usize {
let prefix = encode_path(path);
let prefix_with_separator = format!("{prefix}/");
let mut state = self.inner.state.borrow_mut();
let before = state.snapshot.state.len();
state
.snapshot
.state
.retain(|key, _| key != &prefix && !key.starts_with(&prefix_with_separator));
let removed = before - state.snapshot.state.len();
if removed > 0 {
refresh_counts(&mut state);
}
drop(state);
if removed > 0 {
self.mark_dirty();
}
removed
}
pub(crate) fn reset(&self) {
let mut state = self.inner.state.borrow_mut();
state.snapshot.state.clear();
state.snapshot.windows.clear();
refresh_counts(&mut state);
drop(state);
self.mark_dirty();
}
pub(crate) fn replace_windows(&self, windows: Vec<RestoredWindow>) {
let mut state = self.inner.state.borrow_mut();
if state.snapshot.windows == windows {
return;
}
state.snapshot.windows = windows;
drop(state);
self.mark_dirty();
}
#[must_use]
pub(crate) fn windows(&self) -> Vec<RestoredWindow> {
self.inner.state.borrow().snapshot.windows.clone()
}
#[must_use]
pub(crate) fn diagnostics(&self) -> RestorationDiagnostics {
self.inner.state.borrow().diagnostics.clone()
}
#[must_use]
pub(crate) fn store_location(&self) -> Option<PathBuf> {
self.inner.config.store.location()
}
pub(crate) fn note_restored_window(&self) {
self.inner.state.borrow_mut().diagnostics.restored_windows += 1;
}
pub(crate) fn note_skipped_window(&self) {
self.inner.state.borrow_mut().diagnostics.skipped_windows += 1;
}
pub(crate) fn note_navigation_restore(&self, restored_routes: u64, invalid_routes: u64) {
let mut state = self.inner.state.borrow_mut();
state.diagnostics.restored_routes += restored_routes;
state.diagnostics.invalid_routes += invalid_routes;
}
pub(crate) fn record_restoration_outcome(&self, restored: u64, invalid: u64) {
let mut state = self.inner.state.borrow_mut();
state.diagnostics.restored_scroll_positions += restored;
state.diagnostics.stale_or_invalid_values += invalid;
}
pub(crate) fn remove_window_descriptor(&self, restoration_id: &str) -> bool {
let mut state = self.inner.state.borrow_mut();
let before = state.snapshot.windows.len();
state
.snapshot
.windows
.retain(|window| window.restoration_id != restoration_id);
let removed = state.snapshot.windows.len() != before;
drop(state);
if removed {
self.mark_dirty();
}
removed
}
pub(crate) fn scope(&self) -> RestorationScope {
RestorationScope::root(Rc::new(self.clone()))
}
pub(crate) fn application_scope(&self) -> RestorationScope {
self.scope()
.child_unchecked(RestorationKey::new("application").expect("static key"))
}
pub(crate) fn acquire_scope(&self, path: &[RestorationKey]) -> Result<ScopeLease, String> {
let path = encode_path(path);
let mut state = self.inner.state.borrow_mut();
if !state.active_scope_paths.insert(path.clone()) {
return Err(format!("duplicate restoration sibling scope `{path}`"));
}
Ok(ScopeLease {
manager: Rc::downgrade(&self.inner),
path,
})
}
fn mark_dirty(&self) {
{
let mut state = self.inner.state.borrow_mut();
state.dirty = true;
state.diagnostics.dirty_generation = state.diagnostics.dirty_generation.wrapping_add(1);
state.snapshot.snapshot_generation = state.diagnostics.dirty_generation;
state.diagnostics.save_requests += 1;
state.retry_pending = false;
if state.save_in_flight || state.debounce_pending {
state.diagnostics.save_coalesced += 1;
return;
}
state.debounce_pending = true;
}
self.schedule_debounce();
}
fn schedule_debounce(&self) {
let Some(scheduler) = self
.inner
.scheduler
.borrow()
.as_ref()
.and_then(Weak::upgrade)
else {
return;
};
let manager = self.clone();
let debounce = self.inner.config.debounce;
tasks::TaskScheduler::spawner(&scheduler).spawn_into(
crate::executor::sleep(debounce),
move |result, _runtime| {
if result.is_ok() {
manager.start_save();
}
},
);
}
pub(crate) fn flush(&self) {
{
let mut state = self.inner.state.borrow_mut();
state.debounce_pending = false;
}
self.start_save();
}
#[must_use]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn is_clean(&self) -> bool {
let state = self.inner.state.borrow();
!state.dirty && !state.save_in_flight && !state.debounce_pending
}
fn start_save(&self) {
let (snapshot, generation) = {
let mut state = self.inner.state.borrow_mut();
state.debounce_pending = false;
if !state.dirty || state.save_in_flight {
return;
}
state.save_in_flight = true;
state.diagnostics.saves_started += 1;
(state.snapshot.clone(), state.diagnostics.dirty_generation)
};
let Some(scheduler) = self
.inner
.scheduler
.borrow()
.as_ref()
.and_then(Weak::upgrade)
else {
self.finish_save(
generation,
Err(RestorationStoreError::new(
"runtime scheduler is unavailable",
)),
0,
);
return;
};
let store = self.inner.config.store.clone();
let limit = self.inner.config.snapshot_limit;
let manager = self.clone();
tasks::TaskScheduler::spawner(&scheduler).spawn_blocking(
move || {
let bytes = serde_json::to_vec(&snapshot)
.map_err(|error| RestorationStoreError::new(error.to_string()))?;
if bytes.len() > limit {
return Err(RestorationStoreError::new(format!(
"restoration snapshot is {} bytes, exceeding configured {limit}-byte limit",
bytes.len()
)));
}
store.save(&bytes)?;
Ok(bytes.len())
},
move |result, _runtime| match result {
Ok(Ok(bytes)) => manager.finish_save(generation, Ok(()), bytes),
Ok(Err(error)) => manager.finish_save(generation, Err(error), 0),
Err(_) => manager.finish_save(
generation,
Err(RestorationStoreError::new(
"restoration save task did not complete",
)),
0,
),
},
);
}
fn finish_save(
&self,
generation: u64,
result: Result<(), RestorationStoreError>,
bytes: usize,
) {
let needs_latest;
{
let mut state = self.inner.state.borrow_mut();
state.save_in_flight = false;
match result {
Ok(()) => {
state.diagnostics.saves_completed += 1;
state.diagnostics.last_save_error = None;
state.consecutive_save_failures = 0;
state.retry_pending = false;
state.diagnostics.snapshot_bytes = bytes;
state.diagnostics.peak_snapshot_bytes =
state.diagnostics.peak_snapshot_bytes.max(bytes);
state.diagnostics.persisted_generation = generation;
state.dirty = state.diagnostics.dirty_generation != generation;
needs_latest = state.dirty;
}
Err(error) => {
state.diagnostics.save_failures += 1;
state.diagnostics.last_save_error = Some(error.to_string());
state.dirty = true;
state.consecutive_save_failures += 1;
needs_latest = state.consecutive_save_failures <= MAX_RESTORATION_SAVE_RETRIES;
}
}
}
if needs_latest {
let retry = {
let mut state = self.inner.state.borrow_mut();
if !state.dirty || state.debounce_pending || state.save_in_flight {
return;
}
let failed = state.consecutive_save_failures > 0;
state.retry_pending = failed;
failed
};
if retry {
self.schedule_retry();
} else {
self.schedule_after_in_flight();
}
}
}
fn schedule_retry(&self) {
let attempt = self
.inner
.state
.borrow()
.consecutive_save_failures
.clamp(1, MAX_RESTORATION_SAVE_RETRIES);
let delay = RESTORATION_RETRY_BACKOFF * attempt;
let Some(scheduler) = self
.inner
.scheduler
.borrow()
.as_ref()
.and_then(Weak::upgrade)
else {
return;
};
let manager = self.clone();
tasks::TaskScheduler::spawner(&scheduler).spawn_into(
crate::executor::sleep(delay),
move |result, _runtime| {
if result.is_ok() {
let mut state = manager.inner.state.borrow_mut();
state.retry_pending = false;
if state.dirty && !state.save_in_flight && !state.debounce_pending {
drop(state);
manager.start_save();
}
}
},
);
}
fn schedule_after_in_flight(&self) {
let mut state = self.inner.state.borrow_mut();
if state.debounce_pending || state.save_in_flight {
return;
}
state.debounce_pending = true;
state.diagnostics.save_coalesced += 1;
drop(state);
self.schedule_debounce();
}
pub(crate) fn debug_dump(&self) -> String {
let state = self.inner.state.borrow();
let diagnostics = &state.diagnostics;
let mut lines = vec![format!(
"Restoration generation={} persisted={} bytes={}",
diagnostics.dirty_generation,
diagnostics.persisted_generation,
diagnostics.snapshot_bytes
)];
let mut scopes: BTreeMap<String, usize> = BTreeMap::new();
for key in state.snapshot.state.keys() {
let mut segments = key.split('/').collect::<Vec<_>>();
let _ = segments.pop();
let scope = if segments.is_empty() {
"application".to_owned()
} else {
segments.join("/")
};
*scopes.entry(scope).or_default() += 1;
}
for (scope, values) in scopes {
lines.push(format!(" {scope}/ ({values} values)"));
}
lines.join("\n")
}
}
pub(crate) struct ScopeLease {
manager: Weak<RestorationManagerInner>,
path: String,
}
impl Drop for ScopeLease {
fn drop(&mut self) {
if let Some(manager) = self.manager.upgrade() {
manager
.state
.borrow_mut()
.active_scope_paths
.remove(&self.path);
}
}
}
fn validate_application_id(application_id: &str) -> Result<(), RestorationStoreError> {
let rejected = application_id.trim().is_empty()
|| application_id.contains(['/', '\\', '\0'])
|| application_id.chars().any(char::is_control)
|| application_id
.split(['/', '\\'])
.any(|segment| segment == "." || segment == "..");
if rejected {
return Err(RestorationStoreError::new(
"restoration application id must be a stable filesystem-safe identifier",
));
}
Ok(())
}
fn decode_snapshot(
config: &RestorationConfig,
bytes: &[u8],
diagnostics: &mut RestorationDiagnostics,
) -> Result<SnapshotDocument, ()> {
let mut value: Value = serde_json::from_slice(bytes).map_err(|_| ())?;
let document: SnapshotDocument = serde_json::from_value(value.clone()).map_err(|_| ())?;
if document.framework_format_version != FRAMEWORK_RESTORATION_FORMAT_VERSION
|| document.application_id != config.application_id
{
return Err(());
}
if document.application_schema_version != config.application_schema_version {
let Some(migration) = &config.migration else {
return Err(());
};
migration(document.application_schema_version, &mut value).map_err(|_| ())?;
let mut migrated: SnapshotDocument = serde_json::from_value(value).map_err(|_| ())?;
migrated.framework_format_version = FRAMEWORK_RESTORATION_FORMAT_VERSION;
migrated.application_schema_version = config.application_schema_version;
migrated.application_id = config.application_id.clone();
diagnostics.restoration_migrations += 1;
Ok(migrated)
} else {
Ok(document)
}
}
fn encode_path(path: &[RestorationKey]) -> String {
path.iter()
.map(|segment| segment.as_str().replace('~', "~0").replace('/', "~1"))
.collect::<Vec<_>>()
.join("/")
}
fn scope_count(values: &BTreeMap<String, Value>) -> usize {
values
.keys()
.filter_map(|key| {
key.rsplit_once('/')
.map(|(scope, _)| scope)
.or(Some("application"))
})
.collect::<BTreeSet<_>>()
.len()
}
fn refresh_counts(state: &mut ManagerState) {
state.diagnostics.restoration_values = state.snapshot.state.len();
state.diagnostics.restoration_scopes = scope_count(&state.snapshot.state);
}
pub type RestorableInt = Restorable<i64>;
pub type RestorableIntN = Restorable<Option<i64>>;
pub type RestorableDouble = Restorable<f64>;
pub type RestorableDoubleN = Restorable<Option<f64>>;
pub type RestorableBool = Restorable<bool>;
pub type RestorableBoolN = Restorable<Option<bool>>;
pub type RestorableString = Restorable<String>;
pub type RestorableStringN = Restorable<Option<String>>;
#[derive(Clone)]
pub struct Restorable<T> {
signal: Signal<T>,
write: Rc<dyn Fn(&T)>,
remove: Rc<dyn Fn()>,
}
impl<T> Restorable<T>
where
T: Clone + PartialEq + Serialize + DeserializeOwned + 'static,
{
pub(crate) fn new(
initial: T,
write: impl Fn(&T) + 'static,
remove: impl Fn() + 'static,
) -> Self {
Self {
signal: Signal::new(initial),
write: Rc::new(write),
remove: Rc::new(remove),
}
}
#[must_use]
pub fn from_scope(scope: RestorationScope, key: RestorationKey, default: T) -> Self {
let stored = scope.get_json(&key);
let initial = match stored {
None => default,
Some(value) => match serde_json::from_value(value) {
Ok(initial) => initial,
Err(_) => {
scope.note_restoration_outcome(0, 1);
default
}
},
};
let write_scope = scope.clone();
let write_key = key.clone();
let remove_scope = scope;
Self::new(
initial,
move |value| {
if let Ok(value) = serde_json::to_value(value) {
write_scope.set_json(&write_key, value);
}
},
move || remove_scope.remove(&key),
)
}
#[must_use]
pub fn get(&self) -> T {
self.signal.get()
}
pub fn set(&self, value: T) -> bool {
if !self.signal.set(value.clone()) {
return false;
}
(self.write)(&value);
true
}
pub fn update(&self, update: impl FnOnce(&mut T)) {
let mut value = self.signal.get();
update(&mut value);
let _ = self.set(value);
}
pub fn remove(&self) {
(self.remove)();
}
#[must_use]
pub fn signal(&self) -> Signal<T> {
self.signal.clone()
}
}
impl RestorationBackend for RestorationManager {
fn read_value(&self, path: &[RestorationKey]) -> Option<Value> {
self.read(path)
}
fn write_value(&self, path: &[RestorationKey], value: Value) {
self.write(path, value);
}
fn remove_value(&self, path: &[RestorationKey]) {
let _ = self.remove(path);
}
fn note_restoration_outcome(&self, restored: u64, invalid: u64) {
self.record_restoration_outcome(restored, invalid);
}
}