@lynx-js/web-core
Version:
This is an internal experimental package, do not use
288 lines • 11 kB
JavaScript
/*
* Copyright 2025 The Lynx Authors. All rights reserved.
* Licensed under the Apache License Version 2.0 that can be found in the
* LICENSE file in the root directory of this source tree.
*/
import { TemplateSectionLabel } from '../../constants.js';
const wasm = import(
/* webpackMode: "eager" */
/* webpackChunkName: "wasm-initializer" */
/* webpackFetchPriority: "high" */
/* webpackPrefetch: true */
/* webpackPreload: true */
'../wasm.js');
export class TemplateManager {
#bundles = new Map();
#loadingBundles = new Map();
#loadingPromises = new Map();
#lynxViewInstancesMap = new Map();
#pendingResolves = new Map();
#worker = null;
#workerReadyPromise = null;
#resolveWorkerReady = null;
constructor() {
this.#ensureWorker();
}
fetchBundle(url, lynxViewInstancePromise, transformVW, transformVH, transformREM, overrideConfig) {
if (this.#bundles.has(url) && !overrideConfig) {
return (async () => {
const bundle = this.#bundles.get(url);
const config = (bundle?.config || {});
const lynxViewInstance = await lynxViewInstancePromise;
lynxViewInstance.backgroundThread.markTiming('decode_start');
lynxViewInstance.onPageConfigReady(config);
lynxViewInstance.onStyleInfoReady(url);
lynxViewInstance.onMTSScriptsLoaded(url, config.isLazy === 'true');
lynxViewInstance.onBTSScriptsLoaded(url);
})();
}
else if (this.#loadingPromises.has(url)) {
return this.#loadingPromises.get(url).then(async () => {
const bundle = this.#bundles.get(url);
const config = (bundle?.config || {});
const lynxViewInstance = await lynxViewInstancePromise;
lynxViewInstance.backgroundThread.markTiming('decode_start');
lynxViewInstance.onPageConfigReady(config);
lynxViewInstance.onStyleInfoReady(url);
lynxViewInstance.onMTSScriptsLoaded(url, config.isLazy === 'true');
lynxViewInstance.onBTSScriptsLoaded(url);
});
}
else {
this.createBundle(url);
const promise = this.#load(url, lynxViewInstancePromise, transformVW, transformVH, transformREM, overrideConfig);
this.#loadingPromises.set(url, promise);
return promise;
}
}
async #load(url, lynxViewInstancePromise, transformVW, transformVH, transformREM, overrideConfig) {
const currentTime = performance.now() + performance.timeOrigin;
lynxViewInstancePromise.then((instance) => {
instance.backgroundThread.markTiming('fetch_start', undefined, currentTime);
});
this.#lynxViewInstancesMap.set(url, lynxViewInstancePromise);
await this.#ensureWorker();
const msg = {
type: 'load',
url,
fetchUrl: (new URL(url, location.href)).toString(),
transformVW,
transformVH,
transformREM,
overrideConfig,
};
this.#worker.postMessage(msg);
return new Promise((resolve, reject) => {
this.#pendingResolves.set(url, { resolve, reject });
});
}
#resolvePromise(url) {
const promise = this.#pendingResolves.get(url);
if (promise) {
promise.resolve();
this.#pendingResolves.delete(url);
}
}
#rejectPromise(url, reason) {
const promise = this.#pendingResolves.get(url);
if (promise) {
promise.reject(reason);
this.#pendingResolves.delete(url);
}
}
#ensureWorker() {
if (!this.#worker) {
this.#workerReadyPromise = new Promise((resolve) => {
this.#resolveWorkerReady = resolve;
});
this.#worker = new Worker(new URL(
/* webpackFetchPriority: "high" */
/* webpackChunkName: "web-core-template-loader-thread" */
/* webpackPrefetch: true */
/* webpackPreload: true */
'../decodeWorker/decode.worker.js', import.meta.url), { type: 'module' });
this.#worker.onmessage = this.#handleMessage.bind(this);
this.#workerReadyPromise.then(() => {
wasm.then(({ wasmModule }) => {
this.#worker.postMessage({
type: 'init',
wasmModule,
});
});
});
return this.#workerReadyPromise;
}
else if (this.#workerReadyPromise) {
return this.#workerReadyPromise;
}
}
#handleMessage(event) {
const msg = event.data;
if (msg.type === 'ready') {
if (this.#resolveWorkerReady) {
this.#resolveWorkerReady();
this.#resolveWorkerReady = null;
this.#workerReadyPromise = null;
}
return;
}
const { url } = msg;
const lynxViewInstancePromise = this.#lynxViewInstancesMap.get(url);
if (!lynxViewInstancePromise)
return;
switch (msg.type) {
case 'section':
/**
* The lynxViewInstance is already awaited the wasm is ready
*/
this.#handleSection(msg, lynxViewInstancePromise);
break;
case 'error':
console.error(`Error decoding bundle ${url}:`, msg.error);
this.#cleanup(url);
this.#removeBundle(url);
this.#rejectPromise(url, new Error(msg.error));
this.#loadingPromises.delete(url);
break;
case 'done':
this.#cleanup(url);
const bundle = this.#loadingBundles.get(url);
if (bundle) {
this.#bundles.set(url, bundle);
this.#loadingBundles.delete(url);
}
this.#resolvePromise(url);
this.#loadingPromises.delete(url);
/* TODO: The promise resolution is deferred inside .then() without error handling.
*
*/
lynxViewInstancePromise.then((instance) => {
instance.backgroundThread.markTiming('decode_end');
instance.backgroundThread.markTiming('load_template_start');
});
break;
}
}
async #handleSection(msg, instancePromise) {
const [instance, StyleSheetResource,] = await Promise.all([
instancePromise,
wasm.then((wasm) => (wasm.wasmInstance.StyleSheetResource)),
]);
const { label, data, url, config } = msg;
switch (label) {
case TemplateSectionLabel.Configurations: {
instance.backgroundThread.markTiming('decode_start');
this.#setConfig(url, data);
instance.onPageConfigReady(data);
break;
}
case TemplateSectionLabel.StyleInfo: {
const resource = new StyleSheetResource(new Uint8Array(data), document);
const bundle = this.#loadingBundles.get(url);
if (bundle) {
bundle.styleSheet = resource;
}
instance.onStyleInfoReady(url);
break;
}
case TemplateSectionLabel.LepusCode: {
const blobMap = data;
this.#setLepusCode(url, blobMap);
instance.onMTSScriptsLoaded(url, config['isLazy'] === 'true');
break;
}
case TemplateSectionLabel.CustomSections: {
this.#setCustomSection(url, data);
break;
}
case TemplateSectionLabel.Manifest: {
const blobMap = data;
this.#setBackgroundCode(url, blobMap);
instance.onBTSScriptsLoaded(url);
break;
}
default:
throw new Error(`Unknown section label: ${label}`);
}
}
#cleanup(url) {
this.#lynxViewInstancesMap.delete(url);
}
createBundle(url) {
if (this.#bundles.has(url)) {
const bundle = this.#bundles.get(url);
if (bundle) {
if (bundle.lepusCode) {
for (const blobUrl of Object.values(bundle.lepusCode)) {
URL.revokeObjectURL(blobUrl);
}
}
if (bundle.backgroundCode) {
for (const blobUrl of Object.values(bundle.backgroundCode)) {
URL.revokeObjectURL(blobUrl);
}
}
if (bundle.styleSheet) {
bundle.styleSheet.free();
}
}
this.#bundles.delete(url);
}
if (this.#loadingBundles.has(url)) {
const bundle = this.#loadingBundles.get(url);
if (bundle) {
if (bundle.lepusCode) {
for (const blobUrl of Object.values(bundle.lepusCode)) {
URL.revokeObjectURL(blobUrl);
}
}
if (bundle.backgroundCode) {
for (const blobUrl of Object.values(bundle.backgroundCode)) {
URL.revokeObjectURL(blobUrl);
}
}
if (bundle.styleSheet) {
bundle.styleSheet.free();
}
}
this.#loadingBundles.delete(url);
}
this.#loadingBundles.set(url, {});
}
#removeBundle(url) {
this.createBundle(url); // This actually clears it in current logic
this.#loadingBundles.delete(url);
}
#setConfig(url, config) {
const bundle = this.#loadingBundles.get(url);
if (bundle) {
bundle.config = config;
}
}
#setLepusCode(url, lepusCode) {
const bundle = this.#loadingBundles.get(url);
if (bundle) {
bundle.lepusCode = lepusCode;
}
}
#setCustomSection(url, customSections) {
const bundle = this.#loadingBundles.get(url);
if (bundle) {
bundle.customSections = customSections;
}
}
#setBackgroundCode(url, backgroundCode) {
const bundle = this.#loadingBundles.get(url);
if (bundle) {
bundle.backgroundCode = backgroundCode;
}
}
getBundle(url) {
return this.#bundles.get(url) || this.#loadingBundles.get(url);
}
getStyleSheet(url) {
return this.getBundle(url)?.styleSheet;
}
}
export const templateManager = new TemplateManager();
//# sourceMappingURL=TemplateManager.js.map