You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found during PR #613's review (fixing #585 — the SW controllerchange handler no longer reloads on an ordinary first-ever install). A narrower, compound-timing residual remains that even a fully-corrected live Service Worker API check cannot close.
The scenario
register-sw.ts's registerServiceWorker() runs via window.addEventListener('load', registerServiceWorker) — i.e. at the load event, not at navigation time. Its hadControllerAtLoad snapshot (!!navigator.serviceWorker.controller) is read at that point.
If Tab A is the first tab to ever visit an origin and its install→activate→clients.claim() sequence completes while Tab B (also loading for the very first time) is still parsing/loading its own page, clients.claim() sets Tab B's .controller property immediately — before Tab B's own load event fires, and therefore before Tab B's own registerServiceWorker() (and its controllerchange listener) ever runs.
Two independent consequences:
Tab B's controllerchange event (from Tab A's claim) has no listener yet when it's dispatched — it is not replayed, so it's silently missed (same class as the listener-ordering gap PR fix(pwa): skip the controllerchange reload on a first-ever install (#585) #613 already closed elsewhere, but this instance happens beforeregisterServiceWorker() even starts).
When Tab B's load handler eventually runs, hadControllerAtLoad reads true — because .controller was already set by Tab A's claim — even though this is still, in reality, the very first activation this origin has ever had. Tab B's classification logic (correctly, given only this signal) concludes "this is a genuine update," so if Tab B's page code ever reacts to that state, it treats what was actually the shared first-ever install as if it were a returning-visitor update.
Closing this fully requires a signal that doesn't depend on live, per-tab Service Worker API timing at all — e.g. a persistent flag (localStorage, written the moment any tab first observes a genuine claim) checked synchronously before any Service Worker API call. That is a materially different design from the live-registration-state approach #613 already converged on through several rounds of narrower fixes, and disproportionate to add speculatively without a concrete reproduction proving it matters in practice.
Impact
Extremely narrow: requires two tabs open on the very first-ever visit to the origin, with one tab's own page-load time exceeding the other tab's complete install+activate+claim pipeline (a real network round-trip for precaching). Worst case is a single unwanted reload for the second tab on that one shared first visit — not a recurring or update-blocking defect, unlike the original #585 bug this PR fixes.
Suggested direction (not designed here)
A persistent, storage-backed "this origin has activated a service worker before" flag, written by the first tab to observe any real controllerchange, checked synchronously (no Service Worker API await) before falling back to the current live-state heuristic. Needs its own design pass, not a rushed addition to #613.
Context
Found during PR #613's review (fixing #585 — the SW controllerchange handler no longer reloads on an ordinary first-ever install). A narrower, compound-timing residual remains that even a fully-corrected live Service Worker API check cannot close.
The scenario
register-sw.ts'sregisterServiceWorker()runs viawindow.addEventListener('load', registerServiceWorker)— i.e. at theloadevent, not at navigation time. ItshadControllerAtLoadsnapshot (!!navigator.serviceWorker.controller) is read at that point.If Tab A is the first tab to ever visit an origin and its install→activate→
clients.claim()sequence completes while Tab B (also loading for the very first time) is still parsing/loading its own page,clients.claim()sets Tab B's.controllerproperty immediately — before Tab B's ownloadevent fires, and therefore before Tab B's ownregisterServiceWorker()(and itscontrollerchangelistener) ever runs.Two independent consequences:
controllerchangeevent (from Tab A's claim) has no listener yet when it's dispatched — it is not replayed, so it's silently missed (same class as the listener-ordering gap PR fix(pwa): skip the controllerchange reload on a first-ever install (#585) #613 already closed elsewhere, but this instance happens beforeregisterServiceWorker()even starts).loadhandler eventually runs,hadControllerAtLoadreadstrue— because.controllerwas already set by Tab A's claim — even though this is still, in reality, the very first activation this origin has ever had. Tab B's classification logic (correctly, given only this signal) concludes "this is a genuine update," so if Tab B's page code ever reacts to that state, it treats what was actually the shared first-ever install as if it were a returning-visitor update.Why not fixed in #613
Closing this fully requires a signal that doesn't depend on live, per-tab Service Worker API timing at all — e.g. a persistent flag (localStorage, written the moment any tab first observes a genuine claim) checked synchronously before any Service Worker API call. That is a materially different design from the live-registration-state approach #613 already converged on through several rounds of narrower fixes, and disproportionate to add speculatively without a concrete reproduction proving it matters in practice.
Impact
Extremely narrow: requires two tabs open on the very first-ever visit to the origin, with one tab's own page-load time exceeding the other tab's complete install+activate+claim pipeline (a real network round-trip for precaching). Worst case is a single unwanted reload for the second tab on that one shared first visit — not a recurring or update-blocking defect, unlike the original #585 bug this PR fixes.
Suggested direction (not designed here)
A persistent, storage-backed "this origin has activated a service worker before" flag, written by the first tab to observe any real
controllerchange, checked synchronously (no Service Worker APIawait) before falling back to the current live-state heuristic. Needs its own design pass, not a rushed addition to #613.