feat(sync): auto upload changed browser state

This commit is contained in:
2026-05-16 05:13:19 -04:00
parent 0d755af005
commit 51f836f34c
8 changed files with 233 additions and 96 deletions
+4 -1
View File
@@ -184,7 +184,10 @@ impl ElyShell {
}; };
match import_result { match import_result {
Ok(message) => self.set_bookmark_file_notice(message, cx), Ok(message) => {
self.set_bookmark_file_notice(message, cx);
self.schedule_cloud_sync_upload(cx);
}
Err(error) => self.set_bookmark_file_error(error, cx), Err(error) => self.set_bookmark_file_error(error, cx),
} }
} }
+3 -1
View File
@@ -91,9 +91,11 @@ impl ElyShell {
ShellState::StartupError(message) => Err(message.clone()), ShellState::StartupError(message) => Err(message.clone()),
}; };
let updated = result.is_ok();
self.bookmark_edit_error = result.err(); self.bookmark_edit_error = result.err();
if self.bookmark_edit_error.is_none() { if updated {
self.pending_bookmark_edit = None; self.pending_bookmark_edit = None;
self.schedule_cloud_sync_upload(cx);
} }
cx.notify(); cx.notify();
} }
+23 -79
View File
@@ -105,6 +105,10 @@ pub struct ElyShell {
/// `tick_external_web_surfaces`. /// `tick_external_web_surfaces`.
sync_inbox_rx: std::sync::mpsc::Receiver<SyncStateUpdate>, sync_inbox_rx: std::sync::mpsc::Receiver<SyncStateUpdate>,
pub(crate) sync_inbox_tx: std::sync::mpsc::Sender<SyncStateUpdate>, pub(crate) sync_inbox_tx: std::sync::mpsc::Sender<SyncStateUpdate>,
sync_upload_scheduled: bool,
sync_upload_in_flight: bool,
sync_upload_pending: bool,
sync_upload_pending_logical_clock_floor: Option<u64>,
pub(crate) auth_email_input: Entity<InputState>, pub(crate) auth_email_input: Entity<InputState>,
pub(crate) auth_otp_input: Entity<InputState>, pub(crate) auth_otp_input: Entity<InputState>,
pub(crate) auth_flow_phase: auth::AuthFlowPhase, pub(crate) auth_flow_phase: auth::AuthFlowPhase,
@@ -178,6 +182,7 @@ impl ElyShell {
if sync_address { if sync_address {
shell.sync_address_input(window, cx); shell.sync_address_input(window, cx);
shell.schedule_cloud_sync_upload(cx);
} }
cx.notify(); cx.notify();
@@ -231,6 +236,10 @@ impl ElyShell {
web_surfaces: WebSurfaceStore::new(), web_surfaces: WebSurfaceStore::new(),
sync_inbox_rx, sync_inbox_rx,
sync_inbox_tx, sync_inbox_tx,
sync_upload_scheduled: false,
sync_upload_in_flight: false,
sync_upload_pending: false,
sync_upload_pending_logical_clock_floor: None,
auth_email_input, auth_email_input,
auth_otp_input, auth_otp_input,
auth_flow_phase: auth::AuthFlowPhase::Idle, auth_flow_phase: auth::AuthFlowPhase::Idle,
@@ -245,84 +254,6 @@ impl ElyShell {
shell shell
} }
/// 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 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 => {
latest_connection = Some(ely_domain::SyncConnectionState::SignedOut);
}
SyncStateUpdate::AwaitingDeviceApproval => {
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 });
}
SyncStateUpdate::SyncError { message } => {
latest_connection =
Some(ely_domain::SyncConnectionState::SyncError { message });
}
SyncStateUpdate::AuthOtpSent { email } => {
self.auth_flow_phase = auth::AuthFlowPhase::AwaitingOtp { email };
auth_changed = true;
}
SyncStateUpdate::AuthSucceeded { email } => {
self.auth_flow_phase = auth::AuthFlowPhase::Idle;
latest_connection = Some(ely_domain::SyncConnectionState::SignedIn);
trigger_initial_sync = true;
tracing::info!(target: "ely::sync", email = %email, "email OTP sign-in succeeded");
auth_changed = true;
}
SyncStateUpdate::AuthError { email, message } => {
self.auth_flow_phase = auth::AuthFlowPhase::Error { email, message };
auth_changed = true;
}
}
}
if let (Some(state), ShellState::Ready(core)) = (latest_connection, &mut self.state) {
core.set_sync_connection_state(state);
}
if trigger_initial_sync {
self.trigger_cloud_sync_upload();
}
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>) { 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(">");
@@ -340,6 +271,7 @@ impl ElyShell {
&& core.select_tab(tab_id).is_ok() && core.select_tab(tab_id).is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -349,6 +281,7 @@ impl ElyShell {
&& core.select_space(space_id).is_ok() && core.select_space(space_id).is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -363,6 +296,7 @@ impl ElyShell {
&& core.select_profile(profile_id).is_ok() && core.select_profile(profile_id).is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -372,6 +306,7 @@ impl ElyShell {
&& core.select_next_tab().is_ok() && core.select_next_tab().is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -381,6 +316,7 @@ impl ElyShell {
&& core.select_previous_tab().is_ok() && core.select_previous_tab().is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -390,6 +326,7 @@ impl ElyShell {
&& core.close_active_tab().is_ok() && core.close_active_tab().is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -399,6 +336,7 @@ impl ElyShell {
&& core.restore_last_archived_tab().is_ok() && core.restore_last_archived_tab().is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -413,6 +351,7 @@ impl ElyShell {
&& core.restore_archived_tab(tab_id).is_ok() && core.restore_archived_tab(tab_id).is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -421,6 +360,7 @@ impl ElyShell {
if let ShellState::Ready(core) = &mut self.state if let ShellState::Ready(core) = &mut self.state
&& core.toggle_active_tab_favorite().is_ok() && core.toggle_active_tab_favorite().is_ok()
{ {
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -429,6 +369,7 @@ impl ElyShell {
if let ShellState::Ready(core) = &mut self.state if let ShellState::Ready(core) = &mut self.state
&& core.toggle_active_tab_pinned().is_ok() && core.toggle_active_tab_pinned().is_ok()
{ {
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -437,6 +378,7 @@ impl ElyShell {
if let ShellState::Ready(core) = &mut self.state if let ShellState::Ready(core) = &mut self.state
&& core.zoom_active_tab_in().is_ok() && core.zoom_active_tab_in().is_ok()
{ {
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -445,6 +387,7 @@ impl ElyShell {
if let ShellState::Ready(core) = &mut self.state if let ShellState::Ready(core) = &mut self.state
&& core.zoom_active_tab_out().is_ok() && core.zoom_active_tab_out().is_ok()
{ {
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -453,6 +396,7 @@ impl ElyShell {
if let ShellState::Ready(core) = &mut self.state if let ShellState::Ready(core) = &mut self.state
&& core.reset_active_tab_zoom().is_ok() && core.reset_active_tab_zoom().is_ok()
{ {
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -467,7 +411,7 @@ fn start_external_web_surface_timer(cx: &mut Context<ElyShell>) {
}; };
Timer::after(delay).await; Timer::after(delay).await;
let result = shell.update(cx, |shell, cx| { let result = shell.update(cx, |shell, cx| {
if shell.tick_external_web_surfaces() { if shell.tick_external_web_surfaces(cx) {
cx.notify(); cx.notify();
} }
}); });
+5
View File
@@ -18,6 +18,7 @@ impl ElyShell {
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.focus_address_bar(window, cx); self.focus_address_bar(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -74,6 +75,7 @@ impl ElyShell {
core.open_tab(url); core.open_tab(url);
} }
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -83,6 +85,7 @@ impl ElyShell {
&& core.navigate_active_tab_back().is_ok_and(|changed| changed) && core.navigate_active_tab_back().is_ok_and(|changed| changed)
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -96,6 +99,7 @@ impl ElyShell {
&& core.navigate_active_tab_forward().is_ok_and(|changed| changed) && core.navigate_active_tab_forward().is_ok_and(|changed| changed)
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -108,6 +112,7 @@ impl ElyShell {
core.open_tab(url); core.open_tab(url);
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.focus_address_bar(window, cx); self.focus_address_bar(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
+22 -11
View File
@@ -193,6 +193,7 @@ impl ElyShell {
if let ShellState::Ready(core) = &mut self.state if let ShellState::Ready(core) = &mut self.state
&& core.set_profile_sync_policy(profile_id, sync_policy).is_ok() && core.set_profile_sync_policy(profile_id, sync_policy).is_ok()
{ {
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -200,6 +201,7 @@ impl ElyShell {
pub(super) fn reset_profile_sync_settings(&mut self, cx: &mut Context<Self>) { pub(super) fn reset_profile_sync_settings(&mut self, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state { if let ShellState::Ready(core) = &mut self.state {
core.reset_profile_sync_settings(); core.reset_profile_sync_settings();
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -231,6 +233,7 @@ impl ElyShell {
) { ) {
if let ShellState::Ready(core) = &mut self.state { if let ShellState::Ready(core) = &mut self.state {
core.set_sync_object_policy(kind, policy); core.set_sync_object_policy(kind, policy);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -238,6 +241,7 @@ impl ElyShell {
pub(super) fn reset_sync_settings(&mut self, cx: &mut Context<Self>) { pub(super) fn reset_sync_settings(&mut self, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state { if let ShellState::Ready(core) = &mut self.state {
core.reset_sync_settings(); core.reset_sync_settings();
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -251,6 +255,13 @@ impl ElyShell {
} }
fn trigger_cloud_sync_upload_with_clock_floor(&mut self, logical_clock_floor: Option<u64>) { fn trigger_cloud_sync_upload_with_clock_floor(&mut self, logical_clock_floor: Option<u64>) {
if self.sync_upload_in_flight {
self.sync_upload_scheduled = false;
self.queue_cloud_sync_upload(logical_clock_floor);
return;
}
self.sync_upload_scheduled = false;
let ShellState::Ready(core) = &self.state else { let ShellState::Ready(core) = &self.state else {
return; return;
}; };
@@ -278,19 +289,19 @@ impl ElyShell {
let tx = self.sync_inbox_tx.clone(); let tx = self.sync_inbox_tx.clone();
let thread_name = let thread_name =
if logical_clock_floor.is_some() { "ely-sync-merge-upload" } else { "ely-sync-upload" }; if logical_clock_floor.is_some() { "ely-sync-merge-upload" } else { "ely-sync-upload" };
std::thread::Builder::new() self.sync_upload_in_flight = true;
.name(thread_name.to_string()) if let Err(error) =
.spawn(move || { std::thread::Builder::new().name(thread_name.to_string()).spawn(move || {
run_sync_upload(profile_dir, device_name, bytes, logical_clock_floor, tx) run_sync_upload(profile_dir, device_name, bytes, logical_clock_floor, tx)
}) })
.map(|_| ()) {
.unwrap_or_else(|error| { self.sync_upload_in_flight = false;
tracing::warn!( tracing::warn!(
target: "ely::sync", target: "ely::sync",
error = %error, error = %error,
"failed to spawn ely-sync-upload thread", "failed to spawn ely-sync-upload thread",
); );
}); }
} }
pub(super) fn set_update_policy( pub(super) fn set_update_policy(
+168 -2
View File
@@ -1,6 +1,11 @@
use std::path::Path; use std::{path::Path, time::Duration};
use super::{ElyShell, ShellState}; use ely_domain::SyncConnectionState;
use gpui::{Context, Timer};
use super::{ElyShell, ShellState, auth};
const CLOUD_SYNC_UPLOAD_DEBOUNCE: Duration = Duration::from_millis(750);
/// Messages the off-thread sync workers push back to the shell so /// Messages the off-thread sync workers push back to the shell so
/// `SyncConnectionState` on `BrowserCore` and the in-flight auth /// `SyncConnectionState` on `BrowserCore` and the in-flight auth
@@ -34,6 +39,76 @@ pub(crate) const fn sync_platform_label() -> &'static str {
} }
impl ElyShell { impl ElyShell {
pub(crate) fn schedule_cloud_sync_upload(&mut self, cx: &mut Context<Self>) {
if !self.can_schedule_cloud_sync_upload() {
return;
}
if self.sync_upload_in_flight {
self.queue_cloud_sync_upload(None);
return;
}
if self.sync_upload_scheduled {
return;
}
self.sync_upload_scheduled = true;
cx.spawn(async move |shell, cx| {
Timer::after(CLOUD_SYNC_UPLOAD_DEBOUNCE).await;
let _ = shell.update(cx, |shell, _| {
if shell.sync_upload_scheduled {
shell.sync_upload_scheduled = false;
shell.trigger_cloud_sync_upload();
}
});
})
.detach();
}
pub(super) fn queue_cloud_sync_upload(&mut self, logical_clock_floor: Option<u64>) {
self.sync_upload_pending = true;
if let Some(floor) = logical_clock_floor {
self.sync_upload_pending_logical_clock_floor = Some(
self.sync_upload_pending_logical_clock_floor
.map_or(floor, |current| current.max(floor)),
);
}
}
fn trigger_pending_cloud_sync_upload(&mut self) -> bool {
if !self.sync_upload_pending {
return false;
}
self.sync_upload_pending = false;
let logical_clock_floor = self.sync_upload_pending_logical_clock_floor.take();
match logical_clock_floor {
Some(floor) => self.trigger_cloud_sync_upload_after_remote(floor),
None => self.trigger_cloud_sync_upload(),
}
true
}
fn clear_pending_cloud_sync_upload(&mut self) {
self.sync_upload_pending = false;
self.sync_upload_pending_logical_clock_floor = None;
}
fn can_schedule_cloud_sync_upload(&self) -> bool {
let ShellState::Ready(core) = &self.state else {
return false;
};
let Some(snapshot) = core.snapshot().ok() else {
return false;
};
matches!(
snapshot.sync_status.connection(),
SyncConnectionState::SignedIn
| SyncConnectionState::AwaitingDeviceApproval
| SyncConnectionState::SyncReady { .. }
| SyncConnectionState::SyncError { .. }
)
}
/// Inspect the on-disk bearer token and seed `SyncConnectionState` /// Inspect the on-disk bearer token and seed `SyncConnectionState`
/// so the Sync settings page reads the startup state on first render. /// so the Sync settings page reads the startup state on first render.
pub(super) fn probe_initial_sync_state(&mut self) -> bool { pub(super) fn probe_initial_sync_state(&mut self) -> bool {
@@ -62,6 +137,97 @@ impl ElyShell {
core.set_sync_connection_state(state); core.set_sync_connection_state(state);
bearer_present bearer_present
} }
/// 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 mut latest_connection: Option<SyncConnectionState> = None;
let mut auth_changed = false;
let mut trigger_initial_sync = false;
let mut trigger_merged_upload = None;
let mut upload_finished = false;
while let Ok(update) = self.sync_inbox_rx.try_recv() {
match update {
SyncStateUpdate::SignedOut => {
latest_connection = Some(SyncConnectionState::SignedOut);
upload_finished = true;
}
SyncStateUpdate::AwaitingDeviceApproval => {
latest_connection = Some(SyncConnectionState::AwaitingDeviceApproval);
upload_finished = true;
}
SyncStateUpdate::RemoteSnapshot { bytes, logical_clock } => {
upload_finished = true;
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(SyncConnectionState::SyncError {
message: error.to_string(),
});
}
}
}
}
SyncStateUpdate::SyncReady { last_synced_at_secs } => {
latest_connection =
Some(SyncConnectionState::SyncReady { last_synced_at_secs });
upload_finished = true;
}
SyncStateUpdate::SyncError { message } => {
latest_connection = Some(SyncConnectionState::SyncError { message });
upload_finished = true;
}
SyncStateUpdate::AuthOtpSent { email } => {
self.auth_flow_phase = auth::AuthFlowPhase::AwaitingOtp { email };
auth_changed = true;
}
SyncStateUpdate::AuthSucceeded { email } => {
self.auth_flow_phase = auth::AuthFlowPhase::Idle;
latest_connection = Some(SyncConnectionState::SignedIn);
trigger_initial_sync = true;
tracing::info!(target: "ely::sync", email = %email, "email OTP sign-in succeeded");
auth_changed = true;
}
SyncStateUpdate::AuthError { email, message } => {
self.auth_flow_phase = auth::AuthFlowPhase::Error { email, message };
auth_changed = true;
}
}
}
let connection_changed = latest_connection.is_some();
if let (Some(state), ShellState::Ready(core)) = (latest_connection, &mut self.state) {
core.set_sync_connection_state(state);
}
if upload_finished {
self.sync_upload_in_flight = false;
}
if trigger_initial_sync {
self.trigger_cloud_sync_upload();
}
let merged_upload_requested = trigger_merged_upload.is_some();
if let Some(logical_clock_floor) = trigger_merged_upload {
self.clear_pending_cloud_sync_upload();
self.trigger_cloud_sync_upload_after_remote(logical_clock_floor);
} else if upload_finished {
self.trigger_pending_cloud_sync_upload();
}
auth_changed || trigger_initial_sync || merged_upload_requested || connection_changed
}
} }
fn bearer_token_file_present(path: &Path) -> bool { fn bearer_token_file_present(path: &Path) -> bool {
@@ -15,6 +15,7 @@ impl ElyShell {
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.focus_address_bar(window, cx); self.focus_address_bar(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -30,6 +31,7 @@ impl ElyShell {
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.focus_address_bar(window, cx); self.focus_address_bar(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -50,6 +52,7 @@ impl ElyShell {
&& core.close_tab(tab_id).is_ok() && core.close_tab(tab_id).is_ok()
{ {
self.sync_address_input(window, cx); self.sync_address_input(window, cx);
self.schedule_cloud_sync_upload(cx);
cx.notify(); cx.notify();
} }
} }
@@ -45,7 +45,7 @@ impl ElyShell {
} }
} }
pub(super) fn tick_external_web_surfaces(&mut self) -> bool { pub(super) fn tick_external_web_surfaces(&mut self, cx: &mut Context<Self>) -> bool {
let (visible_tab_ids, open_tab_ids, visible_tabs) = match &self.state { let (visible_tab_ids, open_tab_ids, visible_tabs) = match &self.state {
super::ShellState::Ready(core) => { super::ShellState::Ready(core) => {
let visible_tabs = core.visible_content_tabs().unwrap_or_else(|_| Vec::new()); let visible_tabs = core.visible_content_tabs().unwrap_or_else(|_| Vec::new());
@@ -65,6 +65,9 @@ impl ElyShell {
metadata_changed |= self.apply_web_surface_page_metadata(metadata); metadata_changed |= self.apply_web_surface_page_metadata(metadata);
} }
let sync_changed = self.drain_sync_updates(); let sync_changed = self.drain_sync_updates();
if url_changed || metadata_changed {
self.schedule_cloud_sync_upload(cx);
}
result.changed || url_changed || metadata_changed || sync_changed result.changed || url_changed || metadata_changed || sync_changed
} }
@@ -79,7 +82,7 @@ impl ElyShell {
} }
fn flush_external_web_surface_tick(&mut self, cx: &mut Context<Self>) { fn flush_external_web_surface_tick(&mut self, cx: &mut Context<Self>) {
if self.tick_external_web_surfaces() { if self.tick_external_web_surfaces(cx) {
cx.notify(); cx.notify();
} }
} }