Files
ytplayer/frontend/asset-sync-core.js

26 lines
3.0 KiB
JavaScript

/* Exact, resumable asset synchronization shared by pages and workers. */
(function(root){
'use strict';
const CACHE='ytplayer-assets', STATE='/__ytp_asset_state';
const url=(path,file)=>path+'?v='+file.h;
function paths(m){return [...new Set(Object.values(m.groups).flatMap(g=>g.files))].sort();}
function blocking(m){return [...new Set(Object.entries(m.groups).filter(([n,g])=>n==='core'||!g.background).flatMap(([,g])=>g.files))].sort();}
function plan(m,keys){const held=new Set(keys);return {missing:paths(m).map(p=>url(p,m.files[p])).filter(u=>!held.has(u)),blocking:blocking(m).map(p=>url(p,m.files[p]))};}
function retained(current,previous){const keep=new Set(paths(current).map(p=>url(p,current.files[p])));if(previous)for(const p of paths(previous))if(current.files[p]&&current.files[p].h!==previous.files[p].h)keep.add(url(p,previous.files[p]));return keep;}
async function status(m,cache){let missing=0;for(const p of blocking(m)){const r=await cache.match(url(p,m.files[p]));if(!r||r.headers.get('X-Asset-Hash')!==m.files[p].h)missing++;}return {ready:missing===0,missing,version:m.buildTag};}
async function syncAssets(m,{cache,fetchFn,concurrency=6,attempts=3}){
const missing=[];for(const p of paths(m)){const u=url(p,m.files[p]),r=await cache.match(u);if(!r||r.headers.get('X-Asset-Hash')!==m.files[p].h)missing.push(u);}
const count=missing.length;let failure;
await Promise.all(Array.from({length:Math.min(6,concurrency,Math.max(1,count))},async()=>{
while(missing.length&&!failure){const u=missing.shift();let error;
for(let i=0;i<attempts;i++){try{const ctl=new AbortController();const timer=setTimeout(()=>ctl.abort(),30000);let r;try{r=await fetchFn(u,{credentials:'same-origin',signal:ctl.signal});}finally{clearTimeout(timer);}if(!r.ok||r.headers.get('X-Asset-Hash')!==u.split('v=')[1])throw Error('Asset hash mismatch: '+u);await cache.put(u,r);error=null;break;}catch(e){error=e;}}
if(error)failure=error;
}
}));if(failure)throw failure;if(!(await status(m,cache)).ready)throw Error('Incomplete blocking assets');return {refreshed:count,caches:1};
}
async function state(cache){const r=await cache.match(STATE);return r?await r.json():null;}
async function commit(m,cache){if(!(await status(m,cache)).ready)throw Error('Incomplete blocking assets');const old=await state(cache);const previous=old?.current?.buildTag===m.buildTag?old.previous:old?.current;await cache.put(STATE,new Response(JSON.stringify({current:m,previous}),{headers:{'Content-Type':'application/json'}}));const keep=retained(m,previous);for(const req of await cache.keys()){const u=new URL(req.url);const k=u.pathname+u.search;if(u.searchParams.has('v')&&!keep.has(k))await cache.delete(k);}return {current:m,previous};}
const api={CACHE,STATE,url,paths,blocking,plan,retained,status,syncAssets,state,commit};
if(typeof module!=='undefined'&&module.exports)module.exports=api;else root.AssetSyncCore=api;
})(typeof globalThis!=='undefined'?globalThis:this);