diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4dd60711..abca6601 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,12 +49,14 @@ deploy-web: - echo "$DEPLOY_SSH_KEY" | tr -d '\r' > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H "${DEPLOY_TARGET##*@}" >> ~/.ssh/known_hosts 2>/dev/null - # Two-phase rsync: upload hashed assets first, then index.html, so the - # site never serves an index.html that points at assets that haven't - # finished uploading. The destination ":/" is the rrsync jail root on - # venus, which maps to /var/www/agora.spot/. + # Two-phase rsync: upload hashed assets first, then index.html and sw.js, + # so the site never serves an index.html that points at assets that + # haven't finished uploading. sw.js is in the second pass for the same + # reason — it's a stable filename that all browsers re-fetch to check + # for updates, so we want it to land last. The destination ":/" is the + # rrsync jail root on venus, which maps to /var/www/agora.spot/. - rsync -av --exclude=/sw.js --exclude=/index.html -e "ssh -i ~/.ssh/id_ed25519" dist/ "${DEPLOY_TARGET}:/" - - rsync -av -e "ssh -i ~/.ssh/id_ed25519" dist/index.html "${DEPLOY_TARGET}:/" + - rsync -av -e "ssh -i ~/.ssh/id_ed25519" dist/index.html dist/sw.js "${DEPLOY_TARGET}:/" # Disabled: nsite deploy not needed right now; re-enable by restoring the # rules below to run on default branch (and ensure NSITE_NBUNKSEC is set). diff --git a/public/sw.js b/public/sw.js index 63e932e6..d56f5047 100644 --- a/public/sw.js +++ b/public/sw.js @@ -56,8 +56,23 @@ 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. +// +// This SW itself does not intercept fetches (no 'fetch' handler), so it +// never repopulates a cache — only push notifications are handled below. self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (event) => { - event.waitUntil(self.clients.claim()); + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all(keys.map((key) => caches.delete(key))); + await self.clients.claim(); + })(), + ); }); diff --git a/src/main.tsx b/src/main.tsx index 30c09784..a97a8038 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -86,3 +86,26 @@ createRoot(document.getElementById("root")!).render( requestAnimationFrame(() => { document.getElementById('preloader')?.remove(); }); + +// ─── Service worker takeover (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. +// +// 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. +// +// 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. +if (!Capacitor.isNativePlatform() && 'serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch((err) => { + console.warn('[sw] registration failed:', err); + }); + }); +}