From cd465f9899208ea80c6f990222247cb1ffc3086f Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Fri, 17 Jul 2026 02:00:02 +0800 Subject: [PATCH] fix(web): keep credential sync reset ahead of repaint wiring in mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 7 reordered mount_ck so the daemon-dependent web_credential_sync::start() runs only after the postMessage bridge init gate. But start() bundled two things: a pure state reset (*self = default) and a daemon policy fetch. Moving the whole call past the gate also moved the reset past repaint_coalescer::install, so an early repaint (e.g. during the 2s await_init in an iframe) could queue a credential change via credential_changed() that the later reset silently wiped — the pre-existing invariant guarded by canvaskit_repaint_persists_local_settings_and_syncs_only_credential_changes. Split the two phases: add web_credential_sync::reset() (pure state clear, no daemon request) called BEFORE the first repaint and the rAF coalescer install, and keep start() (now begin_policy_check only, the daemon fetch) after the bridge gate. Both invariants hold: the reset precedes repaint wiring, and no daemon request fires before the gate. The daemon behavior is unchanged in the normal flow (reset() then begin_policy_check equals the old start()), and strictly better in the race edge — a change queued between reset and start is now preserved instead of wiped. Test updated to assert the reset (not the now-daemon-facing start) precedes install; the semantic invariant it protects is unchanged and is now expressed against the actual code order. start()'s ordering after the gate stays covered by canvaskit_mount_queues_an_initial_snapshot_only_when_local_credentials_exist. --- crates/op-host-web/src/canvaskit.rs | 8 ++++++++ crates/op-host-web/src/web_credential_sync.rs | 18 +++++++++++++++++- .../tests/web_mount_event_wiring.rs | 8 ++++---- 3 files changed, 29 insertions(+), 5 deletions(-) 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" ); }