68 lines
3.8 KiB
JavaScript
68 lines
3.8 KiB
JavaScript
/* Cache the single page-owned app response before it registers the worker. */
|
|
(function (root) {
|
|
'use strict';
|
|
const doc=root.document, manifest=root.Lazy.manifest, key=root.Lazy.url('/app.js');
|
|
let domReady=doc.readyState==='complete', appSawDom=false;
|
|
doc.addEventListener('DOMContentLoaded',()=>{domReady=true;appSawDom=typeof root.boot==='function';},{once:true});
|
|
function execute(node) {
|
|
doc.head.append(node);
|
|
node.remove();
|
|
delete root.Lazy.appResponse;
|
|
// app.js keeps its original DOMContentLoaded listener. Async response
|
|
// consumption can finish after that event; boot exactly once in that case.
|
|
if(domReady)root.boot();
|
|
}
|
|
async function external(url) {
|
|
await new Promise((resolve,reject)=>{
|
|
const node=doc.createElement('script');node.src=url;
|
|
node.onload=()=>{if(domReady&&!appSawDom)root.boot();resolve();};node.onerror=()=>reject(Error('Unable to load player'));
|
|
doc.head.append(node);
|
|
});
|
|
}
|
|
async function verified(response, expected) {
|
|
const bytes=await response.clone().arrayBuffer();
|
|
const digest=new Uint8Array(await root.crypto.subtle.digest('SHA-256',bytes));
|
|
const hex=Array.from(digest,b=>b.toString(16).padStart(2,'0')).join('');
|
|
if(hex.slice(0,10)!==expected)throw Error('Player body hash mismatch');
|
|
return {bytes,digest};
|
|
}
|
|
async function start() {
|
|
if(!manifest.appCache || !root.crypto?.subtle || (!root.Lazy.captureApp&&!root.navigator?.serviceWorker?.controller))return external(key);
|
|
const expected=manifest.files['/app.js'].h;
|
|
let cache;
|
|
try { cache=await root.caches?.open('ytplayer-assets'); } catch {}
|
|
let response=await cache?.match(key);
|
|
if(!root.Lazy.captureApp && response?.ok && response.headers.get('X-Asset-Hash')===expected)return external(key);
|
|
if(!response || response.headers.get('X-Asset-Hash')!==expected) {
|
|
try { response=await (root.Lazy.appResponse || root.fetch(key,{credentials:'same-origin'})); } catch {}
|
|
}
|
|
if(!response?.ok || response.headers.get('X-Asset-Hash')!==expected) {
|
|
// Preserve staged N-1 boot after an individual cache eviction. Only the
|
|
// controller can serve this old URL safely; never fetch stale URLs bare.
|
|
const state=cache && await root.AssetSyncCore.state(cache);
|
|
const previousKey=state && root.AssetSyncCore.fallback(manifest,state.previous,'/app.js');
|
|
const previous=previousKey && await cache.match(previousKey);
|
|
const previousHash=state?.previous?.files['/app.js']?.h;
|
|
const contractsMatch=state?.previous && Object.entries(manifest.groups).every(([name,group])=>state.previous.groups[name]?.contract===group.contract);
|
|
if(contractsMatch && root.navigator?.serviceWorker?.controller && previous?.ok && previous.headers.get('X-Asset-Hash')===previousHash) {
|
|
await verified(previous,previousHash);
|
|
delete root.Lazy.appResponse;
|
|
return external(previousKey);
|
|
}
|
|
throw Error('Player asset hash mismatch');
|
|
}
|
|
const {bytes,digest}=await verified(response,expected);
|
|
const permission="'sha256-"+root.btoa(String.fromCharCode(...digest))+"'";
|
|
if(!doc.querySelector('meta[http-equiv="Content-Security-Policy"]').content.includes(permission))throw Error('Player body hash mismatch');
|
|
// CacheStorage may share a full quota with saved music. Never remove data
|
|
// to make room; boot from these verified bytes even if caching is refused.
|
|
try { await cache?.put(key,response); } catch(error) { root.console.warn('[app-cache]',error.message); }
|
|
if(!root.Lazy.captureApp)return external(key);
|
|
const node=doc.createElement('script');node.textContent=new root.TextDecoder().decode(bytes);
|
|
execute(node);
|
|
}
|
|
const ready=start();
|
|
root.AppBootstrap={ready};
|
|
ready.catch(error=>root.console.error('[app-cache]',error.message));
|
|
})(typeof window!=='undefined'?window:globalThis);
|