import type { WebEngineVariantV2 } from "../../../protocol/manifest"; import type { EngineVariantResourceState, EngineVariantSession, } from "./engine-variant-bootstrap"; import { EngineVariantLoadError } from "./engine-variant-bootstrap"; interface EmscriptenPThreadRuntime { unusedWorkers: Worker[]; runningWorkers: Worker[]; terminateAllThreads(): void; } interface BrowserVariantModule { HEAPU8: Uint8Array; _malloc(size: number): number; _free(pointer: number): void; _web_engine_create(): number; _web_engine_destroy(handle: number): void; _web_engine_get_live_handles(): number; _web_engine_open_blend(handle: number, data: number, length: number): number; PThread?: EmscriptenPThreadRuntime; } type BrowserVariantFactory = (options: { wasmBinary: ArrayBuffer }) => Promise; async function verifiedBytes(url: string, expectedSha256: string, controllers: Set): Promise { const controller = new AbortController(); controllers.add(controller); try { const response = await fetch(url, { cache: "no-store", signal: controller.signal }); if (!response.ok) { throw new EngineVariantLoadError( "ENGINE_VARIANT_RESOURCE_REQUEST_FAILED", `${url} returned ${response.status}`, ); } const bytes = await response.arrayBuffer(); const digest = await crypto.subtle.digest("SHA-256", bytes); const actual = [...new Uint8Array(digest)] .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); if (actual !== expectedSha256) { throw new EngineVariantLoadError("ENGINE_VARIANT_RESOURCE_HASH_MISMATCH", url); } return bytes; } finally { controllers.delete(controller); } } class BrowserEngineVariantSession implements EngineVariantSession { readonly variant: WebEngineVariantV2; private readonly module: BrowserVariantModule; private readonly pendingRequests: Set; private readonly timers = new Set>(); private handle: number; constructor( variant: WebEngineVariantV2, module: BrowserVariantModule, handle: number, pendingRequests: Set, ) { this.variant = variant; this.module = module; this.handle = handle; this.pendingRequests = pendingRequests; } async openProject(project: ArrayBuffer): Promise { if (this.handle <= 0) throw new Error("ENGINE_VARIANT_SESSION_DISPOSED"); const pointer = this.module._malloc(project.byteLength); if (project.byteLength > 0 && pointer <= 0) throw new Error("ENGINE_VARIANT_PROJECT_ALLOCATION_FAILED"); try { if (project.byteLength > 0) this.module.HEAPU8.set(new Uint8Array(project), pointer); const result = this.module._web_engine_open_blend(this.handle, pointer, project.byteLength); if (result !== 0) throw new Error(`ENGINE_VARIANT_PROJECT_OPEN_FAILED: ${result}`); } finally { if (pointer > 0) this.module._free(pointer); } } resourceState(): EngineVariantResourceState { const pthread = this.module.PThread; return { handles: this.handle > 0 ? this.module._web_engine_get_live_handles() : 0, workers: pthread ? pthread.unusedWorkers.length + pthread.runningWorkers.length : 0, timers: this.timers.size, pendingRequests: this.pendingRequests.size, }; } testOnlySeedTrackedResources(): void { this.pendingRequests.add(new AbortController()); const timer = setTimeout(() => this.timers.delete(timer), 60_000); this.timers.add(timer); } async dispose(): Promise { for (const controller of this.pendingRequests) controller.abort(); this.pendingRequests.clear(); for (const timer of this.timers) clearTimeout(timer); this.timers.clear(); if (this.handle > 0) { this.module._web_engine_destroy(this.handle); this.handle = 0; } this.module.PThread?.terminateAllThreads(); return this.resourceState(); } } export async function createBrowserEngineVariantSession( variant: WebEngineVariantV2, ): Promise> { const controllers = new Set(); const verifiedResources = new Map(); const verify = async (resource: { url: string; sha256: string }): Promise => { const existing = verifiedResources.get(resource.url); if (existing) return existing; const bytes = await verifiedBytes(resource.url, resource.sha256, controllers); verifiedResources.set(resource.url, bytes); return bytes; }; await verify(variant.resources.js); const wasmBinary = await verify(variant.resources.wasm); if (variant.id === "pthread") await verify(variant.resources.pthreadWorker); const imported = await import(/* @vite-ignore */ variant.resources.js.url) as { default: BrowserVariantFactory }; const module = await imported.default({ wasmBinary }); const handle = module._web_engine_create(); if (handle <= 0) { module.PThread?.terminateAllThreads(); throw new Error("ENGINE_VARIANT_HANDLE_CREATION_FAILED"); } return new BrowserEngineVariantSession(variant, module, handle, controllers); }