Auto-reload open tabs on SW activate so returning users see fresh build immediately
Regression-of: 5b8d2d5c
The previous SW eviction commit wiped caches and called clients.claim()
on activate, but that only changes which SW handles future fetches — it
does not re-render a tab that already finished loading the stale bundle.
In practice, returning users had to manually close and reopen the tab
before seeing the new build.
Fix: after clients.claim(), iterate self.clients.matchAll({ type: 'window' })
and call client.navigate(client.url) on each one. Since this SW has no
fetch handler, the navigation falls through to the network and the tab
re-renders against the fresh index.html + hashed bundle.
Caveats:
- Users mid-interaction (typing a post, scrolling) lose their unsaved
state. Acceptable trade — the alternative is they stay on a broken
cached bundle indefinitely.
- Fires exactly once per user (only on the install -> activate transition
for a byte-different /sw.js). No reload loop.
Also corrected the misleading comment on the main.tsx registration: that
registration is forward-looking insurance for future cache busts, not the
mechanism that evicts the old SW. The browser's own SW update check is
what re-fetches /sw.js out-of-band; our in-page JS never runs on a tab
the old precache SW is controlling.
This commit is contained in:
+26
-7
@@ -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(),
|
||||
),
|
||||
);
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
+17
-12
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user