fix(web): keep credential sync reset ahead of repaint wiring in mount

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.
This commit is contained in:
Kayshen-X 2026-07-17 02:00:02 +08:00
parent b6077ab09b
commit cd465f9899
3 changed files with 29 additions and 5 deletions

View file

@ -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)?;

View file

@ -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<CredentialSyncState> = 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);
}

View file

@ -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"
);
}