diff --git a/public/sw.js b/public/sw.js index d56f5047..af2783a6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -57,14 +57,21 @@ self.addEventListener('notificationclick', (event) => { // --- Activate immediately --- // -// On activate, nuke every Cache Storage entry. A previous version of Agora -// deployed a precaching service worker (Workbox-style) that's still serving -// stale HTML/JS to returning users on the same origin. Wiping caches here -// means the first request a returning user makes after this SW takes over -// will hit the network and get the new build. +// On activate: +// 1. Wipe every Cache Storage entry. A previous version of Agora deployed +// a precaching service worker (Workbox-style) that's still serving stale +// HTML/JS to returning users on this origin. Clearing caches means future +// requests bypass anything the old SW left behind. +// 2. Take control of all open clients via clients.claim(). +// 3. Force each controlled tab to navigate to its own URL. clients.claim() +// only changes which SW handles future fetches — it does not re-render +// pages that already finished loading. Without the explicit navigate, +// the user is stuck on the old rendered bundle until they manually +// close and reopen the tab. Since this SW has no fetch handler, the +// navigation falls through to the network and gets the new build. // -// This SW itself does not intercept fetches (no 'fetch' handler), so it -// never repopulates a cache — only push notifications are handled below. +// This SW has no 'fetch' handler, so it never repopulates a cache — push +// notifications are the only thing it intercepts. self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (event) => { @@ -73,6 +80,18 @@ self.addEventListener('activate', (event) => { const keys = await caches.keys(); await Promise.all(keys.map((key) => caches.delete(key))); await self.clients.claim(); + + // Soft-reload every open same-origin tab so it picks up the fresh + // index.html + hashed bundle from the network. WindowClient.navigate() + // is same-origin-only by spec, which is exactly what we want. + const windowClients = await self.clients.matchAll({ type: 'window' }); + await Promise.all( + windowClients.map((client) => + 'navigate' in client + ? client.navigate(client.url).catch(() => {}) + : Promise.resolve(), + ), + ); })(), ); }); diff --git a/src/main.tsx b/src/main.tsx index a97a8038..f7f06e48 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -87,21 +87,26 @@ requestAnimationFrame(() => { document.getElementById('preloader')?.remove(); }); -// ─── Service worker takeover (web only) ────────────────────────────────────── +// ─── Service worker registration (web only) ───────────────────────────────── // -// A previous version of Agora deployed at this origin shipped a precaching -// service worker that's still serving stale HTML/JS to returning users. The -// SW we ship now (public/sw.js) has no fetch handler and nukes every cache -// on activate, so as soon as the browser installs it, returning users start -// getting fresh builds again. +// Register /sw.js unconditionally on web. The SW itself (public/sw.js) has +// no fetch handler and wipes caches on activate — see that file for the +// stale-SW eviction story. // -// usePushNotifications() also registers /sw.js, but only when the user -// visits the notification settings page — that's not good enough to evict -// the old SW for everyone. We register here on every web page load so the -// new SW takes over for all visitors, regardless of whether they use push. +// This registration does NOT fix the stale-SW problem on its own: returning +// users with the old precache SW never run any of our new JS, because the +// old SW serves the old bundle from cache. The browser evicts the old SW +// out-of-band by re-fetching /sw.js on its own update schedule, and the +// new SW's activate handler does the actual cache wipe + tab reload. // -// Native (Capacitor) skips this — the bundled web assets are served from -// the local filesystem and there's no stale SW on the origin to evict. +// What this registration buys us is forward-looking insurance: it ensures +// every web visitor has a SW in place, so the next time we need to ship an +// emergency cache bust via /sw.js, there's something for the browser to +// update. Without it, only push-enabled users (who hit +// usePushNotifications) would ever have a SW registered. +// +// Native (Capacitor) skips this — assets are served from the local +// filesystem, no SW involved. if (!Capacitor.isNativePlatform() && 'serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch((err) => {