Reflect real sync state on the Sync settings page
`SyncConnectionState` was a one-variant enum (`SignedOut`), so the
Sync page rendered "Local-only · sign-in coming soon" even after the
bearer token landed on disk and the upload thread completed. The
state machine now mirrors the actual lifecycle.
What lands:
- `SyncConnectionState` gains `SignedIn`, `AwaitingDeviceApproval`,
`SyncReady { last_synced_at_secs }`, `SyncError { message }`.
`SyncObjectState::Synced` joins the per-object enum so individual
rows can advertise "Synced" once a successful upload lands.
- `BrowserCore` stores the current `SyncConnectionState` and exposes
`set_sync_connection_state`. `sync_status` now propagates the live
state into the snapshot the UI reads.
- `ElyShell::probe_initial_sync_state` inspects
`<profile_data>/sync/bearer.token` synchronously at construction
so the first render of the sync page is honest about whether the
user is signed in.
- A `std::sync::mpsc` channel ferries upload outcomes from the
off-thread worker back to the shell; the existing 8 ms tick
drains it and stamps `core.set_sync_connection_state` with the
freshest result. The UI now shows "Signed in · awaiting first
sync", "Synced · last upload Xm ago", "Sync error · …", and the
worker-special "Signed in · waiting for device approval" when the
server returns `device_not_approved`.
This commit is contained in:
@@ -266,6 +266,7 @@ fn render_state_dot(state: SyncObjectState) -> AnyElement {
|
|||||||
SyncObjectState::LocalOnly => colors::ink_4(),
|
SyncObjectState::LocalOnly => colors::ink_4(),
|
||||||
SyncObjectState::Paused => colors::ink_5(),
|
SyncObjectState::Paused => colors::ink_5(),
|
||||||
SyncObjectState::PrivacyControlled => colors::accent(),
|
SyncObjectState::PrivacyControlled => colors::accent(),
|
||||||
|
SyncObjectState::Synced => colors::success(),
|
||||||
};
|
};
|
||||||
|
|
||||||
div().size(px(8.0)).rounded_full().bg(rgb(color)).into_any_element()
|
div().size(px(8.0)).rounded_full().bg(rgb(color)).into_any_element()
|
||||||
@@ -304,10 +305,46 @@ fn render_policy_toggle(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connection_label(connection: &SyncConnectionState) -> &'static str {
|
fn connection_label(connection: &SyncConnectionState) -> String {
|
||||||
match connection {
|
match connection {
|
||||||
SyncConnectionState::SignedOut => "Local-only · sign-in coming soon",
|
SyncConnectionState::SignedOut => "Local-only · drop a session token to enable".to_string(),
|
||||||
|
SyncConnectionState::SignedIn => "Signed in · awaiting first sync".to_string(),
|
||||||
|
SyncConnectionState::AwaitingDeviceApproval => {
|
||||||
|
"Signed in · waiting for device approval".to_string()
|
||||||
}
|
}
|
||||||
|
SyncConnectionState::SyncReady { last_synced_at_secs } => {
|
||||||
|
format!("Synced · last upload {}", relative_time_since(*last_synced_at_secs))
|
||||||
|
}
|
||||||
|
SyncConnectionState::SyncError { message } => {
|
||||||
|
format!("Sync error · {}", short_message(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_time_since(secs: u64) -> String {
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
let when = UNIX_EPOCH + Duration::from_secs(secs);
|
||||||
|
let elapsed = SystemTime::now().duration_since(when).unwrap_or_default();
|
||||||
|
let total_secs = elapsed.as_secs();
|
||||||
|
if total_secs < 60 {
|
||||||
|
return format!("{total_secs}s ago");
|
||||||
|
}
|
||||||
|
if total_secs < 3600 {
|
||||||
|
return format!("{}m ago", total_secs / 60);
|
||||||
|
}
|
||||||
|
if total_secs < 86400 {
|
||||||
|
return format!("{}h ago", total_secs / 3600);
|
||||||
|
}
|
||||||
|
format!("{}d ago", total_secs / 86400)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short_message(message: &str) -> String {
|
||||||
|
const MAX_LEN: usize = 72;
|
||||||
|
if message.len() <= MAX_LEN {
|
||||||
|
return message.to_string();
|
||||||
|
}
|
||||||
|
let truncated: String = message.chars().take(MAX_LEN - 1).collect();
|
||||||
|
format!("{truncated}…")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str {
|
fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str {
|
||||||
@@ -329,6 +366,7 @@ fn sync_object_state_label(state: SyncObjectState) -> &'static str {
|
|||||||
SyncObjectState::LocalOnly => "Local only",
|
SyncObjectState::LocalOnly => "Local only",
|
||||||
SyncObjectState::Paused => "Paused",
|
SyncObjectState::Paused => "Paused",
|
||||||
SyncObjectState::PrivacyControlled => "Privacy controlled",
|
SyncObjectState::PrivacyControlled => "Privacy controlled",
|
||||||
|
SyncObjectState::Synced => "Synced",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,10 +99,29 @@ pub struct ElyShell {
|
|||||||
pending_plugin_install: Option<PendingPluginInstall>,
|
pending_plugin_install: Option<PendingPluginInstall>,
|
||||||
pending_plugin_uninstall: Option<PendingPluginUninstall>,
|
pending_plugin_uninstall: Option<PendingPluginUninstall>,
|
||||||
web_surfaces: WebSurfaceStore,
|
web_surfaces: WebSurfaceStore,
|
||||||
|
/// Receives sync upload outcomes from the off-thread worker so the
|
||||||
|
/// shell can refresh the connection state without blocking. Sender
|
||||||
|
/// is cloned for each spawned upload; receiver is drained on every
|
||||||
|
/// `tick_external_web_surfaces`.
|
||||||
|
sync_inbox_rx: std::sync::mpsc::Receiver<SyncStateUpdate>,
|
||||||
|
pub(crate) sync_inbox_tx: std::sync::mpsc::Sender<SyncStateUpdate>,
|
||||||
_command_subscription: Subscription,
|
_command_subscription: Subscription,
|
||||||
_translucency_subscription: Subscription,
|
_translucency_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Messages the off-thread sync worker pushes back to the shell so the
|
||||||
|
/// `SyncConnectionState` on `BrowserCore` reflects the live engine
|
||||||
|
/// without the UI thread ever touching the network. `SignedIn` is the
|
||||||
|
/// initial-probe state set synchronously on shell startup and does not
|
||||||
|
/// flow through this channel.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum SyncStateUpdate {
|
||||||
|
SignedOut,
|
||||||
|
AwaitingDeviceApproval,
|
||||||
|
SyncReady { last_synced_at_secs: u64 },
|
||||||
|
SyncError { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
Self::new_with_config(InitialBrowserConfig::ely_defaults(), window, cx)
|
Self::new_with_config(InitialBrowserConfig::ely_defaults(), window, cx)
|
||||||
@@ -182,7 +201,8 @@ impl ElyShell {
|
|||||||
Err(error) => ShellState::StartupError(error.to_string()),
|
Err(error) => ShellState::StartupError(error.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let shell = Self {
|
let (sync_inbox_tx, sync_inbox_rx) = std::sync::mpsc::channel();
|
||||||
|
let mut shell = Self {
|
||||||
state,
|
state,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
command_input,
|
command_input,
|
||||||
@@ -215,13 +235,76 @@ impl ElyShell {
|
|||||||
pending_plugin_install: None,
|
pending_plugin_install: None,
|
||||||
pending_plugin_uninstall: None,
|
pending_plugin_uninstall: None,
|
||||||
web_surfaces: WebSurfaceStore::new(),
|
web_surfaces: WebSurfaceStore::new(),
|
||||||
|
sync_inbox_rx,
|
||||||
|
sync_inbox_tx,
|
||||||
_command_subscription: command_subscription,
|
_command_subscription: command_subscription,
|
||||||
_translucency_subscription: translucency_subscription,
|
_translucency_subscription: translucency_subscription,
|
||||||
};
|
};
|
||||||
|
shell.probe_initial_sync_state();
|
||||||
start_external_web_surface_timer(cx);
|
start_external_web_surface_timer(cx);
|
||||||
shell
|
shell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inspect the on-disk bearer token (if any) and seed the
|
||||||
|
/// `SyncConnectionState` so the Sync settings page reads the right
|
||||||
|
/// label on first render — without any sync setting page open it
|
||||||
|
/// would otherwise stay `SignedOut` until the user clicks Sync now.
|
||||||
|
fn probe_initial_sync_state(&mut self) {
|
||||||
|
let ShellState::Ready(core) = &mut self.state else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(snapshot) = core.snapshot().ok() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let active_profile_id = snapshot.active_profile_id.clone();
|
||||||
|
let Some(profile_root) = crate::services::servo_profile_data::default_profile_data_root()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let profile_dir = crate::services::servo_profile_data::profile_data_dir(
|
||||||
|
&profile_root,
|
||||||
|
&active_profile_id,
|
||||||
|
);
|
||||||
|
let bearer_path = profile_dir.join("sync").join("bearer.token");
|
||||||
|
let bearer_present = std::fs::metadata(&bearer_path).map(|m| m.len() > 0).unwrap_or(false);
|
||||||
|
let state = if bearer_present {
|
||||||
|
ely_domain::SyncConnectionState::SignedIn
|
||||||
|
} else {
|
||||||
|
ely_domain::SyncConnectionState::SignedOut
|
||||||
|
};
|
||||||
|
core.set_sync_connection_state(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain any sync upload outcomes the off-thread worker pushed
|
||||||
|
/// since the previous tick and stamp the resulting connection
|
||||||
|
/// state on `BrowserCore`. Returns `true` when at least one
|
||||||
|
/// update was applied so callers can `cx.notify()` accordingly.
|
||||||
|
pub(super) fn drain_sync_updates(&mut self) -> bool {
|
||||||
|
let ShellState::Ready(core) = &mut self.state else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut latest: Option<ely_domain::SyncConnectionState> = None;
|
||||||
|
while let Ok(update) = self.sync_inbox_rx.try_recv() {
|
||||||
|
latest = Some(match update {
|
||||||
|
SyncStateUpdate::SignedOut => ely_domain::SyncConnectionState::SignedOut,
|
||||||
|
SyncStateUpdate::AwaitingDeviceApproval => {
|
||||||
|
ely_domain::SyncConnectionState::AwaitingDeviceApproval
|
||||||
|
}
|
||||||
|
SyncStateUpdate::SyncReady { last_synced_at_secs } => {
|
||||||
|
ely_domain::SyncConnectionState::SyncReady { last_synced_at_secs }
|
||||||
|
}
|
||||||
|
SyncStateUpdate::SyncError { message } => {
|
||||||
|
ely_domain::SyncConnectionState::SyncError { message }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(state) = latest {
|
||||||
|
core.set_sync_connection_state(state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_command_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn focus_command_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let ShellState::Ready(core) = &mut self.state {
|
if let ShellState::Ready(core) = &mut self.state {
|
||||||
core.set_command_query(">");
|
core.set_command_query(">");
|
||||||
|
|||||||
@@ -243,10 +243,9 @@ impl ElyShell {
|
|||||||
|
|
||||||
/// Push the active profile's bookmarks to `ely-browser-cloud` as a
|
/// Push the active profile's bookmarks to `ely-browser-cloud` as a
|
||||||
/// snapshot. The HTTP round-trip runs on a dedicated worker thread
|
/// snapshot. The HTTP round-trip runs on a dedicated worker thread
|
||||||
/// (the UI thread never blocks on the network), and the result is
|
/// (the UI thread never blocks on the network), and the worker
|
||||||
/// emitted via `tracing` so the user can inspect it through
|
/// reports back through the shell's `sync_inbox` so the sync page
|
||||||
/// `RUST_LOG=ely::sync=info`. No bearer token on disk → the
|
/// reflects the new state without waiting for a manual refresh.
|
||||||
/// engine reports `SignedOut` and the click is a no-op.
|
|
||||||
pub(super) fn trigger_cloud_sync_upload(&mut self, _cx: &mut Context<Self>) {
|
pub(super) fn trigger_cloud_sync_upload(&mut self, _cx: &mut Context<Self>) {
|
||||||
let ShellState::Ready(core) = &self.state else {
|
let ShellState::Ready(core) = &self.state else {
|
||||||
return;
|
return;
|
||||||
@@ -272,9 +271,10 @@ impl ElyShell {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let tx = self.sync_inbox_tx.clone();
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name("ely-sync-upload".to_string())
|
.name("ely-sync-upload".to_string())
|
||||||
.spawn(move || run_sync_upload(profile_dir, device_name, bytes))
|
.spawn(move || run_sync_upload(profile_dir, device_name, bytes, tx))
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.unwrap_or_else(|error| {
|
.unwrap_or_else(|error| {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -312,29 +312,56 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_sync_upload(profile_dir: std::path::PathBuf, device_name: String, bytes: Vec<u8>) {
|
fn run_sync_upload(
|
||||||
|
profile_dir: std::path::PathBuf,
|
||||||
|
device_name: String,
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
inbox: std::sync::mpsc::Sender<super::SyncStateUpdate>,
|
||||||
|
) {
|
||||||
let mut engine = match SyncEngine::for_profile_dir(&profile_dir, device_name, sync_platform()) {
|
let mut engine = match SyncEngine::for_profile_dir(&profile_dir, device_name, sync_platform()) {
|
||||||
Ok(engine) => engine,
|
Ok(engine) => engine,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!(
|
let message = error.to_string();
|
||||||
target: "ely::sync",
|
tracing::warn!(target: "ely::sync", error = %message, "could not initialise sync engine");
|
||||||
error = %error,
|
let _ = inbox.send(super::SyncStateUpdate::SyncError { message });
|
||||||
"could not initialise sync engine",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match engine.upload_bytes(bytes) {
|
match engine.upload_bytes(bytes) {
|
||||||
Ok(outcome) => tracing::info!(
|
Ok(ely_browser_core::SyncOutcome::SignedOut) => {
|
||||||
|
tracing::info!(target: "ely::sync", "no bearer token on disk; sync skipped");
|
||||||
|
let _ = inbox.send(super::SyncStateUpdate::SignedOut);
|
||||||
|
}
|
||||||
|
Ok(ely_browser_core::SyncOutcome::Uploaded {
|
||||||
|
snapshot_id,
|
||||||
|
logical_clock,
|
||||||
|
payload_bytes,
|
||||||
|
device_id,
|
||||||
|
}) => {
|
||||||
|
tracing::info!(
|
||||||
target: "ely::sync",
|
target: "ely::sync",
|
||||||
outcome = ?outcome,
|
snapshot_id = %snapshot_id,
|
||||||
|
logical_clock,
|
||||||
|
payload_bytes,
|
||||||
|
device_id = %device_id,
|
||||||
"snapshot upload complete",
|
"snapshot upload complete",
|
||||||
),
|
);
|
||||||
Err(error) => tracing::warn!(
|
let last_synced_at_secs = std::time::SystemTime::now()
|
||||||
target: "ely::sync",
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
error = %error,
|
.map(|d| d.as_secs())
|
||||||
"snapshot upload failed",
|
.unwrap_or(0);
|
||||||
),
|
let _ = inbox.send(super::SyncStateUpdate::SyncReady { last_synced_at_secs });
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let message = error.to_string();
|
||||||
|
tracing::warn!(target: "ely::sync", error = %message, "snapshot upload failed");
|
||||||
|
let update = if message.contains("device_not_approved") {
|
||||||
|
super::SyncStateUpdate::AwaitingDeviceApproval
|
||||||
|
} else {
|
||||||
|
super::SyncStateUpdate::SyncError { message }
|
||||||
|
};
|
||||||
|
let _ = inbox.send(update);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ impl ElyShell {
|
|||||||
for metadata in result.page_metadata {
|
for metadata in result.page_metadata {
|
||||||
metadata_changed |= self.apply_web_surface_page_metadata(metadata);
|
metadata_changed |= self.apply_web_surface_page_metadata(metadata);
|
||||||
}
|
}
|
||||||
result.changed || url_changed || metadata_changed
|
let sync_changed = self.drain_sync_updates();
|
||||||
|
result.changed || url_changed || metadata_changed || sync_changed
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn record_external_web_viewport(
|
pub(super) fn record_external_web_viewport(
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ pub struct BrowserCore {
|
|||||||
update_policy: UpdatePolicy,
|
update_policy: UpdatePolicy,
|
||||||
appearance: AppearanceSettings,
|
appearance: AppearanceSettings,
|
||||||
sync_object_policies: SyncObjectPolicies,
|
sync_object_policies: SyncObjectPolicies,
|
||||||
|
sync_connection_state: ely_domain::SyncConnectionState,
|
||||||
command_query: String,
|
command_query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +211,7 @@ impl BrowserCore {
|
|||||||
update_policy: UpdatePolicy::default(),
|
update_policy: UpdatePolicy::default(),
|
||||||
appearance: AppearanceSettings::default(),
|
appearance: AppearanceSettings::default(),
|
||||||
sync_object_policies: SyncObjectPolicies::default(),
|
sync_object_policies: SyncObjectPolicies::default(),
|
||||||
|
sync_connection_state: ely_domain::SyncConnectionState::SignedOut,
|
||||||
spaces: vec![space],
|
spaces: vec![space],
|
||||||
profiles: vec![profile],
|
profiles: vec![profile],
|
||||||
tabs: vec![tab],
|
tabs: vec![tab],
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use ely_domain::{SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, SyncStatus};
|
use ely_domain::{
|
||||||
|
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
|
||||||
|
SyncStatus,
|
||||||
|
};
|
||||||
|
|
||||||
use super::BrowserCore;
|
use super::BrowserCore;
|
||||||
|
|
||||||
@@ -75,42 +78,44 @@ impl BrowserCore {
|
|||||||
self.sync_object_policies.get(kind)
|
self.sync_object_policies.get(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_sync_connection_state(&mut self, state: SyncConnectionState) {
|
||||||
|
self.sync_connection_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn sync_status(&self) -> SyncStatus {
|
pub(super) fn sync_status(&self) -> SyncStatus {
|
||||||
SyncStatus::signed_out(vec![
|
let enabled_state = match &self.sync_connection_state {
|
||||||
self.sync_object_status(
|
SyncConnectionState::SyncReady { .. } => SyncObjectState::Synced,
|
||||||
SyncObjectKind::Spaces,
|
_ => SyncObjectState::LocalOnly,
|
||||||
self.spaces.len(),
|
};
|
||||||
SyncObjectState::LocalOnly,
|
SyncStatus::new(
|
||||||
),
|
self.sync_connection_state.clone(),
|
||||||
|
vec![
|
||||||
|
self.sync_object_status(SyncObjectKind::Spaces, self.spaces.len(), enabled_state),
|
||||||
self.sync_object_status(
|
self.sync_object_status(
|
||||||
SyncObjectKind::Tabs,
|
SyncObjectKind::Tabs,
|
||||||
self.sync_enabled_tab_count(),
|
self.sync_enabled_tab_count(),
|
||||||
SyncObjectState::LocalOnly,
|
enabled_state,
|
||||||
),
|
),
|
||||||
self.sync_object_status(
|
self.sync_object_status(
|
||||||
SyncObjectKind::Bookmarks,
|
SyncObjectKind::Bookmarks,
|
||||||
self.bookmarks.len(),
|
self.bookmarks.len(),
|
||||||
SyncObjectState::LocalOnly,
|
enabled_state,
|
||||||
),
|
|
||||||
self.sync_object_status(
|
|
||||||
SyncObjectKind::Notes,
|
|
||||||
self.notes.len(),
|
|
||||||
SyncObjectState::LocalOnly,
|
|
||||||
),
|
),
|
||||||
|
self.sync_object_status(SyncObjectKind::Notes, self.notes.len(), enabled_state),
|
||||||
self.sync_object_status(
|
self.sync_object_status(
|
||||||
SyncObjectKind::ReadingList,
|
SyncObjectKind::ReadingList,
|
||||||
self.reading_list.len(),
|
self.reading_list.len(),
|
||||||
SyncObjectState::LocalOnly,
|
enabled_state,
|
||||||
),
|
),
|
||||||
self.sync_object_status(
|
self.sync_object_status(
|
||||||
SyncObjectKind::Profiles,
|
SyncObjectKind::Profiles,
|
||||||
self.profiles.len(),
|
self.profiles.len(),
|
||||||
SyncObjectState::LocalOnly,
|
enabled_state,
|
||||||
),
|
),
|
||||||
self.sync_object_status(
|
self.sync_object_status(
|
||||||
SyncObjectKind::SitePermissions,
|
SyncObjectKind::SitePermissions,
|
||||||
self.site_permissions.len(),
|
self.site_permissions.len(),
|
||||||
SyncObjectState::LocalOnly,
|
enabled_state,
|
||||||
),
|
),
|
||||||
self.sync_object_status(
|
self.sync_object_status(
|
||||||
SyncObjectKind::History,
|
SyncObjectKind::History,
|
||||||
@@ -120,9 +125,10 @@ impl BrowserCore {
|
|||||||
self.sync_object_status(
|
self.sync_object_status(
|
||||||
SyncObjectKind::PluginSettings,
|
SyncObjectKind::PluginSettings,
|
||||||
self.installed_plugins.len(),
|
self.installed_plugins.len(),
|
||||||
SyncObjectState::LocalOnly,
|
enabled_state,
|
||||||
),
|
),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_object_status(
|
fn sync_object_status(
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
|
/// Connection lifecycle of the cloud sync client.
|
||||||
|
///
|
||||||
|
/// The previous variant set was a single `SignedOut`, which made the
|
||||||
|
/// Sync settings page report "Local-only · sign-in coming soon" even
|
||||||
|
/// after the user dropped a bearer token in. The state machine now
|
||||||
|
/// progresses from `SignedOut` → `SignedIn` (token present, sync not
|
||||||
|
/// yet attempted) → `SyncReady` (last upload landed) or `SyncError`
|
||||||
|
/// (last upload failed). `AwaitingDeviceApproval` is the dedicated
|
||||||
|
/// state for the 403 the worker returns when the device-id bound to
|
||||||
|
/// the session has not yet been approved by another already-approved
|
||||||
|
/// device — surfacing it as its own variant lets the UI explain the
|
||||||
|
/// gap instead of bucketing it into a generic error.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum SyncConnectionState {
|
pub enum SyncConnectionState {
|
||||||
SignedOut,
|
SignedOut,
|
||||||
|
SignedIn,
|
||||||
|
AwaitingDeviceApproval,
|
||||||
|
SyncReady { last_synced_at_secs: u64 },
|
||||||
|
SyncError { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -28,6 +44,7 @@ pub enum SyncObjectState {
|
|||||||
LocalOnly,
|
LocalOnly,
|
||||||
Paused,
|
Paused,
|
||||||
PrivacyControlled,
|
PrivacyControlled,
|
||||||
|
Synced,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -85,13 +102,20 @@ impl SyncObjectStatus {
|
|||||||
|
|
||||||
impl SyncStatus {
|
impl SyncStatus {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn signed_out(objects: Vec<SyncObjectStatus>) -> Self {
|
pub fn new(connection: SyncConnectionState, objects: Vec<SyncObjectStatus>) -> Self {
|
||||||
Self {
|
let failed_objects = match &connection {
|
||||||
connection: SyncConnectionState::SignedOut,
|
SyncConnectionState::SyncError { .. } => 1,
|
||||||
pending_objects: 0,
|
_ => 0,
|
||||||
failed_objects: 0,
|
};
|
||||||
objects,
|
Self { connection, pending_objects: 0, failed_objects, objects }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convenience constructor preserved for tests + call sites that
|
||||||
|
/// have not been migrated to [`SyncStatus::new`] yet. Same result
|
||||||
|
/// as `SyncStatus::new(SyncConnectionState::SignedOut, objects)`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn signed_out(objects: Vec<SyncObjectStatus>) -> Self {
|
||||||
|
Self::new(SyncConnectionState::SignedOut, objects)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
Reference in New Issue
Block a user