Evict stale precache service worker from old Agora deployment

A previous version of Agora deployed at agora.spot shipped a precaching
service worker that is still controlling returning browsers and serving
them stale HTML/JS — they never see new deploys.

The fix has three parts:

1. public/sw.js — on activate, delete every Cache Storage entry the old
   SW left behind. This SW has no fetch handler, so once it takes over
   nothing re-populates the cache.

2. src/main.tsx — register /sw.js unconditionally on every web page load.
   Previously only usePushNotifications registered it, which meant users
   who never visited NotificationSettings stayed pinned to the old SW
   forever. Native (Capacitor) skips this — there is no stale SW on the
   filesystem origin.

3. .gitlab-ci.yml — the deploy-web rsync was excluding sw.js from the
   first pass and never re-adding it to the second pass, so deploys
   silently never updated sw.js. Now it ships in the second pass
   alongside index.html (after hashed assets land).
This commit is contained in:
Alex Gleason
2026-05-17 13:48:24 -05:00
parent e0024567ff
commit 5b8d2d5c06
3 changed files with 46 additions and 6 deletions
+7 -5
View File
@@ -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).
+16 -1
View File
@@ -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();
})(),
);
});
+23
View File
@@ -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);
});
});
}