Files
ytplayer/frontend/lazy.js

143 lines
7.8 KiB
JavaScript

/* Build-local classic-script loader. The inert manifest precedes this head script. */
(function (root) {
'use strict';
const doc = root.document;
let manifest;
try { manifest = JSON.parse(doc.getElementById('ytp-assets').textContent); }
catch { return; } // The index must supply this map; never fetch one during boot.
const assets = new Map(), groups = new Map(), hooks = new Map();
const css = new Set([...doc.querySelectorAll('link[data-lazy-css]')].map(link => link.dataset.lazyCss));
const selected = new Map();
const url = path => selected.get(path) || (manifest.files[path]?.h ? path + '?v=' + manifest.files[path].h : './' + path.replace(/^\//,''));
function reloadRequired(name) {
api.reloadRequired = true;
if (root.CustomEvent) root.dispatchEvent?.(new root.CustomEvent('ytp-reload-required', { detail: { group: name } }));
}
function compatible(name, current, seen = new Set()) {
if(seen.has(name))return true;
seen.add(name);
const built=manifest.groups[name], newer=current.groups[name];
if(!built || !newer || built.contract!==newer.contract)return false;
return (newer.dependencies || []).every(dependency=>(built.dependencies || []).includes(dependency)&&compatible(dependency,current,seen));
}
async function select(name) {
if (!root.caches) return;
try {
const cache = await root.caches.open('ytplayer-assets');
const response = await cache.match('/__ytp_asset_state');
const current = response && (await response.json()).current;
const built = manifest.groups[name], newer = current?.groups[name];
// The active layout's CSS already executed in the parser before app boot.
if(name === 'layout:'+api.layout) return;
if (!newer || current.buildTag === manifest.buildTag) return;
if (!compatible(name,current)) { reloadRequired(name); return; }
const changedList = newer.files.join('\n') !== built.files.join('\n');
const available = new Map();
for (const path of changedList ? newer.files : built.files) {
// Membership and contract must both match the running core's expectation.
if (!newer.files.includes(path) || !current.files[path]) continue;
const key = path + '?v=' + current.files[path].h;
const held = await cache.match(key);
if (held?.headers.get('X-Asset-Hash') === current.files[path].h) available.set(path, key);
}
if(changedList && available.size !== newer.files.length)return;
for(const [path,key] of available)selected.set(path,key);
if(changedList)return newer;
} catch { /* Private browsing/cache eviction: keep the embedded build-local map. */ }
}
function inject(path) {
if (assets.has(path)) return assets.get(path);
const style = /\.css$/.test(path);
if (style && css.has(path)) return Promise.resolve();
if (!style && !/\.js$/.test(path)) return Promise.resolve(); // workers/imports/fonts are not page scripts
const task = new Promise((resolve, reject) => {
const node = doc.createElement(style ? 'link' : 'script');
if (style) { node.rel = 'stylesheet'; node.href = url(path); }
else { node.async = false; node.src = url(path); }
node.onload = resolve;
node.onerror = () => reject(new Error('Unable to load ' + path));
doc.head.append(node);
});
assets.set(path, task);
task.catch(() => assets.delete(path));
return task;
}
function load(name) {
if (groups.has(name)) return groups.get(name);
const group = manifest.groups[name];
if (!group) return Promise.reject(new Error('Unknown asset group: ' + name));
const task = (async () => {
for (const dependency of group.dependencies || []) await load(dependency);
const selectedGroup = await select(name) || group;
for (const path of selectedGroup.files) if (!/-worker\.js$/.test(path)) await inject(path);
for (const fn of hooks.get(name) || []) await fn();
group.executed = true;
})();
groups.set(name, task);
task.catch(error => {
groups.delete(name);
if (root.CustomEvent) root.dispatchEvent?.(new root.CustomEvent('ytp-lazy-error', { detail: { group: name, error } }));
});
return task;
}
const api = {
load, prefetch: load, loaded: name => !!manifest.groups[name]?.executed, url,
manifest, layout: 'classic', reloadRequired: false,
worker(path) {
const worker=new root.Worker(url(path)), post=worker.postMessage.bind(worker); let first=true;
worker.postMessage=(message,transfer)=>{
if(first) { first=false; message={...message,assetUrls:{sha256:url('/sha256.js'),wasm:url('/sha256-wasm.js'),resume:url('/resume-core.js')}}; }
post(message,transfer);
};
return worker;
},
reportLayout(registration) {
const send = reg => {
const workers = new Set([root.navigator.serviceWorker.controller, reg?.installing, reg?.waiting, reg?.active]);
for(const worker of workers) worker?.postMessage({type:'LAYOUT',value:api.layout});
};
if(registration)send(registration);
else root.navigator.serviceWorker?.getRegistration().then(send).catch(()=>{});
},
warm() {
if(root.navigator.connection?.saveData || !root.navigator.serviceWorker) return;
const idle = root.requestIdleCallback || (fn => root.setTimeout(fn,1000));
idle(()=>root.navigator.serviceWorker.ready.then(reg=>reg.active?.postMessage({type:'WARM_ASSETS',saveData:!!root.navigator.connection?.saveData})).catch(()=>{}));
},
onLoad(name, fn) {
if(api.loaded(name)) { Promise.resolve().then(fn).catch(error=>{if(root.CustomEvent)root.dispatchEvent?.(new root.CustomEvent('ytp-lazy-error',{detail:{group:name,error}}));}); return; }
const list = hooks.get(name) || []; list.push(fn); hooks.set(name, list);
},
proxy(name, methods, globalName) {
return Object.fromEntries(methods.map(method => [method, async (...args) => {
await load(name);
const target = root[globalName || name.split(':')[1]];
if (typeof target?.[method] !== 'function') throw new Error('Missing lazy method: ' + method);
return target[method](...args);
}]));
},
intent(element, name) {
if (!element) return;
for (const type of ['pointerenter', 'focus', 'touchstart']) element.addEventListener(type, () => load(name).catch(() => {}), { once: true, passive: true });
},
};
root.Lazy = api;
root.navigator.serviceWorker?.addEventListener('message',event=>{
if(event.data?.type==='ASSET_LAYOUT_REQUEST') event.source?.postMessage({type:'LAYOUT',value:api.layout});
if(event.data?.type==='SW_UPDATE_AVAILABLE') api.warm();
});
root.navigator.serviceWorker?.addEventListener('controllerchange',()=>{api.reportLayout();api.warm();});
let settings={};
try { settings=JSON.parse(root.localStorage.getItem('_ytpdata') || '{}').settings || {}; api.layout=settings.layout || 'classic'; } catch {}
Object.assign(doc.documentElement.dataset,{layout:api.layout,theme:settings.theme || 'dark',font:settings.fontScale || 'normal',density:settings.density || 'comfortable',perf:settings.perfMode?'on':'off',motion:settings.reduceMotion?'reduced':'full'});
const layoutName = manifest.groups['layout:' + api.layout] ? 'layout:'+api.layout : 'layout:classic';
const layoutGroups = new Set([layoutName]);
for(const name of layoutGroups) for(const dependency of manifest.groups[name]?.dependencies || []) layoutGroups.add(dependency);
if (doc.readyState === 'loading') for (const path of [...layoutGroups].flatMap(name=>manifest.groups[name]?.files || [])) {
if (!/\.css$/.test(path) || css.has(path)) continue;
// Parser-inserted CSS blocks rendering; dynamic head links do not on all WebKit versions.
css.add(path);
doc.write('<link rel="stylesheet" href="' + url(path) + '" data-lazy-css="' + path + '">');
}
})(typeof window !== 'undefined' ? window : globalThis);