@lynx-js/web-core
Version:
This is an internal experimental package, do not use
326 lines • 12.9 kB
JavaScript
import { TemplateSectionLabel, MagicHeader0, MagicHeader1, } from '../../constants.js';
import { wasmInstance } from '../wasm.js';
let wasmModuleLoadedResolve;
const wasmModuleLoadedPromise = new Promise((resolve) => {
wasmModuleLoadedResolve = resolve;
});
import { loadStyleFromJSON } from './cssLoader.js';
import { decodeBinaryMap } from '../../common/decodeUtils.js';
class StreamReader {
#reader;
#buffer = new Uint8Array(0);
constructor(reader) {
this.#reader = reader;
}
async read(size) {
if (this.#buffer.length >= size) {
const result = this.#buffer.slice(0, size);
this.#buffer = this.#buffer.slice(size);
return result;
}
while (this.#buffer.length < size) {
const { done, value } = await this.#reader.read();
if (value) {
const newBuffer = new Uint8Array(this.#buffer.length + value.length);
newBuffer.set(this.#buffer);
newBuffer.set(value, this.#buffer.length);
this.#buffer = newBuffer;
}
if (done) {
break;
}
}
if (this.#buffer.length < size) {
if (this.#buffer.length === 0) {
return null;
}
throw new Error(`Unexpected end of stream. Expected ${size} bytes, got ${this.#buffer.length}`);
}
const result = this.#buffer.slice(0, size);
this.#buffer = this.#buffer.slice(size);
return result;
}
async readRest() {
while (true) {
const { done, value } = await this.#reader.read();
if (value) {
const newBuffer = new Uint8Array(this.#buffer.length + value.length);
newBuffer.set(this.#buffer);
newBuffer.set(value, this.#buffer.length);
this.#buffer = newBuffer;
}
if (done) {
break;
}
}
const result = this.#buffer;
this.#buffer = new Uint8Array(0);
return result;
}
}
function decodeJSONMap(buffer) {
const utf16Array = new Uint16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2);
let jsonString = '';
const CHUNK_SIZE = 8192;
for (let i = 0; i < utf16Array.length; i += CHUNK_SIZE) {
jsonString += String.fromCharCode.apply(null, utf16Array.subarray(i, i + CHUNK_SIZE));
}
return JSON.parse(jsonString);
}
self.onmessage = async (event) => {
const data = event.data;
if (data.type === 'init') {
const { wasmModule } = data;
wasmInstance.initSync({ module: wasmModule });
wasmModuleLoadedResolve();
}
else if (data.type === 'load') {
const { url, fetchUrl, overrideConfig, transformVW, transformVH, transformREM, } = data;
try {
const response = await fetch(fetchUrl, {
headers: {
'Accept': 'application/octet-stream, application/json',
},
});
if (!response.body || response.status !== 200) {
throw new Error(`Failed to fetch template: ${response.statusText}`);
}
const reader = response.body.getReader();
await handleStream(url, reader, transformVW, transformVH, transformREM, overrideConfig);
postMessage({ type: 'done', url });
}
catch (error) {
postMessage({ type: 'error', url, error: error.message });
}
}
};
async function handleStream(url, reader, transformVW, transformVH, transformREM, overrideConfig) {
const streamReader = new StreamReader(reader);
let config = {};
// 1. Check MagicHeader
const headerBytes = await streamReader.read(8);
if (!headerBytes) {
throw new Error('Empty stream');
}
// Check if JSON (starts with {)
if (headerBytes[0] === 123) {
const rest = await streamReader.readRest();
const decoder = new TextDecoder();
const jsonStr = decoder.decode(headerBytes) + decoder.decode(rest);
const json = JSON.parse(jsonStr);
await handleJSON(json, url, transformVW, transformVH, transformREM, overrideConfig);
return;
}
const view = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
const magic0 = view.getUint32(0, true);
const magic1 = view.getUint32(4, true);
if (magic0 !== MagicHeader0 || magic1 !== MagicHeader1) {
throw new Error('Invalid Magic Header');
}
// 2. Check Version
const versionBytes = await streamReader.read(4);
if (!versionBytes) {
throw new Error('Unexpected EOF reading version');
}
const versionView = new DataView(versionBytes.buffer, versionBytes.byteOffset, versionBytes.byteLength);
const version = versionView.getUint32(0, true);
if (version > 1) {
throw new Error(`Unsupported version: ${version}`);
}
// 3. Read Sections
while (true) {
const labelBytes = await streamReader.read(4);
if (!labelBytes) {
break; // EOF
}
const labelView = new DataView(labelBytes.buffer, labelBytes.byteOffset, labelBytes.byteLength);
const label = labelView.getUint32(0, true);
const lengthBytes = await streamReader.read(4);
if (!lengthBytes) {
throw new Error('Unexpected EOF reading section length');
}
const lengthView = new DataView(lengthBytes.buffer, lengthBytes.byteOffset, lengthBytes.byteLength);
const length = lengthView.getUint32(0, true);
const content = await streamReader.read(length);
if (!content) {
throw new Error(`Unexpected EOF reading section content. Expected ${length} bytes.`);
}
switch (label) {
case TemplateSectionLabel.Configurations: {
config = overrideConfig
? { ...decodeJSONMap(content), ...overrideConfig }
: decodeJSONMap(content);
postMessage({ type: 'section', label, url, data: config });
break;
}
case TemplateSectionLabel.StyleInfo: {
await wasmModuleLoadedPromise;
const buffer = wasmInstance.decode_style_info(content, config['isLazy'] === 'true' ? url : undefined, config['enableCSSSelector'] === 'true', transformVW, transformVH, transformREM);
postMessage({
type: 'section',
label,
url,
data: buffer.buffer,
config,
}, {
transfer: [buffer.buffer],
});
break;
}
case TemplateSectionLabel.LepusCode: {
const codeMap = decodeBinaryMap(content);
const isLazy = config['isLazy'] === 'true';
const blobMap = {};
for (const [key, code] of Object.entries(codeMap)) {
const blob = new Blob([
'//# allFunctionsCalledOnLoad\n(function(){ "use strict"; const navigator=void 0,postMessage=void 0,window=void 0; ',
isLazy ? 'module.exports=' : '',
code,
' \n })()\n//# sourceURL=',
url,
'/',
key,
'\n',
], {
type: 'text/javascript; charset=utf-8',
});
blobMap[key] = URL.createObjectURL(blob);
}
postMessage({ type: 'section', label, url, data: blobMap, config });
break;
}
case TemplateSectionLabel.ElementTemplates: {
postMessage({ type: 'section', label, url, data: content }, [content.buffer]);
break;
}
case TemplateSectionLabel.CustomSections: {
postMessage({ type: 'section', label, url, data: content.buffer }, {
transfer: [content.buffer],
});
break;
}
case TemplateSectionLabel.Manifest: {
const codeMap = decodeBinaryMap(content);
const blobMap = {};
for (const [key, code] of Object.entries(codeMap)) {
const blob = new Blob([
code,
'//# sourceURL=',
url,
'/',
key,
], {
type: 'text/javascript; charset=utf-8',
});
blobMap[key] = URL.createObjectURL(blob);
}
postMessage({ type: 'section', label, url, data: blobMap });
break;
}
default:
throw new Error(`Unknown section label: ${label}`);
}
}
}
async function handleJSON(json, url, transformVW, transformVH, transformREM, overrideConfig) {
// Configurations
let config = {};
if (json.pageConfig) {
config = { ...json.pageConfig };
}
if (json.lepusCode?.root && typeof json.lepusCode.root === 'string') {
const appType = json.appType
?? (json.lepusCode.root.startsWith('(function (globDynamicComponentEntry')
? 'lazy'
: 'card');
config.appType = config.appType ?? appType;
config.isLazy = (appType === 'card') ? 'false' : 'true';
}
if (overrideConfig) {
config = { ...config, ...overrideConfig };
}
config = Object.fromEntries(Object.entries(config).map(([key, value]) => [key, value.toString()]));
postMessage({
type: 'section',
label: TemplateSectionLabel.Configurations,
url,
data: config,
});
// StyleInfo
if (json.styleInfo) {
await wasmModuleLoadedPromise;
const buffer = loadStyleFromJSON(json.styleInfo, config['enableCSSSelector'] === 'true', transformVW, transformVH, transformREM, config['isLazy'] === 'true' ? url : undefined);
postMessage({
type: 'section',
label: TemplateSectionLabel.StyleInfo,
url,
data: buffer.buffer,
config,
}, {
transfer: [buffer.buffer],
});
}
// LepusCode
if (json.lepusCode) {
// Flattened structure in json: { root: "...", chunk1: "..." }
const isLazy = config['isLazy'] === 'true';
const blobMap = {};
for (const [key, code] of Object.entries(json.lepusCode)) {
if (typeof code !== 'string')
continue;
const prefix = `//# allFunctionsCalledOnLoad\n(function(){ "use strict"; const navigator=void 0,postMessage=void 0,window=void 0; ${isLazy ? 'module.exports=' : ''} `;
const suffix = ` \n })()\n//# sourceURL=${url}/${key}\n`;
const blob = new Blob([prefix, code, suffix], {
type: 'text/javascript; charset=utf-8',
});
blobMap[key] = URL.createObjectURL(blob);
}
postMessage({
type: 'section',
label: TemplateSectionLabel.LepusCode,
url,
data: blobMap,
config,
});
}
// Manifest
if (json.manifest) {
const blobMap = {};
for (const [key, code] of Object.entries(json.manifest)) {
if (typeof code !== 'string')
continue;
const blob = new Blob([code], {
type: 'text/javascript;',
});
blobMap[key] = URL.createObjectURL(blob);
}
postMessage({
type: 'section',
label: TemplateSectionLabel.Manifest,
url,
data: blobMap,
});
}
// CustomSections
if (json.customSections) {
// Currently we don't have a way to encode custom sections here.
// If main thread accepts generic object, we send it.
// But TemplateManager expects buffer?
// TemplateManager: case CustomSections: #setCustomSection(url, data). data: any.
// So passing object is fine!
postMessage({
type: 'section',
label: TemplateSectionLabel.CustomSections,
url,
data: json.customSections,
});
}
// ElementTemplates
if (json.elementTemplates && Object.keys(json.elementTemplates).length > 0) {
// TemplateManager expects Uint8Array for ElementTemplates.
// We can't support this easily for JSON.
throw new Error('ElementTemplates in JSON artifacts are not supported yet.');
}
}
postMessage({ type: 'ready' });
//# sourceMappingURL=decode.worker.js.map