diff --git a/crates/ely_browser_core/src/lib.rs b/crates/ely_browser_core/src/lib.rs index 1290db3..861be42 100644 --- a/crates/ely_browser_core/src/lib.rs +++ b/crates/ely_browser_core/src/lib.rs @@ -2,6 +2,7 @@ mod error; mod navigation; mod state; mod sync_engine; +mod sync_records; pub use error::CoreError; pub use state::{ diff --git a/crates/ely_browser_core/src/state/sync.rs b/crates/ely_browser_core/src/state/sync.rs index 22c4d12..2f130b8 100644 --- a/crates/ely_browser_core/src/state/sync.rs +++ b/crates/ely_browser_core/src/state/sync.rs @@ -8,9 +8,8 @@ use ely_domain::{ use ely_sync_client::SyncClientError; use super::BrowserCore; -use crate::sync_engine::{ - BookmarkSyncRecord, SpaceSyncRecord, SyncSnapshotApplySummary, SyncSnapshotBody, TabSyncRecord, -}; +use crate::sync_engine::SyncSnapshotApplySummary; +use crate::sync_records::{BookmarkSyncRecord, SpaceSyncRecord, SyncSnapshotBody, TabSyncRecord}; #[derive(Clone, Debug)] pub(super) struct SyncObjectPolicies { diff --git a/crates/ely_browser_core/src/sync_engine.rs b/crates/ely_browser_core/src/sync_engine.rs index 8e539a9..b8aa370 100644 --- a/crates/ely_browser_core/src/sync_engine.rs +++ b/crates/ely_browser_core/src/sync_engine.rs @@ -3,14 +3,13 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use ely_domain::{ArchivePolicy, BookmarkEntry, BrowserTab, Space, TabFlags}; use ely_sync_client::{ ApiClientConfig, BearerToken, BearerTokenStore, DeviceIdentity, SnapshotPayload, SnapshotUploadRequest, SyncApiClient, SyncClientError, SyncLatestSnapshotDocument, }; -use serde::{Deserialize, Serialize}; use crate::state::BrowserCore; +use crate::sync_records::{SNAPSHOT_SCHEMA_REV, SyncSnapshotBody}; /// Per-profile sync engine for device identity, bearer-token storage, and snapshot IO. #[derive(Debug)] @@ -261,8 +260,6 @@ impl SyncSnapshotApplySummary { } } -const SNAPSHOT_SCHEMA_REV: u32 = 1; - fn current_logical_clock() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|elapsed| elapsed.as_secs()).unwrap_or(0) } @@ -275,186 +272,6 @@ fn device_registration_idempotency_key(identity: &DeviceIdentity) -> String { format!("device-register:{}", identity.device_id) } -#[derive(Serialize, Deserialize)] -pub(crate) struct SyncSnapshotBody { - pub(crate) schema_rev: u32, - #[serde(default)] - pub(crate) spaces: Vec, - pub(crate) bookmarks: Vec, - #[serde(default)] - pub(crate) tabs: Vec, -} - -impl SyncSnapshotBody { - fn from_core(core: &BrowserCore) -> Self { - Self { - schema_rev: SNAPSHOT_SCHEMA_REV, - spaces: core - .visible_spaces_for_sync() - .into_iter() - .map(SpaceSyncRecord::from_space) - .collect(), - bookmarks: core - .visible_bookmarks_for_sync() - .into_iter() - .map(|entry| { - BookmarkSyncRecord::from_entry( - entry, - core.sync_space_name_for(entry.space_id()), - ) - }) - .collect(), - tabs: core - .visible_tabs_for_sync() - .into_iter() - .map(|entry| { - TabSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id())) - }) - .collect(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct SpaceSyncRecord { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) icon: String, - pub(crate) accent_hex: u32, - pub(crate) default_profile_id: String, - pub(crate) archive_policy: SpaceArchivePolicySyncRecord, - pub(crate) sidebar_width_px: u16, - pub(crate) sort_key: u64, -} - -impl SpaceSyncRecord { - fn from_space(space: &Space) -> Self { - Self { - id: space.id().as_str().to_string(), - name: space.name().to_string(), - icon: space.icon().to_string(), - accent_hex: space.accent_hex(), - default_profile_id: space.default_profile_id().as_str().to_string(), - archive_policy: SpaceArchivePolicySyncRecord::from(space.archive_policy()), - sidebar_width_px: space.sidebar_width_px(), - sort_key: space.sort_key(), - } - } -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub(crate) enum SpaceArchivePolicySyncRecord { - Manual, - IdleDays { days: u16 }, -} - -impl From<&ArchivePolicy> for SpaceArchivePolicySyncRecord { - fn from(policy: &ArchivePolicy) -> Self { - match policy { - ArchivePolicy::Manual => Self::Manual, - ArchivePolicy::IdleDays(days) => Self::IdleDays { days: *days }, - } - } -} - -impl From for ArchivePolicy { - fn from(policy: SpaceArchivePolicySyncRecord) -> Self { - match policy { - SpaceArchivePolicySyncRecord::Manual => Self::Manual, - SpaceArchivePolicySyncRecord::IdleDays { days } => Self::IdleDays(days), - } - } -} - -/// Wire representation of a bookmark with default-fill fields for older snapshots. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct BookmarkSyncRecord { - pub(crate) id: String, - pub(crate) title: String, - pub(crate) url: String, - pub(crate) profile_id: String, - pub(crate) space_id: String, - #[serde(default)] - pub(crate) space_name: Option, - pub(crate) collection_name: String, - pub(crate) tags: Vec, - pub(crate) note: Option, - #[serde(default)] - pub(crate) thumbnail_key: Option, - pub(crate) added_at_secs: u64, -} - -impl BookmarkSyncRecord { - fn from_entry(entry: &BookmarkEntry, space_name: Option) -> Self { - Self { - id: entry.id().as_str().to_string(), - title: entry.title().to_string(), - url: entry.url().as_str().to_string(), - profile_id: entry.profile_id().as_str().to_string(), - space_id: entry.space_id().as_str().to_string(), - space_name, - collection_name: entry.collection_name().to_string(), - tags: entry.tags().to_vec(), - note: entry.note().map(str::to_string), - thumbnail_key: entry.thumbnail_key().map(str::to_string), - added_at_secs: system_time_secs(entry.added_at()), - } - } -} - -/// Wire representation of an open tab. The record carries only -/// user-visible tab state; runtime-only fields such as split layout -/// membership and crash state stay local to the receiving device. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct TabSyncRecord { - pub(crate) id: String, - pub(crate) title: String, - pub(crate) url: String, - pub(crate) profile_id: String, - pub(crate) space_id: String, - #[serde(default)] - pub(crate) space_name: Option, - #[serde(default)] - pub(crate) favicon_key: Option, - #[serde(default)] - pub(crate) flags: TabFlags, - pub(crate) sort_key: u64, - #[serde(default = "default_sync_enabled")] - pub(crate) sync_enabled: bool, - pub(crate) zoom_percent: u16, - pub(crate) created_at_secs: u64, - pub(crate) last_active_at_secs: u64, -} - -impl TabSyncRecord { - fn from_entry(entry: &BrowserTab, space_name: Option) -> Self { - Self { - id: entry.id().as_str().to_string(), - title: entry.title().to_string(), - url: entry.url().as_str().to_string(), - profile_id: entry.profile_id().as_str().to_string(), - space_id: entry.space_id().as_str().to_string(), - space_name, - favicon_key: entry.favicon_key().map(str::to_string), - flags: entry.flags().clone(), - sort_key: entry.sort_key(), - sync_enabled: entry.sync_enabled(), - zoom_percent: entry.zoom_percent(), - created_at_secs: system_time_secs(entry.created_at()), - last_active_at_secs: system_time_secs(entry.last_active_at()), - } - } -} - -fn default_sync_enabled() -> bool { - true -} - -fn system_time_secs(time: SystemTime) -> u64 { - time.duration_since(UNIX_EPOCH).map(|elapsed| elapsed.as_secs()).unwrap_or(0) -} - #[derive(Debug)] pub struct SyncEngineBuilder { pub profile_data_dir: PathBuf, diff --git a/crates/ely_browser_core/src/sync_records.rs b/crates/ely_browser_core/src/sync_records.rs new file mode 100644 index 0000000..5809ca2 --- /dev/null +++ b/crates/ely_browser_core/src/sync_records.rs @@ -0,0 +1,184 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use ely_domain::{ArchivePolicy, BookmarkEntry, BrowserTab, Space, TabFlags}; +use serde::{Deserialize, Serialize}; + +use crate::state::BrowserCore; + +pub(crate) const SNAPSHOT_SCHEMA_REV: u32 = 1; + +#[derive(Serialize, Deserialize)] +pub(crate) struct SyncSnapshotBody { + pub(crate) schema_rev: u32, + #[serde(default)] + pub(crate) spaces: Vec, + pub(crate) bookmarks: Vec, + #[serde(default)] + pub(crate) tabs: Vec, +} + +impl SyncSnapshotBody { + pub(crate) fn from_core(core: &BrowserCore) -> Self { + Self { + schema_rev: SNAPSHOT_SCHEMA_REV, + spaces: core + .visible_spaces_for_sync() + .into_iter() + .map(SpaceSyncRecord::from_space) + .collect(), + bookmarks: core + .visible_bookmarks_for_sync() + .into_iter() + .map(|entry| { + BookmarkSyncRecord::from_entry( + entry, + core.sync_space_name_for(entry.space_id()), + ) + }) + .collect(), + tabs: core + .visible_tabs_for_sync() + .into_iter() + .map(|entry| { + TabSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id())) + }) + .collect(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct SpaceSyncRecord { + pub(crate) id: String, + pub(crate) name: String, + pub(crate) icon: String, + pub(crate) accent_hex: u32, + pub(crate) default_profile_id: String, + pub(crate) archive_policy: SpaceArchivePolicySyncRecord, + pub(crate) sidebar_width_px: u16, + pub(crate) sort_key: u64, +} + +impl SpaceSyncRecord { + fn from_space(space: &Space) -> Self { + Self { + id: space.id().as_str().to_string(), + name: space.name().to_string(), + icon: space.icon().to_string(), + accent_hex: space.accent_hex(), + default_profile_id: space.default_profile_id().as_str().to_string(), + archive_policy: SpaceArchivePolicySyncRecord::from(space.archive_policy()), + sidebar_width_px: space.sidebar_width_px(), + sort_key: space.sort_key(), + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub(crate) enum SpaceArchivePolicySyncRecord { + Manual, + IdleDays { days: u16 }, +} + +impl From<&ArchivePolicy> for SpaceArchivePolicySyncRecord { + fn from(policy: &ArchivePolicy) -> Self { + match policy { + ArchivePolicy::Manual => Self::Manual, + ArchivePolicy::IdleDays(days) => Self::IdleDays { days: *days }, + } + } +} + +impl From for ArchivePolicy { + fn from(policy: SpaceArchivePolicySyncRecord) -> Self { + match policy { + SpaceArchivePolicySyncRecord::Manual => Self::Manual, + SpaceArchivePolicySyncRecord::IdleDays { days } => Self::IdleDays(days), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct BookmarkSyncRecord { + pub(crate) id: String, + pub(crate) title: String, + pub(crate) url: String, + pub(crate) profile_id: String, + pub(crate) space_id: String, + #[serde(default)] + pub(crate) space_name: Option, + pub(crate) collection_name: String, + pub(crate) tags: Vec, + pub(crate) note: Option, + #[serde(default)] + pub(crate) thumbnail_key: Option, + pub(crate) added_at_secs: u64, +} + +impl BookmarkSyncRecord { + fn from_entry(entry: &BookmarkEntry, space_name: Option) -> Self { + Self { + id: entry.id().as_str().to_string(), + title: entry.title().to_string(), + url: entry.url().as_str().to_string(), + profile_id: entry.profile_id().as_str().to_string(), + space_id: entry.space_id().as_str().to_string(), + space_name, + collection_name: entry.collection_name().to_string(), + tags: entry.tags().to_vec(), + note: entry.note().map(str::to_string), + thumbnail_key: entry.thumbnail_key().map(str::to_string), + added_at_secs: system_time_secs(entry.added_at()), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct TabSyncRecord { + pub(crate) id: String, + pub(crate) title: String, + pub(crate) url: String, + pub(crate) profile_id: String, + pub(crate) space_id: String, + #[serde(default)] + pub(crate) space_name: Option, + #[serde(default)] + pub(crate) favicon_key: Option, + #[serde(default)] + pub(crate) flags: TabFlags, + pub(crate) sort_key: u64, + #[serde(default = "default_sync_enabled")] + pub(crate) sync_enabled: bool, + pub(crate) zoom_percent: u16, + pub(crate) created_at_secs: u64, + pub(crate) last_active_at_secs: u64, +} + +impl TabSyncRecord { + fn from_entry(entry: &BrowserTab, space_name: Option) -> Self { + Self { + id: entry.id().as_str().to_string(), + title: entry.title().to_string(), + url: entry.url().as_str().to_string(), + profile_id: entry.profile_id().as_str().to_string(), + space_id: entry.space_id().as_str().to_string(), + space_name, + favicon_key: entry.favicon_key().map(str::to_string), + flags: entry.flags().clone(), + sort_key: entry.sort_key(), + sync_enabled: entry.sync_enabled(), + zoom_percent: entry.zoom_percent(), + created_at_secs: system_time_secs(entry.created_at()), + last_active_at_secs: system_time_secs(entry.last_active_at()), + } + } +} + +fn default_sync_enabled() -> bool { + true +} + +fn system_time_secs(time: SystemTime) -> u64 { + time.duration_since(UNIX_EPOCH).map(|elapsed| elapsed.as_secs()).unwrap_or(0) +}