Complete verified offline assets in a resumable worker job

This commit is contained in:
Jonathan Sykes
2026-10-09 09:32:08 +08:00
parent 97f4a7396e
commit 0251720fa2
4 changed files with 150 additions and 22 deletions

View File

@@ -264,6 +264,8 @@ self.addEventListener('fetch', (e) => {
return;
}
if (ASSET_SYNC && request.mode === 'navigate') e.waitUntil(completeAssets());
// Only intercept GET/HEAD — let POST (sync endpoint) go through unmodified
if (request.method !== 'GET' && request.method !== 'HEAD') return;
@@ -300,7 +302,32 @@ self.addEventListener('fetch', (e) => {
// ---- Message: handle SKIP_WAITING from the client ----
const playingClients = new Map(), deferredClients = new Set();
let activeLayout = 'classic', layoutReply, warmTask;
let activeLayout = 'classic', layoutReply;
let completionTask;
function completeAssets(force = false) {
if (!ASSET_SYNC) return Promise.resolve();
completionTask ||= (async () => {
const cache = await caches.open(AssetSyncCore.CACHE);
const m = await candidate(cache) || (await AssetSyncCore.state(cache))?.current;
if (!m || m.buildTag !== VERSION) return;
const job = await AssetSyncCore.complete(m, { cache, fetchFn: fetch, force,
notify: async status => {
for (const client of await self.clients.matchAll({ type: 'window' }))
client.postMessage({ type: 'OFFLINE_STATUS', ...status });
}
});
if (!job.offlineReady) {
// Chromium can wake this job without a page; Safari resumes on the next
// navigation/online message. No timers outside an event lifetime.
try { await self.registration.sync?.register('ytp-offline-complete'); } catch {}
}
})().catch(error => console.warn('[asset-sync] completion paused', error.message))
.finally(() => { completionTask = null; });
return completionTask;
}
self.addEventListener('sync', e => {
if (e.tag === 'ytp-offline-complete') e.waitUntil(completeAssets());
});
self.addEventListener('message', (e) => {
if (ASSET_SYNC && e.data?.type === 'LAYOUT' && e.source && typeof e.data.value === 'string') {
activeLayout = e.data.value;
@@ -315,14 +342,8 @@ self.addEventListener('message', (e) => {
}
})());
}
if (ASSET_SYNC && e.data?.type === 'WARM_ASSETS' && !e.data.saveData) {
// Boot, activation and multiple tabs can report idle together. Share the
// verified pool so they never download the same missing group twice.
warmTask ||= (async()=>{
const cache=await caches.open(AssetSyncCore.CACHE), m=(await AssetSyncCore.state(cache))?.current;
if(m)await AssetSyncCore.syncAssets(m,{cache,fetchFn:fetch,groups:Object.keys(m.groups).filter(name=>m.groups[name].background)});
})().catch(error=>console.warn('[asset-sync] idle warm interrupted',error.message)).finally(()=>{warmTask=null;});
e.waitUntil(warmTask);
if (ASSET_SYNC && ['WARM_ASSETS', 'COMPLETE_ASSETS'].includes(e.data?.type)) {
e.waitUntil(completeAssets(e.data.type === 'COMPLETE_ASSETS'));
}
if (e.data && e.data.type === 'PLAYING' && e.source) {
playingClients.set(e.source.id, !!e.data.value);
@@ -339,6 +360,7 @@ self.addEventListener('message', (e) => {
// deletes its cache on failure, but a cache can also be evicted under
// storage pressure, so the files are actually checked.
if (e.data && e.data.type === 'CACHE_STATUS') {
if (ASSET_SYNC) e.waitUntil(completeAssets());
e.waitUntil((async () => {
if (ASSET_SYNC) {
let reply = { ready: false, missing: -1, version: VERSION };
@@ -347,7 +369,7 @@ self.addEventListener('message', (e) => {
const pending = await candidate(cache), m = pending || state?.current;
if(m) reply = await AssetSyncCore.status(m,cache,{previous:pending ? state?.current : state?.previous,activeLayout:e.data.activeLayout || (pending ? m.activeLayout : activeLayout) || 'classic'});
} catch {}
reply.type = 'CACHE_STATUS'; reply.assetSync = true;
reply.type = 'CACHE_STATUS'; reply.assetSync = true; reply.completing = !!completionTask && !reply.offlineReady;
if(e.ports && e.ports[0]) e.ports[0].postMessage(reply); else if(e.source) e.source.postMessage(reply);
return;
}
@@ -551,6 +573,7 @@ async function activateAssets() {
for (const req of await cache.keys()) if(new URL(req.url).pathname.startsWith('/__ytp_candidate/')) await cache.delete(req);
await self.clients.claim();
if(old && old.current.buildTag !== m.buildTag) for(const c of await self.clients.matchAll({type:'window',includeUncontrolled:true})) c.postMessage({type:'SW_UPDATE_AVAILABLE',version:VERSION});
await completeAssets(true);
try { console.info('[asset-sync] storage', await self.navigator.storage.estimate()); } catch {}
}
async function assetFetch(request, clientId) {