diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2ce7a5ec..5b72e485 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -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); } }; }); diff --git a/src/service-worker.js b/src/service-worker.js index e9be0988..52c84dac 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -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) => {