diff --git a/crates/op-host-web/src/canvaskit.rs b/crates/op-host-web/src/canvaskit.rs index bf660ec6b..0b5ee6ee7 100644 --- a/crates/op-host-web/src/canvaskit.rs +++ b/crates/op-host-web/src/canvaskit.rs @@ -952,6 +952,14 @@ pub async fn mount_ck(canvas_id: String) -> Result<(), JsValue> { a11y, ime, })); + // Reset the credential-sync queue BEFORE the first repaint and before the + // rAF coalescer is installed below. `repaint` calls + // `web_credential_sync::credential_changed` whenever a credential edit + // lands, so the reset must precede any repaint wiring — otherwise an early + // repaint could queue a change that a later reset silently wipes. This is a + // pure state reset (no daemon request), so it is safe ahead of the bridge + // init gate; the daemon-facing policy fetch (`start`) still waits for it. + crate::web_credential_sync::reset(); { let mut b = inner.borrow_mut(); let _ = b.resize_to_window(&window)?; diff --git a/crates/op-host-web/src/web_credential_sync.rs b/crates/op-host-web/src/web_credential_sync.rs index c42cc3601..b0faca4ae 100644 --- a/crates/op-host-web/src/web_credential_sync.rs +++ b/crates/op-host-web/src/web_credential_sync.rs @@ -25,6 +25,10 @@ enum SyncAction { } impl CredentialSyncState { + // Combined reset + policy-check. The mount now drives these two phases + // separately (`reset` early, `start` after the bridge gate), so this stays + // as the state-machine's tested "full start" shape. + #[cfg(test)] fn start(&mut self) -> SyncAction { *self = Self::default(); self.begin_policy_check() @@ -138,8 +142,20 @@ thread_local! { static SYNC_STATE: RefCell = RefCell::new(CredentialSyncState::default()); } +/// Clear the transient credential-sync queue. Split out of `start` so the +/// mount can reset state BEFORE repaint callbacks are wired: `repaint` calls +/// `credential_changed` when a credential edit lands, so an early repaint must +/// not queue a change that a later reset silently wipes. This issues no daemon +/// request, so it is safe to run ahead of the postMessage bridge init gate. +pub(crate) fn reset() { + SYNC_STATE.with(|state| *state.borrow_mut() = CredentialSyncState::default()); +} + +/// Begin credential-policy discovery against the daemon. This issues a daemon +/// request, so the mount calls it only AFTER the bridge init gate (managed mode +/// needs the auth token on the wire). State must already be cleared via `reset`. pub(crate) fn start() { - let action = SYNC_STATE.with(|state| state.borrow_mut().start()); + let action = SYNC_STATE.with(|state| state.borrow_mut().begin_policy_check()); dispatch(action); } diff --git a/crates/op-host-web/tests/web_mount_event_wiring.rs b/crates/op-host-web/tests/web_mount_event_wiring.rs index 463c43050..f6cbe4467 100644 --- a/crates/op-host-web/tests/web_mount_event_wiring.rs +++ b/crates/op-host-web/tests/web_mount_event_wiring.rs @@ -117,11 +117,11 @@ fn canvaskit_repaint_persists_local_settings_and_syncs_only_credential_changes() let install = source .find("repaint_coalescer::install") .expect("repaint coalescer installation"); - let sync_start = source - .find("web_credential_sync::start") - .expect("credential policy discovery"); + let sync_reset = source + .find("web_credential_sync::reset") + .expect("credential sync state reset"); assert!( - sync_start < install, + sync_reset < install, "credential sync state is reset before repaint callbacks can queue changes" ); }