import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { runInNewContext } from 'node:vm'; function worker() { const root='https://chat.example.org/'; const rows=new Map(); const network=new Map(); const events: Recordvoid>={}; const pending:Promise[]=[]; const holds=new Map>(); const writes=new Map>(); let offline=false; const key=(value:any)=>new URL(typeof value==='string'?value:value.url,root).href; const fetch=async(value:any)=>{ if(offline)throw Error('offline'); await holds.get(key(value)); const body=network.get(key(value)); return new Response(body??'missing',{status:body===undefined?404:200}); }; const cache={ match:async(value:any)=>rows.get(key(value))?.clone(), put:async(value:any,res:Response)=>{await writes.get(key(value));rows.set(key(value),res.clone());}, addAll:async(values:string[])=>{for(const value of values){const res=await fetch(value);if(!res.ok)throw Error('cache failed');await cache.put(value,res);}}, }; runInNewContext(readFileSync(new URL('../public/sw.js',import.meta.url),'utf8'),{ self:{location:{origin:new URL(root).origin},addEventListener:(name:string,cb:any)=>{events[name]=cb;}}, caches:{open:async()=>cache,match:cache.match},fetch,URL,Response,console:{warn(){}}, }); const request=(path:string,mode='cors'):Promise=>{ let response:Promise|undefined; events.fetch({request:{url:key(path),method:'GET',mode},respondWith(p:Promise){response=p;},waitUntil(p:Promise){pending.push(p);}}); return response!; }; for(const name of ['config.js','manifest.webmanifest','icon-192.png'])network.set(key(name),name); const hold=(map:Map>,path:string)=>{ let release!:()=>void; map.set(key(path),new Promise(resolve=>{release=resolve;})); return release; }; return {rows,network,key,request,offline:()=>{offline=true;},settle:()=>Promise.all(pending),holdFetch:(path:string)=>hold(holds,path),holdWrite:(path:string)=>hold(writes,path)}; } test('a client-only shell update caches new bundles before replacing the offline page',async()=>{ const w=worker(); w.rows.set(w.key('./'),new Response('')); w.rows.set(w.key('assets/old.js'),new Response('old bundle')); const html=''; w.network.set(w.key('./'),html);w.network.set(w.key('assets/new.js'),'new bundle');w.network.set(w.key('assets/new.css'),'new style'); assert.equal(await(await w.request('./','navigate')).text(),html); await w.settle(); w.offline(); assert.equal(await(await w.request('./','navigate')).text(),html); assert.equal(await(await w.request('assets/new.js')).text(),'new bundle'); assert.equal(await(await w.request('assets/new.css')).text(),'new style'); assert.equal(await(await w.request('assets/old.js')).text(),'old bundle'); }); test('a failed bundle download retains the previous complete offline shell',async()=>{ const w=worker();const old=''; w.rows.set(w.key('./'),new Response(old));w.rows.set(w.key('assets/old.js'),new Response('old bundle')); const next=''; w.network.set(w.key('./'),next); assert.equal(await(await w.request('./','navigate')).text(),next); await w.settle(); w.offline(); assert.equal(await(await w.request('./','navigate')).text(),old); assert.equal(await(await w.request('assets/old.js')).text(),'old bundle'); }); test('navigation returns while shell downloads are pending and keeps a complete offline page',async()=>{ const w=worker();const old=''; w.rows.set(w.key('./'),new Response(old));w.rows.set(w.key('assets/old.js'),new Response('old bundle')); const next=''; w.network.set(w.key('./'),next);w.network.set(w.key('assets/new.js'),'new bundle'); const release=w.holdFetch('assets/new.js'); try { const body=w.request('./','navigate').then(res=>res.text()); assert.equal(await Promise.race([body,new Promise(resolve=>setTimeout(()=>resolve('blocked'),100))]),next); assert.equal(await w.rows.get(w.key('./'))!.clone().text(),old); } finally {release();await w.settle();} w.offline(); assert.equal(await(await w.request('./','navigate')).text(),next); assert.equal(await(await w.request('assets/new.js')).text(),'new bundle'); }); test('manifest and icon downloads do not delay navigation',async()=>{ const w=worker();w.network.set(w.key('./'),'page'); const release=w.holdFetch('manifest.webmanifest'); try { assert.equal(await Promise.race([w.request('./','navigate').then(res=>res.text()),new Promise(resolve=>setTimeout(()=>resolve('blocked'),100))]),'page'); } finally {release();await w.settle();} assert.equal(await w.rows.get(w.key('./'))!.clone().text(),'page'); }); test('bundle and config responses do not wait for storage writes',async()=>{ for(const path of ['assets/new.js','config.js']){ const w=worker();w.network.set(w.key(path),'content'); const release=w.holdWrite(path); try { assert.equal(await Promise.race([w.request(path).then(res=>res.text()),new Promise(resolve=>setTimeout(()=>resolve('blocked'),100))]),'content'); assert.equal(w.rows.has(w.key(path)),false); } finally {release();await w.settle();} assert.equal(await w.rows.get(w.key(path))!.clone().text(),'content'); } });