feat(sync): include history in snapshots

This commit is contained in:
2026-05-16 07:03:59 -04:00
parent b550360a47
commit 80729083eb
9 changed files with 270 additions and 3 deletions
+1
View File
@@ -32,6 +32,7 @@ mod splits;
mod sync;
mod sync_apply;
mod sync_context;
mod sync_history;
mod sync_notes;
mod sync_profiles;
mod sync_reading_list;
@@ -31,6 +31,9 @@ impl BrowserCore {
for record in body.site_permissions {
self.apply_site_permission_sync_record(record, &mut summary, &context)?;
}
for record in body.history {
self.apply_history_sync_record(record, &mut summary, &context)?;
}
Ok(summary)
}
}
@@ -0,0 +1,90 @@
use std::{
num::NonZeroU32,
time::{Duration, UNIX_EPOCH},
};
use ely_domain::{HistoryEntry, SyncObjectKind, SyncObjectPolicy, TabId, UrlText};
use ely_sync_client::SyncClientError;
use super::{BrowserCore, sync::snapshot_schema_error, sync_context::SyncSnapshotApplyContext};
use crate::{sync_engine::SyncSnapshotApplySummary, sync_records::HistorySyncRecord};
impl BrowserCore {
pub(crate) fn visible_history_for_sync(&self) -> Vec<&HistoryEntry> {
if self.sync_object_policy(SyncObjectKind::History) == SyncObjectPolicy::Paused {
return Vec::new();
}
self.history_entries
.iter()
.filter(|entry| self.profile_allows_cloud_sync(entry.profile_id()))
.collect()
}
pub(super) fn apply_history_sync_record(
&mut self,
record: HistorySyncRecord,
summary: &mut SyncSnapshotApplySummary,
context: &SyncSnapshotApplyContext,
) -> Result<(), SyncClientError> {
let profile_id = self.sync_profile_id(&record.profile_id, context)?;
let space_id = self.sync_space_id(&record.space_id, record.space_name.as_deref())?;
let url = UrlText::parse(&record.url).map_err(snapshot_schema_error)?;
let source_tab_id = self.resolve_history_source_tab_id(
&record.source_tab_id,
&profile_id,
&space_id,
&url,
)?;
let visited_at = UNIX_EPOCH + Duration::from_secs(record.visited_at_secs);
let visit_count = NonZeroU32::new(record.visit_count).ok_or_else(|| {
snapshot_schema_error("history visit_count must be greater than zero")
})?;
let existing_index = self.history_entries.iter().position(|entry| {
entry.profile_id() == &profile_id
&& entry.space_id() == &space_id
&& entry.url() == &url
});
let entry = HistoryEntry::new(
profile_id,
space_id,
source_tab_id,
record.title,
url,
record.favicon_key,
visited_at,
);
let entry = HistoryEntry::restore(entry, visit_count);
match existing_index {
Some(index) if self.history_entries[index] == entry => summary.record_skipped(),
Some(index) => {
self.history_entries[index] = entry;
summary.record_updated();
}
None => {
self.history_entries.push(entry);
summary.record_imported();
}
}
Ok(())
}
fn resolve_history_source_tab_id(
&self,
raw: &str,
profile_id: &ely_domain::ProfileId,
space_id: &ely_domain::SpaceId,
url: &UrlText,
) -> Result<TabId, SyncClientError> {
let remote_tab_id = TabId::parse(raw).map_err(snapshot_schema_error)?;
if self.tabs.iter().any(|tab| tab.id() == &remote_tab_id) {
return Ok(remote_tab_id);
}
if let Some(tab) = self.tabs.iter().find(|tab| {
tab.profile_id() == profile_id && tab.space_id() == space_id && tab.url() == url
}) {
return Ok(tab.id().clone());
}
Ok(remote_tab_id)
}
}
+43 -2
View File
@@ -1,8 +1,9 @@
use std::time::{SystemTime, UNIX_EPOCH};
use ely_domain::{
ArchivePolicy, BookmarkEntry, BrowserTab, NoteEntry, NoteTarget, Profile, ProfileKind,
ProfileSyncPolicy, ReadingListEntry, ReadingProgress, SitePermissionEntry, Space, TabFlags,
ArchivePolicy, BookmarkEntry, BrowserTab, HistoryEntry, NoteEntry, NoteTarget, Profile,
ProfileKind, ProfileSyncPolicy, ReadingListEntry, ReadingProgress, SitePermissionEntry, Space,
TabFlags,
};
use serde::{Deserialize, Serialize};
@@ -26,6 +27,8 @@ pub(crate) struct SyncSnapshotBody {
pub(crate) reading_list: Vec<ReadingListSyncRecord>,
#[serde(default)]
pub(crate) site_permissions: Vec<SitePermissionSyncRecord>,
#[serde(default)]
pub(crate) history: Vec<HistorySyncRecord>,
}
impl SyncSnapshotBody {
@@ -81,6 +84,13 @@ impl SyncSnapshotBody {
.into_iter()
.map(SitePermissionSyncRecord::from_entry)
.collect(),
history: core
.visible_history_for_sync()
.into_iter()
.map(|entry| {
HistorySyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
})
.collect(),
}
}
}
@@ -399,6 +409,37 @@ impl SitePermissionSyncRecord {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct HistorySyncRecord {
pub(crate) profile_id: String,
pub(crate) space_id: String,
#[serde(default)]
pub(crate) space_name: Option<String>,
pub(crate) source_tab_id: String,
pub(crate) title: String,
pub(crate) url: String,
#[serde(default)]
pub(crate) favicon_key: Option<String>,
pub(crate) visited_at_secs: u64,
pub(crate) visit_count: u32,
}
impl HistorySyncRecord {
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(),
space_name,
source_tab_id: entry.source_tab_id().as_str().to_string(),
title: entry.title().to_string(),
url: entry.url().as_str().to_string(),
favicon_key: entry.favicon_key().map(str::to_string),
visited_at_secs: system_time_secs(entry.visited_at()),
visit_count: entry.visit_count(),
}
}
}
fn default_sync_enabled() -> bool {
true
}