Files
ytplayer/frontend/sw-update.js
Claude Worker 8018060d26 feat: update ready keeps showing even after updating
Task #61 completed by ClaudeQueue

ClaudeQueue
2026-07-01 17:40:50 +00:00

71 lines
2.9 KiB
JavaScript

/* ============================================================================
* sw-update — applies a waiting service-worker update in place.
*
* Framework-free and dependency-free on purpose (same pattern as
* async-guard.js):
* • Loads as a plain <script> under CSP `script-src 'self'` (browser
* global `window.SwUpdate`).
* • `require`-able by `node --test` (CommonJS `module.exports`).
*
* Bug this fixes (Task #61): "Update ready" kept reappearing right after the
* user clicked "Reload now" / "Refresh UI". The old flow called
* hardReloadUI(), which unregisters the service worker and wipes every cache
* before navigating — forcing a brand-new install on the next load. That
* fresh install briefly has `reg.waiting` truthy again (a normal but
* transient SW lifecycle state), which registerServiceWorker() misread as a
* genuinely new update and re-showed the banner immediately.
*
* Fix: activate the *already-installed* waiting worker in place —
* postMessage SKIP_WAITING to it, wait for it to actually take control
* (`controllerchange`), and only then reload. The reloaded page is served by
* the new SW from its very first request, and no fresh install/registration
* cycle happens, so the banner has nothing to spuriously re-trigger on.
* ========================================================================== */
(function (root) {
'use strict';
/**
* Applies a pending SW update: messages the waiting worker to skipWaiting(),
* waits for controllerchange, then reloads exactly once.
*
* @param {object} opts
* @param {ServiceWorkerRegistration|null} opts.reg the current registration
* @param {ServiceWorkerContainer} opts.container navigator.serviceWorker
* @param {() => void} opts.reload called at most once
* @param {(fn: () => void, ms: number) => any} [opts.setTimeout] injectable for tests
* @returns {Promise<void>}
*/
async function applyUpdate({ reg, container, reload, setTimeout: setTimeoutFn }) {
const waiting = reg && reg.waiting;
const scheduleTimeout = setTimeoutFn || (typeof setTimeout !== 'undefined' ? setTimeout : null);
if (!waiting) {
// Nothing to activate (e.g. banner was shown from a broadcast message
// rather than an actual waiting worker) — just reload.
reload();
return;
}
let reloaded = false;
const reloadOnce = () => {
if (reloaded) return;
reloaded = true;
reload();
};
container.addEventListener('controllerchange', reloadOnce, { once: true });
// Safety net in case controllerchange never fires (e.g. no controller yet).
if (scheduleTimeout) scheduleTimeout(reloadOnce, 3000);
waiting.postMessage({ type: 'SKIP_WAITING' });
}
const SwUpdate = { applyUpdate };
if (typeof module !== 'undefined' && module.exports) {
module.exports = SwUpdate;
} else {
root.SwUpdate = SwUpdate;
}
})(typeof globalThis !== 'undefined' ? globalThis : this);