fix(sw): postMessage all open tabs on activate to break stale-JS loop
After clients.claim(), the new SW sends SW_ACTIVATE_RELOAD to every open window tab. +layout.svelte now listens for this alongside the existing controllerchange listener. The had_controller guard prevents a spurious reload on fresh page loads where no SW was controlling the tab. WHY: controllerchange only fires in JS that already has the listener (added 2026-06-22). Tabs stuck on builds from before that date (e.g. October 2025 users) never registered it, so the SW update under them was silent. The postMessage path reaches those tabs directly. Users who are already on new JS get the reload from controllerchange; users on old JS get it from the message event once they receive this new SW. Note: October 2025 users still need a manual Full Reset on their first visit after this deploy (old JS has no message listener). After that one reset they are permanently self-healing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -356,6 +356,21 @@ $effect(() => {
|
||||
navigator.serviceWorker.addEventListener('controllerchange', on_controller_change);
|
||||
}
|
||||
|
||||
// SW activate → reload bridge (parallel path to controllerchange above).
|
||||
// WHY: controllerchange only fires in JS that has the listener registered.
|
||||
// Tabs stuck on very old JS (pre-June-2026 builds) never set up that listener,
|
||||
// so the new SW's postMessage from its activate event is the only way to reach
|
||||
// them. The same had_controller guard prevents a spurious reload on fresh page
|
||||
// loads where no SW was previously controlling the tab.
|
||||
const on_sw_message = (event: MessageEvent) => {
|
||||
if (event.data?.type === 'SW_ACTIVATE_RELOAD' && had_controller) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', on_sw_message);
|
||||
}
|
||||
|
||||
// Initialise PWA install prompt listener (captures beforeinstallprompt early).
|
||||
pwa_install.init();
|
||||
|
||||
@@ -443,6 +458,7 @@ $effect(() => {
|
||||
window.removeEventListener('message', handler);
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.removeEventListener('controllerchange', on_controller_change);
|
||||
navigator.serviceWorker.removeEventListener('message', on_sw_message);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -33,18 +33,30 @@ self.addEventListener('install', (event) => {
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// Remove previous cached data from other screens
|
||||
async function deleteOldCaches() {
|
||||
async function activate() {
|
||||
// Remove previous cached data from other deployments.
|
||||
for (const key of await caches.keys()) {
|
||||
if (key !== CACHE) await caches.delete(key);
|
||||
}
|
||||
|
||||
// Take control of all open tabs immediately after activation so they use
|
||||
// the new service worker (and new cached assets) without needing a reload.
|
||||
await self.clients.claim();
|
||||
|
||||
// Tell every open window tab to reload so it runs the new JS bundle.
|
||||
// WHY: clients.claim() makes this SW the active controller but does not
|
||||
// swap the JS already executing in those tabs. The `controllerchange`
|
||||
// listener in +layout.svelte handles tabs already running new JS, but tabs
|
||||
// stuck on old JS (e.g. from a build months ago, before that listener was
|
||||
// added) never registered it. This postMessage reaches those tabs directly.
|
||||
// Tabs that ignore it are still on old code and need a manual Full Reset.
|
||||
const client_list = await self.clients.matchAll({ type: 'window' });
|
||||
for (const client of client_list) {
|
||||
client.postMessage({ type: 'SW_ACTIVATE_RELOAD' });
|
||||
}
|
||||
}
|
||||
|
||||
event.waitUntil(deleteOldCaches());
|
||||
|
||||
// Take control of all open tabs immediately after activation so they use
|
||||
// the new service worker (and new cached assets) without needing a reload.
|
||||
self.clients.claim();
|
||||
event.waitUntil(activate());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
|
||||
Reference in New Issue
Block a user