feat(sync): round trip cloud snapshots

This commit is contained in:
2026-05-16 00:01:28 -04:00
parent 06e947f43e
commit 40ab6b4874
22 changed files with 694 additions and 69 deletions
+28 -1
View File
@@ -277,6 +277,7 @@ impl ElyShell {
let mut latest_connection: Option<ely_domain::SyncConnectionState> = None;
let mut auth_changed = false;
let mut trigger_initial_sync = false;
let mut trigger_merged_upload = None;
while let Ok(update) = self.sync_inbox_rx.try_recv() {
match update {
SyncStateUpdate::SignedOut => {
@@ -286,6 +287,28 @@ impl ElyShell {
latest_connection =
Some(ely_domain::SyncConnectionState::AwaitingDeviceApproval);
}
SyncStateUpdate::RemoteSnapshot { bytes, logical_clock } => {
if let ShellState::Ready(core) = &mut self.state {
match core.apply_sync_snapshot_bytes(&bytes) {
Ok(summary) => {
tracing::info!(
target: "ely::sync",
imported = summary.imported(),
updated = summary.updated(),
skipped = summary.skipped(),
"remote snapshot applied",
);
trigger_merged_upload = Some(logical_clock);
}
Err(error) => {
latest_connection =
Some(ely_domain::SyncConnectionState::SyncError {
message: error.to_string(),
});
}
}
}
}
SyncStateUpdate::SyncReady { last_synced_at_secs } => {
latest_connection =
Some(ely_domain::SyncConnectionState::SyncReady { last_synced_at_secs });
@@ -317,7 +340,11 @@ impl ElyShell {
if trigger_initial_sync {
self.trigger_cloud_sync_upload();
}
auth_changed || trigger_initial_sync
let merged_upload_requested = trigger_merged_upload.is_some();
if let Some(logical_clock_floor) = trigger_merged_upload {
self.trigger_cloud_sync_upload_after_remote(logical_clock_floor);
}
auth_changed || trigger_initial_sync || merged_upload_requested
}
fn focus_command_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+65 -8
View File
@@ -242,12 +242,15 @@ impl ElyShell {
}
}
/// Push the active profile's bookmarks to `ely-browser-cloud` as a
/// snapshot. The HTTP round-trip runs on a dedicated worker thread
/// (the UI thread never blocks on the network), and the worker
/// reports back through the shell's `sync_inbox` so the sync page
/// reflects the new state without waiting for a manual refresh.
pub(crate) fn trigger_cloud_sync_upload(&mut self) {
self.trigger_cloud_sync_upload_with_clock_floor(None);
}
pub(crate) fn trigger_cloud_sync_upload_after_remote(&mut self, logical_clock_floor: u64) {
self.trigger_cloud_sync_upload_with_clock_floor(Some(logical_clock_floor));
}
fn trigger_cloud_sync_upload_with_clock_floor(&mut self, logical_clock_floor: Option<u64>) {
let ShellState::Ready(core) = &self.state else {
return;
};
@@ -273,9 +276,13 @@ impl ElyShell {
}
};
let tx = self.sync_inbox_tx.clone();
let thread_name =
if logical_clock_floor.is_some() { "ely-sync-merge-upload" } else { "ely-sync-upload" };
std::thread::Builder::new()
.name("ely-sync-upload".to_string())
.spawn(move || run_sync_upload(profile_dir, device_name, bytes, tx))
.name(thread_name.to_string())
.spawn(move || {
run_sync_upload(profile_dir, device_name, bytes, logical_clock_floor, tx)
})
.map(|_| ())
.unwrap_or_else(|error| {
tracing::warn!(
@@ -317,6 +324,7 @@ fn run_sync_upload(
profile_dir: std::path::PathBuf,
device_name: String,
bytes: Vec<u8>,
logical_clock_floor: Option<u64>,
inbox: std::sync::mpsc::Sender<SyncStateUpdate>,
) {
let mut engine = match SyncEngine::for_profile_dir(
@@ -332,11 +340,60 @@ fn run_sync_upload(
return;
}
};
match engine.upload_bytes(bytes) {
let outcome = match logical_clock_floor {
Some(floor) => engine.upload_merged_bytes(bytes, floor),
None => engine.sync_bytes(bytes),
};
match outcome {
Ok(ely_browser_core::SyncOutcome::SignedOut) => {
tracing::info!(target: "ely::sync", "no bearer token on disk; sync skipped");
let _ = inbox.send(SyncStateUpdate::SignedOut);
}
Ok(ely_browser_core::SyncOutcome::AwaitingDeviceApproval { device_id }) => {
tracing::info!(
target: "ely::sync",
device_id = %device_id,
"sync device is awaiting approval",
);
let _ = inbox.send(SyncStateUpdate::AwaitingDeviceApproval);
}
Ok(ely_browser_core::SyncOutcome::RemoteSnapshot {
snapshot_id,
logical_clock,
payload_bytes,
device_id,
bytes,
}) => {
tracing::info!(
target: "ely::sync",
snapshot_id = %snapshot_id,
logical_clock,
payload_bytes,
device_id = %device_id,
"remote snapshot downloaded",
);
let _ = inbox.send(SyncStateUpdate::RemoteSnapshot { bytes, logical_clock });
}
Ok(ely_browser_core::SyncOutcome::AlreadyCurrent {
snapshot_id,
logical_clock,
payload_bytes,
device_id,
}) => {
tracing::info!(
target: "ely::sync",
snapshot_id = %snapshot_id,
logical_clock,
payload_bytes,
device_id = %device_id,
"snapshot already current",
);
let last_synced_at_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let _ = inbox.send(SyncStateUpdate::SyncReady { last_synced_at_secs });
}
Ok(ely_browser_core::SyncOutcome::Uploaded {
snapshot_id,
logical_clock,
+1
View File
@@ -7,6 +7,7 @@
pub(crate) enum SyncStateUpdate {
SignedOut,
AwaitingDeviceApproval,
RemoteSnapshot { bytes: Vec<u8>, logical_clock: u64 },
SyncReady { last_synced_at_secs: u64 },
SyncError { message: String },
AuthOtpSent { email: String },