Add Sync now button that uploads a bookmarks snapshot

Wire `SyncEngine::upload_bytes` to a Settings → Sync button:

- `BrowserCore::build_sync_snapshot_bytes` serialises the user's
  bookmarks on the UI thread (cheap, synchronous).
- `ElyShell::trigger_cloud_sync_upload` resolves the active
  profile data dir, spawns a dedicated `ely-sync-upload` thread,
  and lets the engine run the blocking HTTP round-trip there so
  the GPUI render loop never stalls on the network — the same
  invariant the Servo IPC worker enforces.
- Outcomes go through `tracing` on the `ely::sync` target. Users
  drop a Better Auth bearer token into
  `<profile_data>/sync/bearer.token` to opt in; without one, the
  engine reports `SignedOut` and the click is a no-op.

The Better Auth handshake + device-approval UX still need their
own UI passes; this lands the data-plane plumbing so those pieces
slot in without re-architecting the snapshot path.
This commit is contained in:
2026-05-15 17:15:07 -04:00
parent a47fbdc09c
commit 6bacb3faa8
3 changed files with 139 additions and 22 deletions
+20 -9
View File
@@ -75,20 +75,17 @@ impl SyncEngine {
self.bearer_store.load().map(|token| token.is_some())
}
/// Build the JSON snapshot payload and ship it to the worker. The
/// caller passes the BrowserCore directly so the snapshot reads
/// the latest committed state; we don't keep a parallel copy.
pub fn upload_now(&mut self, core: &BrowserCore) -> Result<SyncOutcome, SyncClientError> {
/// Ship a pre-serialised snapshot payload to the worker. Callers
/// usually pair this with `BrowserCore::build_sync_snapshot_bytes`
/// — building the bytes on the UI thread and only crossing the
/// thread boundary with `Vec<u8>` keeps `BrowserCore` itself
/// single-threaded.
pub fn upload_bytes(&mut self, bytes: Vec<u8>) -> Result<SyncOutcome, SyncClientError> {
let Some(bearer) = self.bearer_store.load()? else {
let outcome = SyncOutcome::SignedOut;
self.last_outcome = Some(outcome.clone());
return Ok(outcome);
};
let snapshot = SyncSnapshotBody::from_core(core);
let bytes = serde_json::to_vec(&snapshot).map_err(|error| SyncClientError::Json {
endpoint: "snapshot".to_string(),
source: error,
})?;
let payload = SnapshotPayload::new(bytes)?;
let logical_clock = current_logical_clock();
let snapshot_id = snapshot_id_for_user(&self.identity);
@@ -201,3 +198,17 @@ impl SyncEngineBuilder {
SyncEngine::for_profile_dir(&self.profile_data_dir, self.device_name, self.platform)
}
}
impl BrowserCore {
/// Build the JSON byte payload the sync engine expects. Lives on
/// `BrowserCore` so the snapshot reads the live state and so the
/// UI thread does the (synchronous, cheap) serialization before
/// handing bytes off to the worker thread.
pub fn build_sync_snapshot_bytes(&self) -> Result<Vec<u8>, SyncClientError> {
let body = SyncSnapshotBody::from_core(self);
serde_json::to_vec(&body).map_err(|error| SyncClientError::Json {
endpoint: "snapshot".to_string(),
source: error,
})
}
}