Synchronize verified assets incrementally and guard update activation

This commit is contained in:
Jonathan Sykes
2026-10-07 23:40:20 +08:00
parent a1f16f2e3b
commit 54a07a0a2a
15 changed files with 260 additions and 22 deletions

View File

@@ -64,7 +64,7 @@
* @param {Function} [opts.setTimeout]
* @returns {Promise<{refreshed: number, caches: number, resumed: number}>}
*/
async function refreshShellInPlace({ cachesApi, fetchFn, isShellCache, bust, timeoutMs, attempts, attemptTimeoutMs, concurrency, stagingKey, setTimeout: setTimeoutFn }) {
async function refreshLegacyShell({ cachesApi, fetchFn, isShellCache, bust, timeoutMs, attempts, attemptTimeoutMs, concurrency, stagingKey, setTimeout: setTimeoutFn }) {
const names = (await cachesApi.keys()).filter(isShellCache);
if (!names.length) return { refreshed: 0, caches: 0, resumed: 0 }; // uncontrolled page: a reload already hits the network
@@ -145,6 +145,42 @@
return { refreshed: urls.size, caches: names.length, resumed };
}
async function refreshShellInPlace(opts) {
const core = root.AssetSyncCore;
if (core) {
const response = await opts.fetchFn('/api/manifest', { cache: 'no-store' });
if (!response.ok) throw new Error('Manifest unavailable');
const m = await response.json();
const cache = await opts.cachesApi.open(core.CACHE);
// Only upgraded workers can route the persistent scheme. A rollback
// worker explicitly advertises the legacy path through CACHE_STATUS.
const reg = root.navigator && await root.navigator.serviceWorker.getRegistration();
const worker = reg && (reg.waiting || reg.active);
const status = await askCacheStatus(worker);
if (status && status.assetSync) {
const result = await core.syncAssets(m,{cache,fetchFn:opts.fetchFn});
if (!reg.waiting) await core.commit(m,cache);
return result;
}
}
return refreshLegacyShell({ ...opts, isShellCache: n => n !== 'ytplayer-assets' && opts.isShellCache(n) });
}
function playing() { return typeof Player !== 'undefined' && !!Player._wantsPlaying; }
function reportPlaying(value = playing()) {
const sw = root.navigator && root.navigator.serviceWorker;
if (!sw) return;
sw.getRegistration().then(reg => {
for (const worker of [sw.controller, reg && reg.waiting]) if(worker) worker.postMessage({type:'PLAYING',value});
}).catch(()=>{});
}
if (root.document && root.navigator && root.navigator.serviceWorker) {
if (root.VisibleTimer) root.VisibleTimer.setIntervalWhenVisible(()=>reportPlaying(),1000);
for (const event of ['play','pause','ended']) root.document.addEventListener(event,()=>reportPlaying(),true);
root.document.addEventListener('visibilitychange',()=>reportPlaying());
root.navigator.serviceWorker.addEventListener('controllerchange',()=>reportPlaying());
}
/**
* Asks a worker whether it has already precached its whole shell.
* Resolves to null on any problem (no reply, no MessageChannel, timeout) —
@@ -191,7 +227,14 @@
* @param {Function} [opts.setTimeout] injectable for tests
* @returns {Promise<void>}
*/
async function applyUpdate({ reg, container, reload, refreshShell, askStatus, onPhase, setTimeout: setTimeoutFn }) {
async function applyUpdate({ reg, container, reload, refreshShell, askStatus, onPhase, isPlaying = playing, confirmOverride = root.confirm && root.confirm.bind(root), setTimeout: setTimeoutFn }) {
let overridden = false;
const guard = () => {
if (!isPlaying() || overridden) return true;
if (confirmOverride && confirmOverride('Playing - update after this song / when paused. Update now anyway? Playback may stop.')) { overridden = true; return true; }
return false;
};
if (!guard()) throw new Error('Playing - update after this song / when paused');
const scheduleTimeout = setTimeoutFn || (typeof setTimeout !== 'undefined' ? setTimeout : null);
const waiting = reg && reg.waiting;
@@ -209,6 +252,7 @@
if (onPhase) onPhase(precached ? 'ready' : 'downloading');
if (!precached && refreshShell) await refreshShell();
if (!guard()) throw new Error('Playing - update after this song / when paused');
if (!waiting) {
reload();
return;
@@ -216,7 +260,7 @@
let reloaded = false;
const reloadOnce = () => {
if (reloaded) return;
if (reloaded || (isPlaying() && !overridden)) return;
reloaded = true;
reload();
};
@@ -224,6 +268,7 @@
// either way; reload on controllerchange, or after a short safety net.
container.addEventListener('controllerchange', reloadOnce, { once: true });
if (scheduleTimeout) scheduleTimeout(reloadOnce, 4000);
if (overridden) waiting.postMessage({type:'PLAYING',value:false});
waiting.postMessage({ type: 'SKIP_WAITING' });
}