Skip to content

fix(sw): whoever promotes the waiting worker owns the reload - #199

Closed
chaxus wants to merge 2 commits into
mainfrom
sw-promote-reload
Closed

fix(sw): whoever promotes the waiting worker owns the reload#199
chaxus wants to merge 2 commits into
mainfrom
sw-promote-reload

Conversation

@chaxus

@chaxus chaxus commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

CI kept failing the @serial silent-update case with a 90s timeout, and the
screenshot was a blank white page. The trace says why: the editor iframe's own
document request came back -1, and nothing retries it.

Activating a worker terminates the outgoing one, and every fetch it still had
in flight fails -- including that iframe navigation. That is survivable only
because a reload follows. The reload did not follow.

promoteWaitingWorker runs the moment register() resolves, which on the editor
route is a few hundred milliseconds before the editor instance exists. It
therefore sees "no document open" and promotes. By the time controllerchange
arrives the document is open, shouldReloadOnControllerChange asks the same
question again, gets the other answer, and refuses -- with the page already
torn in half. wireServiceWorkerUpdates was dropping promoteWaitingWorker's
return value, so the page never knew it had asked for the swap itself.

It now reports it. index.ts keeps one flag for both promotion paths (the
ordinary update and the stale-build heal), renamed promotedFromThisTab, and a
tab that asked for the swap reloads regardless of what is open -- unsaved
edits still veto. Refusing the reload does not undo the swap; it only leaves
the reader on a blank editor.

Reverse-verified: stashing lib/sw-update.ts and index.ts turns all four new
unit cases red. sw-silent-update and sw-warm pass locally.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

CI kept failing the @serial silent-update case with a 90s timeout, and the
screenshot was a blank white page. The trace says why: the editor iframe's own
document request came back -1, and nothing retries it.

Activating a worker terminates the outgoing one, and every fetch it still had
in flight fails -- including that iframe navigation. That is survivable only
because a reload follows. The reload did not follow.

promoteWaitingWorker runs the moment register() resolves, which on the editor
route is a few hundred milliseconds before the editor instance exists. It
therefore sees "no document open" and promotes. By the time controllerchange
arrives the document is open, shouldReloadOnControllerChange asks the same
question again, gets the other answer, and refuses -- with the page already
torn in half. wireServiceWorkerUpdates was dropping promoteWaitingWorker's
return value, so the page never knew it had asked for the swap itself.

It now reports it. index.ts keeps one flag for both promotion paths (the
ordinary update and the stale-build heal), renamed promotedFromThisTab, and a
tab that asked for the swap reloads regardless of what is open -- unsaved
edits still veto. Refusing the reload does not undo the swap; it only leaves
the reader on a blank editor.

Reverse-verified: stashing lib/sw-update.ts and index.ts turns all four new
unit cases red. sw-silent-update and sw-warm pass locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chaxus
chaxus enabled auto-merge (rebase) August 23, 2026 07:24
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 23, 2026

Copy link
Copy Markdown

Deploying document with  Cloudflare Pages  Cloudflare Pages

Latest commit: fa8dd65
Status: ✅  Deploy successful!
Preview URL: https://4068b206.document-7hm.pages.dev
Branch Preview URL: https://sw-promote-reload.document-7hm.pages.dev

View logs

The first half of this change turned every promotion into a reload, which the
Cloudflare Pages E2E caught: autosave-recovery's reload case went red on the
shard and stayed red on retry, while the same shard was green on a branch
without it.

CLAUDE.md already says why. A worker waiting is not a new version: the
vendored editor registers a worker of its own into this scope from inside the
iframe, so the scope's script alternates and ours sits in `waiting` on almost
every editor load with no new build anywhere. "Refuse the reload when a
document is open" was doubling as the brake on that. Removing it made those
routine swaps reload the page every time.

So the other half: a page that is about to open a document does not promote at
all. hasOpenDocument() reads the store, which fills a few hundred milliseconds
after register() resolves -- the URL knows sooner, and ?new= / ?file= / ?src= /
?open= / ?saved= all say a document is coming. Embed mode counts too, and for a
stronger reason: the host can push a document at any moment, and the reload
would throw away something the host page owns.

A worker left waiting on those routes is the case the silent heal exists for,
and that path checks isUnseenBuild() first, so the vendor's worker shuffling
cannot fool it.

Reverse-verified: stashing lib/sw-update.ts turns the three new
documentIsExpected cases red. autosave-recovery (all three), sw-silent-update
and sw-warm pass locally -- the reload case being the one that was red with
only the first half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chaxus

chaxus commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #201, which carries these commits unchanged alongside the structured-data work.

@chaxus chaxus closed this Aug 23, 2026
auto-merge was automatically disabled August 23, 2026 07:45

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant