Skip to content

fix(member): mountEdge recovers a page its ServiceWorker does not control (hard reload hang) - #5

Merged
mkotelnikov merged 3 commits into
mainfrom
fix/edge-uncontrolled-page
Sep 18, 2026
Merged

mkotelnikov merged 3 commits into
mainfrom
fix/edge-uncontrolled-page

Conversation

@mkotelnikov

Copy link
Copy Markdown
Contributor

Problem

llm-chat's mesh.html hangs forever at "Joining… (mounting-edge)" after a hard reload (Ctrl+Shift+R). A hard reload loads the page bypassing its ServiceWorker; the worker is already active and ran clients.claim() long ago, so no controllerchange ever fires, and SwHttpAdapter.start() (webrun-http-browser) waits for navigator.serviceWorker.controller with no timeout.

Fix (page side, works with the published webrun-http-browser 0.4.2)

mountEdge now calls ensureControlled (packages/httpeers-member/src/edge-control.ts) before starting the adapter:

  • controlled, or no active worker yet (first visit) → carry on as before;
  • uncontrolled under an active worker → reload once, guarded by a timestamped sessionStorage flag so it cannot loop;
  • still uncontrolled with the guard spent, or sessionStorage unusable → throw UncontrolledPageError ("…close the tab and reopen it"), which the join widget shows as "Could not join".

The adapter's wait is also bounded (controlTimeoutMs, default 30 s), so an unforeseen case fails with a message instead of hanging.

Why a reload and not a claim: I measured that clients.claim() from the worker does take over a hard-reloaded page in Chromium, but the stock sw-worker only claims on activate and gives a page no way to ask again (a byte-identical register()/update() installs nothing). An ordinary reload comes back controlled in both Chromium and Firefox.

Tests

  • tests/edge-control.test.ts: 15 unit tests for the decision, including "a sequence of loads can never reload twice in a row" and storage that is missing or throws.
  • packages/httpeers-browser-conformance/scripts/edge-reload.mjs (pnpm run test:reload): real Chromium and Firefox. First visit, normal reload, hard reload, a second tab, a spent guard (must show the error), then a normal reload. The hard reload is CDP Page.reload({ignoreCache:true}) in Chromium and location.reload(true) in Firefox; Playwright's keyboard can't press the browser's own Ctrl+Shift+R. Before the fix: 6 FAIL (hard reload stays at "mounting" in both browsers). After: all pass.
  • The built llm-chat against a local relay + hub (throwaway stack) in Chromium and Firefox. First visit (join), reload, hard reload, second tab and first tab all reach Connected (direct). The pre-fix bundle, which is the one currently live, stays at Joining… (mounting-edge) after the hard reload in both browsers.
  • Workspace lint:check, build, typecheck and test are green, and so is the browser-conformance test:browser suite.

🤖 Generated with Claude Code

mkotelnikov and others added 3 commits September 18, 2026 16:51
…Firefox

scripts/edge-reload.mjs (pnpm run test:reload) drives a tiny page that mounts a
dispatch on the edge through first visit, normal reload, hard reload, a second
tab and a hard reload with the reload guard spent. A hard reload is CDP
Page.reload({ignoreCache}) in Chromium and location.reload(true) in Firefox;
Playwright's keyboard cannot press the browser's own Ctrl+Shift+R.

Against the current mountEdge the hard reload hangs at "mounting" in both
browsers -- the llm-chat mesh.html bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…trol

A hard reload loads the page bypassing its ServiceWorker while the worker is
already active and claimed its clients long ago, so no controllerchange ever
fires and SwHttpAdapter.start() waited forever: llm-chat's mesh.html stuck at
"Joining... (mounting-edge)".

Before starting the adapter, mountEdge now reloads such a page once (an ordinary
reload comes back controlled in Chromium and Firefox; the stock worker offers no
way to be asked for another claim), guarded by a timestamped sessionStorage flag
so it cannot loop, and throws UncontrolledPageError ("close the tab and reopen
it") when the guard is spent or unusable. The whole wait is bounded
(controlTimeoutMs, default 30 s) so an unforeseen case fails loudly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mkotelnikov
mkotelnikov merged commit 3998666 into main Sep 18, 2026
3 checks passed
@mkotelnikov
mkotelnikov deleted the fix/edge-uncontrolled-page branch September 20, 2026 07:50
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