Show the update banner only when the running build differs from the server and make Refresh UI land the new shell on flaky links

This commit is contained in:
Jonathan Sykes
2026-09-13 17:42:50 +08:00
parent 5fb863ba25
commit 91dab289c3
8 changed files with 526 additions and 266 deletions

View File

@@ -11,12 +11,12 @@
* Auto-update flow:
* 1. New SW installs alongside the old one and waits (skipWaiting() is NOT
* called automatically — see the install handler below).
* 2. Client (app.js registerServiceWorker()) notices the waiting worker
* and shows the "Update ready" banner.
* 3. User clicks "Reload now" → client (applyUpdate() in sw-update.js)
* posts SKIP_WAITING to the waiting worker and waits for it to take
* control (controllerchange) before reloading — it does NOT unregister
* or wipe caches, so the reload doesn't force a brand-new install.
* 2. The client (app.js) shows the "Update available" banner only when the
* build it is running differs from the server's /api/version buildTag —
* a waiting worker or a broadcast is merely a prompt to re-check.
* 3. "Refresh UI" (sw-update.js) re-downloads every shell file into every
* versioned shell cache (all-or-nothing), activates a waiting worker if
* there is one (SKIP_WAITING), and reloads once.
* 4. activate: if an older *versioned shell cache* is found (i.e. this
* activation is genuinely replacing a previous deploy, not just the
* first-ever install of a freshly (re)registered worker), broadcast
@@ -68,14 +68,40 @@ const SHELL = [
];
// ---- Install: pre-cache the app shell ----
// skipWaiting() is NOT called here — the page activates a waiting worker
// from "Refresh UI" (see sw-update.js).
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(SHELL))
// skipWaiting() is NOT called here — we wait for the client to confirm
// before activating, so the update banner can appear first.
);
e.waitUntil(precacheShell());
});
// Fresh requests (`cache: 'reload'` skips the browser HTTP cache, which could
// otherwise hand the new worker yesterday's app.js), a few attempts per file
// because the homelab link drops requests, and a failed install deletes its
// partial cache — a leftover ytplayer-<tag> cache is exactly what activate
// uses to decide "this is a genuine update", and it used to re-open the
// update banner on pages that were already current.
const PRECACHE_ATTEMPTS = 3;
async function precacheShell() {
const cache = await caches.open(CACHE);
try {
await Promise.all(SHELL.map(async (url) => {
let lastErr = null;
for (let attempt = 0; attempt < PRECACHE_ATTEMPTS; attempt++) {
try {
const res = await fetch(new Request(url, { cache: 'reload' }));
if (!res.ok) throw new Error(url + ' → ' + res.status);
await cache.put(url, res);
return;
} catch (err) { lastErr = err; }
}
throw lastErr;
}));
} catch (err) {
await caches.delete(CACHE);
throw err;
}
}
// ---- Activate: evict old caches, claim clients, notify about update ----
self.addEventListener('activate', (e) => {
e.waitUntil((async () => {