UNPKG

@lynx-js/web-core

Version:

This is an internal experimental package, do not use

326 lines 12.9 kB
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