feat(core): persist and restore browser state across launches
This commit is contained in:
@@ -27,6 +27,9 @@ pub enum CoreError {
|
||||
#[error("invalid .elydata package: {reason}")]
|
||||
InvalidLocalDataPackage { reason: String },
|
||||
|
||||
#[error("local state persistence failed: {reason}")]
|
||||
LocalState { reason: String },
|
||||
|
||||
#[error("trashed space not found: {id}")]
|
||||
TrashedSpaceNotFound { id: SpaceId },
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod error;
|
||||
mod local_state;
|
||||
mod navigation;
|
||||
mod state;
|
||||
mod sync_engine;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//! Versioned on-disk browser state. The wire shape reuses the sync
|
||||
//! snapshot records and appliers — one canonical serialization with two
|
||||
//! consumers — but visibility is local: cloud sync policies never
|
||||
//! reduce what survives a restart, and Private-profile data never
|
||||
//! reaches disk.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
CoreError,
|
||||
state::BrowserCore,
|
||||
sync_records::{
|
||||
BookmarkSyncRecord, HistorySyncRecord, NoteSyncRecord, PluginSettingsSyncRecord,
|
||||
ProfileSyncRecord, ReadingListSyncRecord, SNAPSHOT_SCHEMA_REV, SitePermissionSyncRecord,
|
||||
SpaceSyncRecord, SyncSnapshotBody, TabSyncRecord,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) const LOCAL_STATE_REV: u32 = 1;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct LocalStateDocument {
|
||||
local_rev: u32,
|
||||
body: SyncSnapshotBody,
|
||||
}
|
||||
|
||||
impl BrowserCore {
|
||||
pub fn build_local_state_bytes(&self) -> Result<Vec<u8>, CoreError> {
|
||||
let document =
|
||||
LocalStateDocument { local_rev: LOCAL_STATE_REV, body: local_body_from_core(self) };
|
||||
serde_json::to_vec(&document)
|
||||
.map_err(|error| CoreError::LocalState { reason: error.to_string() })
|
||||
}
|
||||
|
||||
pub fn apply_local_state_bytes(&mut self, bytes: &[u8]) -> Result<(), CoreError> {
|
||||
let document: LocalStateDocument = serde_json::from_slice(bytes)
|
||||
.map_err(|error| CoreError::LocalState { reason: error.to_string() })?;
|
||||
if document.local_rev != LOCAL_STATE_REV {
|
||||
return Err(CoreError::LocalState {
|
||||
reason: format!("unsupported local_rev {}", document.local_rev),
|
||||
});
|
||||
}
|
||||
if document.body.schema_rev != SNAPSHOT_SCHEMA_REV {
|
||||
return Err(CoreError::LocalState {
|
||||
reason: format!("unsupported schema_rev {}", document.body.schema_rev),
|
||||
});
|
||||
}
|
||||
self.apply_sync_snapshot_body(document.body)
|
||||
.map_err(|error| CoreError::LocalState { reason: error.to_string() })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn local_body_from_core(core: &BrowserCore) -> SyncSnapshotBody {
|
||||
SyncSnapshotBody {
|
||||
schema_rev: SNAPSHOT_SCHEMA_REV,
|
||||
profiles: core
|
||||
.visible_profiles_for_local()
|
||||
.into_iter()
|
||||
.map(ProfileSyncRecord::from_profile)
|
||||
.collect(),
|
||||
spaces: core
|
||||
.visible_spaces_for_local()
|
||||
.into_iter()
|
||||
.map(SpaceSyncRecord::from_space)
|
||||
.collect(),
|
||||
bookmarks: core
|
||||
.visible_bookmarks_for_local()
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
BookmarkSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
||||
})
|
||||
.collect(),
|
||||
tabs: core
|
||||
.visible_tabs_for_local()
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
TabSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
||||
})
|
||||
.collect(),
|
||||
notes: core
|
||||
.visible_notes_for_local()
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
NoteSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
||||
})
|
||||
.collect(),
|
||||
reading_list: core
|
||||
.visible_reading_list_for_local()
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
ReadingListSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
||||
})
|
||||
.collect(),
|
||||
site_permissions: core
|
||||
.visible_site_permissions_for_local()
|
||||
.into_iter()
|
||||
.map(SitePermissionSyncRecord::from_entry)
|
||||
.collect(),
|
||||
history: core
|
||||
.visible_history_for_local()
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
HistorySyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
||||
})
|
||||
.collect(),
|
||||
plugin_settings: core
|
||||
.visible_plugin_settings_for_local()
|
||||
.into_iter()
|
||||
.map(PluginSettingsSyncRecord::from_plugin)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ mod downloads;
|
||||
mod history;
|
||||
mod local_data_export_records;
|
||||
mod local_data_exports;
|
||||
mod local_visibility;
|
||||
mod notes;
|
||||
mod plugins;
|
||||
mod privacy;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! Local persistence sees more than cloud sync: pausing cloud sync or
|
||||
//! disabling a tab's sync must never reduce what survives a restart.
|
||||
//! Only Private-profile data stays out of the on-disk state.
|
||||
|
||||
use ely_domain::{
|
||||
BookmarkEntry, BrowserTab, HistoryEntry, NoteEntry, Profile, ProfileId, ProfileKind,
|
||||
ReadingListEntry, SitePermissionEntry, Space,
|
||||
};
|
||||
|
||||
use super::BrowserCore;
|
||||
use crate::state::InstalledPlugin;
|
||||
|
||||
impl BrowserCore {
|
||||
fn profile_persists_locally(&self, profile_id: &ProfileId) -> bool {
|
||||
self.profiles
|
||||
.iter()
|
||||
.any(|profile| profile.id() == profile_id && profile.kind() != &ProfileKind::Private)
|
||||
}
|
||||
|
||||
pub(crate) fn visible_profiles_for_local(&self) -> Vec<&Profile> {
|
||||
self.profiles.iter().filter(|profile| profile.kind() != &ProfileKind::Private).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_spaces_for_local(&self) -> Vec<&Space> {
|
||||
self.spaces.iter().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_tabs_for_local(&self) -> Vec<&BrowserTab> {
|
||||
self.tabs.iter().filter(|tab| self.profile_persists_locally(tab.profile_id())).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_bookmarks_for_local(&self) -> Vec<&BookmarkEntry> {
|
||||
self.bookmarks
|
||||
.iter()
|
||||
.filter(|entry| self.profile_persists_locally(entry.profile_id()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_notes_for_local(&self) -> Vec<&NoteEntry> {
|
||||
self.notes
|
||||
.iter()
|
||||
.filter(|entry| self.profile_persists_locally(entry.profile_id()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_reading_list_for_local(&self) -> Vec<&ReadingListEntry> {
|
||||
self.reading_list
|
||||
.iter()
|
||||
.filter(|entry| self.profile_persists_locally(entry.profile_id()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_site_permissions_for_local(&self) -> Vec<&SitePermissionEntry> {
|
||||
self.site_permissions
|
||||
.iter()
|
||||
.filter(|entry| self.profile_persists_locally(entry.profile_id()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_history_for_local(&self) -> Vec<&HistoryEntry> {
|
||||
self.history_entries
|
||||
.iter()
|
||||
.filter(|entry| self.profile_persists_locally(entry.profile_id()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn visible_plugin_settings_for_local(&self) -> Vec<&InstalledPlugin> {
|
||||
self.installed_plugins.iter().collect()
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ pub(crate) struct ProfileSyncRecord {
|
||||
}
|
||||
|
||||
impl ProfileSyncRecord {
|
||||
fn from_profile(profile: &Profile) -> Self {
|
||||
pub(crate) fn from_profile(profile: &Profile) -> Self {
|
||||
Self {
|
||||
id: profile.id().as_str().to_string(),
|
||||
name: profile.name().to_string(),
|
||||
@@ -189,7 +189,7 @@ pub(crate) struct SpaceSyncRecord {
|
||||
}
|
||||
|
||||
impl SpaceSyncRecord {
|
||||
fn from_space(space: &Space) -> Self {
|
||||
pub(crate) fn from_space(space: &Space) -> Self {
|
||||
Self {
|
||||
id: space.id().as_str().to_string(),
|
||||
name: space.name().to_string(),
|
||||
@@ -246,7 +246,7 @@ pub(crate) struct BookmarkSyncRecord {
|
||||
}
|
||||
|
||||
impl BookmarkSyncRecord {
|
||||
fn from_entry(entry: &BookmarkEntry, space_name: Option<String>) -> Self {
|
||||
pub(crate) fn from_entry(entry: &BookmarkEntry, space_name: Option<String>) -> Self {
|
||||
Self {
|
||||
id: entry.id().as_str().to_string(),
|
||||
title: entry.title().to_string(),
|
||||
@@ -285,7 +285,7 @@ pub(crate) struct TabSyncRecord {
|
||||
}
|
||||
|
||||
impl TabSyncRecord {
|
||||
fn from_entry(entry: &BrowserTab, space_name: Option<String>) -> Self {
|
||||
pub(crate) fn from_entry(entry: &BrowserTab, space_name: Option<String>) -> Self {
|
||||
Self {
|
||||
id: entry.id().as_str().to_string(),
|
||||
title: entry.title().to_string(),
|
||||
@@ -320,7 +320,7 @@ pub(crate) struct NoteSyncRecord {
|
||||
}
|
||||
|
||||
impl NoteSyncRecord {
|
||||
fn from_entry(entry: &NoteEntry, space_name: Option<String>) -> Self {
|
||||
pub(crate) fn from_entry(entry: &NoteEntry, space_name: Option<String>) -> Self {
|
||||
Self {
|
||||
id: entry.id().as_str().to_string(),
|
||||
profile_id: entry.profile_id().as_str().to_string(),
|
||||
@@ -366,7 +366,7 @@ pub(crate) struct ReadingListSyncRecord {
|
||||
}
|
||||
|
||||
impl ReadingListSyncRecord {
|
||||
fn from_entry(entry: &ReadingListEntry, space_name: Option<String>) -> Self {
|
||||
pub(crate) fn from_entry(entry: &ReadingListEntry, space_name: Option<String>) -> Self {
|
||||
Self {
|
||||
id: entry.id().as_str().to_string(),
|
||||
profile_id: entry.profile_id().as_str().to_string(),
|
||||
@@ -407,7 +407,7 @@ pub(crate) struct SitePermissionSyncRecord {
|
||||
}
|
||||
|
||||
impl SitePermissionSyncRecord {
|
||||
fn from_entry(entry: &SitePermissionEntry) -> Self {
|
||||
pub(crate) fn from_entry(entry: &SitePermissionEntry) -> Self {
|
||||
Self {
|
||||
profile_id: entry.profile_id().as_str().to_string(),
|
||||
origin: entry.origin().as_str().to_string(),
|
||||
@@ -433,7 +433,7 @@ pub(crate) struct HistorySyncRecord {
|
||||
}
|
||||
|
||||
impl HistorySyncRecord {
|
||||
fn from_entry(entry: &HistoryEntry, space_name: Option<String>) -> Self {
|
||||
pub(crate) fn from_entry(entry: &HistoryEntry, space_name: Option<String>) -> Self {
|
||||
Self {
|
||||
profile_id: entry.profile_id().as_str().to_string(),
|
||||
space_id: entry.space_id().as_str().to_string(),
|
||||
@@ -457,7 +457,7 @@ pub(crate) struct PluginSettingsSyncRecord {
|
||||
}
|
||||
|
||||
impl PluginSettingsSyncRecord {
|
||||
fn from_plugin(plugin: &InstalledPlugin) -> Self {
|
||||
pub(crate) fn from_plugin(plugin: &InstalledPlugin) -> Self {
|
||||
Self {
|
||||
plugin_id: plugin.id().as_str().to_string(),
|
||||
checksum: plugin.manifest().checksum().to_string(),
|
||||
|
||||
Reference in New Issue
Block a user