Publish asset state atomically and reject unavailable versions

This commit is contained in:
Jonathan Sykes
2026-10-07 23:56:46 +08:00
parent 54a07a0a2a
commit 5b25ccaa86
4 changed files with 119 additions and 30 deletions

View File

@@ -306,7 +306,7 @@ self.addEventListener('message', (e) => {
e.waitUntil((async () => {
if (ASSET_SYNC) {
let reply = { ready: false, missing: -1, version: VERSION };
try { const cache = await caches.open(AssetSyncCore.CACHE); const m = await candidate(cache); if(m) reply = await AssetSyncCore.status(m,cache); } catch {}
try { const cache = await caches.open(AssetSyncCore.CACHE); const m = await candidate(cache) || (await AssetSyncCore.state(cache))?.current; if(m) reply = await AssetSyncCore.status(m,cache); } catch {}
reply.type = 'CACHE_STATUS'; reply.assetSync = true;
if(e.ports && e.ports[0]) e.ports[0].postMessage(reply); else if(e.source) e.source.postMessage(reply);
return;
@@ -488,9 +488,8 @@ async function activateAssets() {
const cache = await caches.open(AssetSyncCore.CACHE), m = await candidate(cache);
if (!m) throw Error('Missing candidate manifest');
const names = await migrateLegacy(cache,m), old = await AssetSyncCore.state(cache);
const state = await AssetSyncCore.commit(m,cache);
state.previousClients = (await self.clients.matchAll({type:'window'})).map(c=>c.id);
await cache.put(AssetSyncCore.STATE,new Response(JSON.stringify(state)));
const previousClients = (await self.clients.matchAll({type:'window'})).map(c=>c.id);
await AssetSyncCore.commit(m,cache,{previousClients});
// Legacy deletion occurs strictly after verified commit.
await Promise.all(names.map(n => caches.delete(n)));
for (const req of await cache.keys()) if(new URL(req.url).pathname.startsWith('/__ytp_candidate/')) await cache.delete(req);
@@ -509,7 +508,7 @@ async function assetFetch(request, clientId) {
if(!nav && state.previousClients?.includes(clientId) && state.previous?.files[u.pathname]) m = state.previous;
let path = nav ? '/index.html' : u.pathname;
let key;
if(u.searchParams.has('v') && !nav) {
if(u.searchParams.has('v') && state.current.files[u.pathname]) {
key = u.pathname + u.search;
if(state.previous?.legacyURLs && u.searchParams.get('v') === state.previous.buildTag) key = state.previous.legacyURLs[path] || key;
} else if(m.files[path]) key = AssetSyncCore.url(path,m.files[path]);
@@ -517,7 +516,7 @@ async function assetFetch(request, clientId) {
const cached = await cache.match(key);
const h = new URL(key,self.location.origin).searchParams.get('v');
if(cached && cached.headers.get('X-Asset-Hash') === h) return cached;
try { const r = await fetch(key); if(r.ok && r.headers.get('X-Asset-Hash') === h) await cache.put(key,r.clone()); return r; } catch { return new Response('Offline',{status:503}); }
try { const r = await fetch(key); if(r.ok && r.headers.get('X-Asset-Hash') !== h) return new Response('Asset version unavailable',{status:409}); if(r.ok && request.method !== 'HEAD') await cache.put(key,r.clone()); return r; } catch { return new Response('Offline',{status:503}); }
}
try { return await fetch(request); } catch { return new Response('Offline',{status:503}); }
}