Stage legacy asset migration before activation
This commit is contained in:
@@ -57,6 +57,31 @@ test('waiting status does not start completion and activation delegates it to it
|
|||||||
assert.equal((await core.status(e.m,cache)).offlineReady,true);
|
assert.equal((await core.status(e.m,cache)).offlineReady,true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('legacy shell verification is staged during install so Refresh UI activation does no hashing',async()=>{
|
||||||
|
const e=environment(),legacy=await e.storage.open('ytplayer-production-old');let digests=0;
|
||||||
|
await legacy.put('/app.js',new Response('legacy app bytes'));
|
||||||
|
const subtle=e.sandbox.crypto.subtle;
|
||||||
|
e.sandbox.crypto={subtle:{digest:async(...args)=>{digests++;return subtle.digest(...args);}}};
|
||||||
|
await e.dispatch('install');
|
||||||
|
const stagedDigests=digests,cache=await e.storage.open(core.CACHE),candidate=await(await cache.match('/__ytp_candidate/next')).json();
|
||||||
|
assert.ok(stagedDigests>0,'install verified old cached bytes before offering Refresh UI');
|
||||||
|
assert.deepEqual(candidate.legacyCacheNames,['ytplayer-production-old']);
|
||||||
|
assert.ok((await core.state(cache)).current.legacyURLs['/app.js']);
|
||||||
|
await e.dispatch('activate');
|
||||||
|
assert.equal(digests,stagedDigests,'the user-triggered activation does not hash the old shell again');
|
||||||
|
assert.equal((await e.storage.keys()).includes('ytplayer-production-old'),false,'old shell is deleted only after commit');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('offline opt-in is persisted and adds only declared optional groups to completion',async()=>{
|
||||||
|
const e=environment();e.m.files['/large.wasm']={h:'wasm'};e.m.groups.dsp={files:['/large.wasm'],contract:1,background:true,offlineOptIn:'jamesDsp'};
|
||||||
|
await e.dispatch('install');await e.dispatch('activate');await e.complete();
|
||||||
|
const cache=await e.storage.open(core.CACHE);assert.equal(await cache.match('/large.wasm?v=wasm'),undefined);
|
||||||
|
await e.dispatch('message',{data:{type:'OFFLINE_OPT_INS',values:['jamesDsp','not-a-valid-opt-in']}});
|
||||||
|
assert.deepEqual(await core.readOptIns(cache),['jamesDsp']);
|
||||||
|
assert.equal((await cache.match('/large.wasm?v=wasm')).headers.get('X-Asset-Hash'),'wasm');
|
||||||
|
const status=await core.status(e.m,cache);assert.equal(status.offlineReady,true);assert.equal(status.total,3);
|
||||||
|
});
|
||||||
|
|
||||||
test('page-captured app is reused on first install without a worker download',async()=>{
|
test('page-captured app is reused on first install without a worker download',async()=>{
|
||||||
const e=environment(),cache=await e.storage.open(core.CACHE);
|
const e=environment(),cache=await e.storage.open(core.CACHE);
|
||||||
await cache.put('/app.js?v=app',new Response('page captured app',{headers:{'X-Asset-Hash':'app'}}));
|
await cache.put('/app.js?v=app',new Response('page captured app',{headers:{'X-Asset-Hash':'app'}}));
|
||||||
|
|||||||
@@ -305,13 +305,18 @@ self.addEventListener('fetch', (e) => {
|
|||||||
const playingClients = new Map(), deferredClients = new Set();
|
const playingClients = new Map(), deferredClients = new Set();
|
||||||
let activeLayout = 'classic', layoutReply;
|
let activeLayout = 'classic', layoutReply;
|
||||||
let completionTask;
|
let completionTask;
|
||||||
function completeAssets(force = false) {
|
function completeAssets(force = false, requestedOptIns) {
|
||||||
if (!ASSET_SYNC) return Promise.resolve();
|
if (!ASSET_SYNC) return Promise.resolve();
|
||||||
|
if (completionTask) {
|
||||||
|
if (!requestedOptIns) return completionTask;
|
||||||
|
return completionTask.finally(() => completeAssets(true, requestedOptIns));
|
||||||
|
}
|
||||||
completionTask ||= (async () => {
|
completionTask ||= (async () => {
|
||||||
const cache = await caches.open(AssetSyncCore.CACHE);
|
const cache = await caches.open(AssetSyncCore.CACHE);
|
||||||
const m = (await AssetSyncCore.state(cache))?.current;
|
const m = (await AssetSyncCore.state(cache))?.current;
|
||||||
if (!m || m.buildTag !== VERSION) return;
|
if (!m || m.buildTag !== VERSION) return;
|
||||||
const job = await AssetSyncCore.complete(m, { cache, fetchFn: fetch, force,
|
const optIns = requestedOptIns || await AssetSyncCore.readOptIns(cache);
|
||||||
|
const job = await AssetSyncCore.complete(m, { cache, fetchFn: fetch, force, optIns,
|
||||||
notify: async status => {
|
notify: async status => {
|
||||||
for (const client of await self.clients.matchAll({ type: 'window' }))
|
for (const client of await self.clients.matchAll({ type: 'window' }))
|
||||||
client.postMessage({ type: 'OFFLINE_STATUS', ...status });
|
client.postMessage({ type: 'OFFLINE_STATUS', ...status });
|
||||||
@@ -330,6 +335,16 @@ self.addEventListener('sync', e => {
|
|||||||
if (e.tag === 'ytp-offline-complete') e.waitUntil(completeAssets());
|
if (e.tag === 'ytp-offline-complete') e.waitUntil(completeAssets());
|
||||||
});
|
});
|
||||||
self.addEventListener('message', (e) => {
|
self.addEventListener('message', (e) => {
|
||||||
|
if (ASSET_SYNC && e.data?.type === 'OFFLINE_OPT_INS' && Array.isArray(e.data.values)) {
|
||||||
|
e.waitUntil((async () => {
|
||||||
|
const cache = await caches.open(AssetSyncCore.CACHE);
|
||||||
|
const current = (await AssetSyncCore.state(cache))?.current || await candidate(cache);
|
||||||
|
const allowed = new Set(Object.values(current?.groups || {}).map(group => group.offlineOptIn).filter(Boolean));
|
||||||
|
const values = [...new Set(e.data.values.filter(value => typeof value === 'string' && allowed.has(value)))];
|
||||||
|
await cache.put(AssetSyncCore.OPT_INS, Response.json(values));
|
||||||
|
await completeAssets(true, values);
|
||||||
|
})());
|
||||||
|
}
|
||||||
if (ASSET_SYNC && e.data?.type === 'LAYOUT' && e.source && typeof e.data.value === 'string') {
|
if (ASSET_SYNC && e.data?.type === 'LAYOUT' && e.source && typeof e.data.value === 'string') {
|
||||||
activeLayout = e.data.value;
|
activeLayout = e.data.value;
|
||||||
layoutReply?.();
|
layoutReply?.();
|
||||||
@@ -536,6 +551,10 @@ async function installAssets() {
|
|||||||
for(const name of layouts)for(const dependency of m.groups[name].dependencies || [])layouts.add(dependency);
|
for(const name of layouts)for(const dependency of m.groups[name].dependencies || [])layouts.add(dependency);
|
||||||
await AssetSyncCore.syncAssets(m,{cache,fetchFn:fetchAsset,groups:[...layouts]});
|
await AssetSyncCore.syncAssets(m,{cache,fetchFn:fetchAsset,groups:[...layouts]});
|
||||||
}
|
}
|
||||||
|
// Copy and hash legacy shell entries before the candidate reaches `installed`.
|
||||||
|
// A legacy page's Refresh UI then activates quickly, before its four-second
|
||||||
|
// reload safety timer can send it back into the old shell.
|
||||||
|
m.legacyCacheNames = await migrateLegacy(cache,m);
|
||||||
await cache.put(candidateKey, new Response(JSON.stringify(m)));
|
await cache.put(candidateKey, new Response(JSON.stringify(m)));
|
||||||
}
|
}
|
||||||
async function migrateLegacy(cache, m) {
|
async function migrateLegacy(cache, m) {
|
||||||
@@ -566,7 +585,11 @@ async function migrateLegacy(cache, m) {
|
|||||||
async function activateAssets() {
|
async function activateAssets() {
|
||||||
const cache = await caches.open(AssetSyncCore.CACHE), m = await candidate(cache);
|
const cache = await caches.open(AssetSyncCore.CACHE), m = await candidate(cache);
|
||||||
if (!m) throw Error('Missing candidate manifest');
|
if (!m) throw Error('Missing candidate manifest');
|
||||||
const names = await migrateLegacy(cache,m), old = await AssetSyncCore.state(cache);
|
// Legacy migration is staged by installAssets. Keep the fallback for
|
||||||
|
// candidates created by an older worker version.
|
||||||
|
const names = m.legacyCacheNames || await migrateLegacy(cache,m);
|
||||||
|
delete m.legacyCacheNames;
|
||||||
|
const old = await AssetSyncCore.state(cache);
|
||||||
const previousClients = (await self.clients.matchAll({type:'window'})).map(c=>c.id);
|
const previousClients = (await self.clients.matchAll({type:'window'})).map(c=>c.id);
|
||||||
await AssetSyncCore.commit(m,cache,{previousClients,activeLayout:m.activeLayout || activeLayout});
|
await AssetSyncCore.commit(m,cache,{previousClients,activeLayout:m.activeLayout || activeLayout});
|
||||||
// Legacy deletion occurs strictly after verified commit.
|
// Legacy deletion occurs strictly after verified commit.
|
||||||
|
|||||||
Reference in New Issue
Block a user