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:
@@ -136,19 +136,40 @@ fn render_metric(label: &'static str, value: usize, color: u32) -> AnyElement {
|
||||
|
||||
fn render_reset_button(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div()
|
||||
.id(SharedString::from("sync-reset"))
|
||||
.px(px(12.0))
|
||||
.py(px(7.0))
|
||||
.rounded(px(8.0))
|
||||
.bg(rgba(BUTTON_BG))
|
||||
.text_size(px(12.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::INK_2))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(rgba(BUTTON_BG_HOVER)))
|
||||
.active(|style| style.opacity(0.85))
|
||||
.on_click(cx.listener(|shell, _, _, cx| shell.reset_sync_settings(cx)))
|
||||
.child("Reset to defaults")
|
||||
.flex()
|
||||
.gap(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.id(SharedString::from("sync-upload"))
|
||||
.px(px(12.0))
|
||||
.py(px(7.0))
|
||||
.rounded(px(8.0))
|
||||
.bg(rgba(colors::ACCENT))
|
||||
.text_size(px(12.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(0xfff5e6))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.opacity(0.92))
|
||||
.active(|style| style.opacity(0.78))
|
||||
.on_click(cx.listener(|shell, _, _, cx| shell.trigger_cloud_sync_upload(cx)))
|
||||
.child("Sync now"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id(SharedString::from("sync-reset"))
|
||||
.px(px(12.0))
|
||||
.py(px(7.0))
|
||||
.rounded(px(8.0))
|
||||
.bg(rgba(BUTTON_BG))
|
||||
.text_size(px(12.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::INK_2))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(rgba(BUTTON_BG_HOVER)))
|
||||
.active(|style| style.opacity(0.85))
|
||||
.on_click(cx.listener(|shell, _, _, cx| shell.reset_sync_settings(cx)))
|
||||
.child("Reset to defaults"),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ely_browser_core::SyncEngine;
|
||||
use ely_domain::{
|
||||
ArchivePolicy, DEFAULT_TRANSLUCENCY_PCT, DiagnosticsReportingPolicy, DownloadPolicy,
|
||||
FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, ProfileId, ProfileSyncPolicy,
|
||||
@@ -6,6 +7,8 @@ use ely_domain::{
|
||||
use gpui::Context;
|
||||
use gpui_component::slider::SliderValue;
|
||||
|
||||
use crate::services::servo_profile_data::{default_profile_data_root, profile_data_dir};
|
||||
|
||||
use super::{ElyShell, ShellState};
|
||||
|
||||
impl ElyShell {
|
||||
@@ -238,6 +241,50 @@ 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 result is
|
||||
/// emitted via `tracing` so the user can inspect it through
|
||||
/// `RUST_LOG=ely::sync=info`. No bearer token on disk → the
|
||||
/// engine reports `SignedOut` and the click is a no-op.
|
||||
pub(super) fn trigger_cloud_sync_upload(&mut self, _cx: &mut Context<Self>) {
|
||||
let ShellState::Ready(core) = &self.state else {
|
||||
return;
|
||||
};
|
||||
let Some(snapshot) = core.snapshot().ok() else {
|
||||
return;
|
||||
};
|
||||
let active_profile_id = snapshot.active_profile_id.clone();
|
||||
let device_name = format!("ELY · {}", snapshot.active_profile_name);
|
||||
let Some(profile_root) = default_profile_data_root() else {
|
||||
tracing::warn!(target: "ely::sync", "profile data root is unavailable");
|
||||
return;
|
||||
};
|
||||
let profile_dir = profile_data_dir(&profile_root, &active_profile_id);
|
||||
let bytes = match core.build_sync_snapshot_bytes() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
target: "ely::sync",
|
||||
error = %error,
|
||||
"snapshot serialisation failed; aborting upload",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
std::thread::Builder::new()
|
||||
.name("ely-sync-upload".to_string())
|
||||
.spawn(move || run_sync_upload(profile_dir, device_name, bytes))
|
||||
.map(|_| ())
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::warn!(
|
||||
target: "ely::sync",
|
||||
error = %error,
|
||||
"failed to spawn ely-sync-upload thread",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn set_update_policy(
|
||||
&mut self,
|
||||
update_policy: UpdatePolicy,
|
||||
@@ -264,3 +311,41 @@ impl ElyShell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_sync_upload(profile_dir: std::path::PathBuf, device_name: String, bytes: Vec<u8>) {
|
||||
let mut engine = match SyncEngine::for_profile_dir(&profile_dir, device_name, sync_platform()) {
|
||||
Ok(engine) => engine,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
target: "ely::sync",
|
||||
error = %error,
|
||||
"could not initialise sync engine",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
match engine.upload_bytes(bytes) {
|
||||
Ok(outcome) => tracing::info!(
|
||||
target: "ely::sync",
|
||||
outcome = ?outcome,
|
||||
"snapshot upload complete",
|
||||
),
|
||||
Err(error) => tracing::warn!(
|
||||
target: "ely::sync",
|
||||
error = %error,
|
||||
"snapshot upload failed",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const fn sync_platform() -> &'static str {
|
||||
if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"other"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user