refactor(sync): split snapshot records

This commit is contained in:
2026-05-16 05:52:39 -04:00
parent bfdc97d7fd
commit 7d5e936ca4
4 changed files with 188 additions and 187 deletions
+1
View File
@@ -2,6 +2,7 @@ mod error;
mod navigation;
mod state;
mod sync_engine;
mod sync_records;
pub use error::CoreError;
pub use state::{
+2 -3
View File
@@ -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 {
+1 -184
View File
@@ -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<SpaceSyncRecord>,
pub(crate) bookmarks: Vec<BookmarkSyncRecord>,
#[serde(default)]
pub(crate) tabs: Vec<TabSyncRecord>,
}
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<SpaceArchivePolicySyncRecord> 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<String>,
pub(crate) collection_name: String,
pub(crate) tags: Vec<String>,
pub(crate) note: Option<String>,
#[serde(default)]
pub(crate) thumbnail_key: Option<String>,
pub(crate) added_at_secs: u64,
}
impl BookmarkSyncRecord {
fn from_entry(entry: &BookmarkEntry, space_name: Option<String>) -> 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<String>,
#[serde(default)]
pub(crate) favicon_key: Option<String>,
#[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<String>) -> 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,
+184
View File
@@ -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<SpaceSyncRecord>,
pub(crate) bookmarks: Vec<BookmarkSyncRecord>,
#[serde(default)]
pub(crate) tabs: Vec<TabSyncRecord>,
}
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<SpaceArchivePolicySyncRecord> 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<String>,
pub(crate) collection_name: String,
pub(crate) tags: Vec<String>,
pub(crate) note: Option<String>,
#[serde(default)]
pub(crate) thumbnail_key: Option<String>,
pub(crate) added_at_secs: u64,
}
impl BookmarkSyncRecord {
fn from_entry(entry: &BookmarkEntry, space_name: Option<String>) -> 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<String>,
#[serde(default)]
pub(crate) favicon_key: Option<String>,
#[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<String>) -> 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)
}