Paint saved playlists before the launch sync and swap a background-downloaded update instantly

This commit is contained in:
Jonathan Sykes
2026-09-22 13:27:12 +08:00
parent 691e204d61
commit 987697e759
7 changed files with 300 additions and 18 deletions

View File

@@ -109,11 +109,44 @@
return { refreshed: fetched.length, caches: names.length };
}
/**
* Asks a worker whether it has already precached its whole shell.
* Resolves to null on any problem (no reply, no MessageChannel, timeout) —
* the caller then takes the download path, which is always correct.
*
* @param {ServiceWorker} worker
* @param {object} [opts]
* @param {number} [opts.timeoutMs]
* @param {Function} [opts.setTimeout]
* @param {Function} [opts.Channel] MessageChannel constructor (injectable)
* @returns {Promise<{ready: boolean, version: string}|null>}
*/
function askCacheStatus(worker, { timeoutMs = 3000, setTimeout: setTimeoutFn, Channel } = {}) {
const Ctor = Channel || (typeof MessageChannel !== 'undefined' ? MessageChannel : null);
const schedule = setTimeoutFn || (typeof setTimeout !== 'undefined' ? setTimeout : null);
if (!worker || !Ctor) return Promise.resolve(null);
return new Promise((resolve) => {
let done = false;
const finish = (v) => { if (!done) { done = true; resolve(v); } };
try {
const ch = new Ctor();
ch.port1.onmessage = (ev) => finish(ev && ev.data ? ev.data : null);
worker.postMessage({ type: 'CACHE_STATUS' }, [ch.port2]);
} catch { finish(null); return; }
if (schedule) schedule(() => finish(null), timeoutMs);
});
}
/**
* Applies an update: refresh the shell in place (throws on failure — the
* caller reports it and nothing reloads), then activate a waiting worker if
* there is one, then reload exactly once.
*
* Fast path: a waiting worker precached the entire new shell while it
* installed, so when it confirms that, the download is skipped and the swap
* is immediate. If it cannot confirm, the all-or-nothing download runs as
* before — the slow path is still what guarantees correctness.
*
* @param {object} opts
* @param {ServiceWorkerRegistration|null} opts.reg
* @param {ServiceWorkerContainer} opts.container
@@ -122,12 +155,24 @@
* @param {Function} [opts.setTimeout] injectable for tests
* @returns {Promise<void>}
*/
async function applyUpdate({ reg, container, reload, refreshShell, setTimeout: setTimeoutFn }) {
async function applyUpdate({ reg, container, reload, refreshShell, askStatus, onPhase, setTimeout: setTimeoutFn }) {
const scheduleTimeout = setTimeoutFn || (typeof setTimeout !== 'undefined' ? setTimeout : null);
if (refreshShell) await refreshShell();
const waiting = reg && reg.waiting;
// Did the waiting worker already download this build in the background?
// Opt-in: without an askStatus the download path runs exactly as before.
// (It must never be implicit — a probe that waits on a reply would hang
// forever if the caller injected a setTimeout that never fires.)
let precached = false;
if (waiting && askStatus) {
try {
const status = await askStatus(waiting);
precached = !!(status && status.ready);
} catch { precached = false; }
}
if (onPhase) onPhase(precached ? 'ready' : 'downloading');
if (!precached && refreshShell) await refreshShell();
if (!waiting) {
reload();
return;
@@ -146,7 +191,7 @@
waiting.postMessage({ type: 'SKIP_WAITING' });
}
const SwUpdate = { applyUpdate, refreshShellInPlace, BUST_PARAM };
const SwUpdate = { applyUpdate, refreshShellInPlace, askCacheStatus, BUST_PARAM };
if (typeof module !== 'undefined' && module.exports) {
module.exports = SwUpdate;