feat(web): preload completed and load debug wasm modules on demand
This commit is contained in:
40
web/app/public/wasm-module-manifest.json
Normal file
40
web/app/public/wasm-module-manifest.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"releaseId": "blender-wasm-0.1.0-rc.1",
|
||||
"modules": [
|
||||
{
|
||||
"id": "core-completed",
|
||||
"lifecycle": "complete",
|
||||
"preload": true,
|
||||
"resources": {
|
||||
"single": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/web_engine.wasm",
|
||||
"sha256": "66fc59cba55a8c17d398b74d12ea4a9f09746c0f30c40f579c44aa34956e3072"
|
||||
},
|
||||
"pthread": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/pthread/web_engine.wasm",
|
||||
"sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "debug-incremental",
|
||||
"lifecycle": "debug",
|
||||
"preload": false,
|
||||
"resources": {
|
||||
"single": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/single/web_engine.wasm",
|
||||
"sha256": "7f3a50f1d41b2ac0e6366e84caaabefc3cdfc47c055d5ea459abec654da6fcc0"
|
||||
},
|
||||
"pthread": {
|
||||
"fileName": "web_engine.wasm",
|
||||
"url": "/vendor/blender/pthread/web_engine.wasm",
|
||||
"sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
AssetRequestResult,
|
||||
MeshGeometryBuffer,
|
||||
WebEngineEditCommand,
|
||||
WebEngineJsonCommand,
|
||||
WebEngineLODResult,
|
||||
WebEngineRequest,
|
||||
WebEngineResponse,
|
||||
@@ -82,6 +83,12 @@ export class WebEngineClient {
|
||||
return (await this.request({ type: "init" })).status;
|
||||
}
|
||||
|
||||
/** Load one debug WASM module without replacing the completed engine module. */
|
||||
async loadDebugWasmModule(id: string): Promise<NonNullable<WebEngineResult["wasmModules"]>> {
|
||||
const result = await this.request({ type: "loadWasmModule", id });
|
||||
return result.wasmModules ?? [];
|
||||
}
|
||||
|
||||
async openBlend(buffer: ArrayBuffer, onProgress?: (progress: ProgressEvent) => void, signal?: AbortSignal): Promise<BlendOpenResult> {
|
||||
if (buffer.byteLength === 0) {
|
||||
throw this.report("INVALID_ARGUMENT", "无法打开空的 .blend 文件", true);
|
||||
@@ -101,6 +108,15 @@ export class WebEngineClient {
|
||||
return { status: result.status, snapshot: result.snapshot, geometryBuffers: [...this.geometryBuffers], nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers] };
|
||||
}
|
||||
|
||||
/** Invoke the stable read-only cmd/JSON facade without adding a native registration. */
|
||||
async callJson<T>(cmd: WebEngineJsonCommand, params: Record<string, unknown> = {}): Promise<{ status: WebEngineStatus; result: T }> {
|
||||
const result = await this.request({ type: "jsonCommand", cmd, params });
|
||||
if (result.json === undefined) throw this.report("INVALID_ARGUMENT", `WebEngine JSON command returned no result: ${cmd}`, true);
|
||||
if (result.geometryBuffers) this.geometryBuffers = result.geometryBuffers;
|
||||
if (result.nonMeshGeometryBuffers) this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers;
|
||||
return { status: result.status, result: result.json as T };
|
||||
}
|
||||
|
||||
async delta(): Promise<{ status: WebEngineStatus; delta: SceneDelta }> {
|
||||
const result = await this.request({ type: "delta" });
|
||||
if (!result.delta) throw this.report("INVALID_ARGUMENT", "WebEngine 未返回 SceneDelta", true);
|
||||
|
||||
74
web/app/src/engine-client/wasm-module-loader.ts
Normal file
74
web/app/src/engine-client/wasm-module-loader.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { validateWasmModuleManifest, type WasmModuleEntry, type WasmModuleManifest, type WasmModuleVariant } from "../../../protocol/wasm-modules";
|
||||
|
||||
export type WasmModuleLoadStatus = "pending" | "loaded" | "failed";
|
||||
|
||||
export interface WasmModuleState {
|
||||
id: string;
|
||||
lifecycle: WasmModuleEntry["lifecycle"];
|
||||
status: WasmModuleLoadStatus;
|
||||
byteLength: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface WasmModuleLoader {
|
||||
preloadCompleted(): Promise<WasmModuleState[]>;
|
||||
load(id: string): Promise<ArrayBuffer>;
|
||||
loadDebug(id: string): Promise<ArrayBuffer>;
|
||||
state(): WasmModuleState[];
|
||||
manifest(): WasmModuleManifest;
|
||||
}
|
||||
|
||||
async function fetchVerified(resource: { url: string; sha256: string }, fetcher: typeof fetch): Promise<ArrayBuffer> {
|
||||
const response = await fetcher(resource.url, { cache: "force-cache" });
|
||||
if (!response.ok) throw new Error(`WASM_MODULE_REQUEST_FAILED: ${resource.url}:${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 !== resource.sha256) throw new Error(`WASM_MODULE_HASH_MISMATCH: ${resource.url}`);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function createWasmModuleLoader(
|
||||
rawManifest: unknown,
|
||||
variant: WasmModuleVariant,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): WasmModuleLoader {
|
||||
const manifest = validateWasmModuleManifest(rawManifest);
|
||||
const bytes = new Map<string, ArrayBuffer>();
|
||||
const statuses = new Map<string, WasmModuleState>(manifest.modules.map((entry) => [entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "pending", byteLength: 0 }]));
|
||||
const loading = new Map<string, Promise<ArrayBuffer>>();
|
||||
const load = (entry: WasmModuleEntry): Promise<ArrayBuffer> => {
|
||||
const existing = bytes.get(entry.id);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
const active = loading.get(entry.id);
|
||||
if (active) return active;
|
||||
const promise = fetchVerified(entry.resources[variant], fetcher).then((value) => {
|
||||
bytes.set(entry.id, value);
|
||||
statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "loaded", byteLength: value.byteLength });
|
||||
return value;
|
||||
}).catch((error: unknown) => {
|
||||
statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "failed", byteLength: 0, error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}).finally(() => loading.delete(entry.id));
|
||||
loading.set(entry.id, promise);
|
||||
return promise;
|
||||
};
|
||||
return {
|
||||
async preloadCompleted() {
|
||||
await Promise.all(manifest.modules.filter((entry) => entry.lifecycle === "complete" && entry.preload).map(load));
|
||||
return [...statuses.values()].map((state) => ({ ...state }));
|
||||
},
|
||||
load: async (id) => {
|
||||
const entry = manifest.modules.find((candidate) => candidate.id === id);
|
||||
if (!entry) throw new Error(`WASM_MODULE_NOT_FOUND: ${id}`);
|
||||
return load(entry);
|
||||
},
|
||||
async loadDebug(id) {
|
||||
const entry = manifest.modules.find((candidate) => candidate.id === id);
|
||||
if (!entry || entry.lifecycle !== "debug") throw new Error(`WASM_MODULE_DEBUG_NOT_FOUND: ${id}`);
|
||||
return load(entry);
|
||||
},
|
||||
state: () => [...statuses.values()].map((state) => ({ ...state })),
|
||||
manifest: () => manifest,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ErrorReport } from "../../../protocol/error";
|
||||
import { parseSceneDelta, type SceneDelta } from "../../../protocol/scene-delta";
|
||||
import type { MeshGeometryBuffer, WebEngineEditCommand, WebEngineLODError, WebEngineLODLevelResult, WebEngineOpenResourceStatus, WebEngineRequest, WebEngineResponse, WebEngineResult, WebEngineStatus } from "../../../protocol/web-engine";
|
||||
import type { MeshGeometryBuffer, WebEngineEditCommand, WebEngineJsonCommand, WebEngineLODError, WebEngineLODLevelResult, WebEngineOpenResourceStatus, WebEngineRequest, WebEngineResponse, WebEngineResult, WebEngineStatus } from "../../../protocol/web-engine";
|
||||
import { parseSceneSnapshotIR } from "../../../protocol/scene-ir";
|
||||
import { parseEditorWorkflow, EditorWorkflowValidationError } from "../../../protocol/editor-workflow";
|
||||
import { parseLODManifest, type SimplifyResult } from "../../../protocol/simplify";
|
||||
import type { LODGenerationRequest } from "../../../protocol/lod";
|
||||
import { evaluateSkinSimplifyGate } from "../../../protocol/skin";
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
sha256LibraryBytes,
|
||||
type LibraryAppendClosureIR,
|
||||
} from "../../../protocol/library-main-append";
|
||||
import { createWasmModuleLoader, type WasmModuleLoader } from "../engine-client/wasm-module-loader";
|
||||
|
||||
type WasmModule = {
|
||||
_malloc: (size: number) => number;
|
||||
@@ -106,6 +108,7 @@ let wasmFactory: WasmFactory | null = null;
|
||||
let wasmBinary: ArrayBuffer | null = null;
|
||||
let handle = 0;
|
||||
let initializationPromise: Promise<WebEngineStatus> | null = null;
|
||||
let wasmModuleLoader: WasmModuleLoader | null = null;
|
||||
let currentSnapshot: ReturnType<typeof parseSceneSnapshotIR> | null = null;
|
||||
let currentGeometryBuffers: MeshGeometryBuffer[] = [];
|
||||
let sourceBlendBuffer: ArrayBuffer | null = null;
|
||||
@@ -909,7 +912,7 @@ function nativeError(fallbackCode: ErrorReport["code"]): ErrorReport {
|
||||
const message = module && module._web_engine_last_error_message() !== 0
|
||||
? module.UTF8ToString(module._web_engine_last_error_message())
|
||||
: "WebEngine native call failed";
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : fallbackCode;
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : message.startsWith("GN_NODE_UNSUPPORTED") ? "CAPABILITY_MISSING" : fallbackCode;
|
||||
return report(mappedCode, message);
|
||||
}
|
||||
|
||||
@@ -918,7 +921,7 @@ function nativeErrorFor(target: WasmModule, fallbackCode: ErrorReport["code"]):
|
||||
const message = target._web_engine_last_error_message() !== 0
|
||||
? target.UTF8ToString(target._web_engine_last_error_message())
|
||||
: "WebEngine native call failed";
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : fallbackCode;
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : message.startsWith("GN_NODE_UNSUPPORTED") ? "CAPABILITY_MISSING" : fallbackCode;
|
||||
return report(mappedCode, message);
|
||||
}
|
||||
|
||||
@@ -950,12 +953,15 @@ async function initialize(): Promise<WebEngineStatus> {
|
||||
if (module) return status();
|
||||
if (!initializationPromise) {
|
||||
initializationPromise = (async () => {
|
||||
const moduleManifest = await fetch("/wasm-module-manifest.json", { cache: "force-cache" }).then(async (response) => {
|
||||
if (!response.ok) throw new Error(`wasm-module-manifest request failed: ${response.status}`);
|
||||
return response.json();
|
||||
});
|
||||
wasmModuleLoader = createWasmModuleLoader(moduleManifest, "single");
|
||||
await wasmModuleLoader.preloadCompleted();
|
||||
const imported = await import("../vendor/blender/web_engine.js") as unknown as { default: WasmFactory };
|
||||
const factory = imported.default;
|
||||
const binary = await fetch("/vendor/blender/web_engine.wasm?v=2").then((response) => {
|
||||
if (!response.ok) throw new Error(`web_engine.wasm request failed: ${response.status}`);
|
||||
return response.arrayBuffer();
|
||||
});
|
||||
const binary = await wasmModuleLoader.load("core-completed");
|
||||
const initializedModule = await factory({ wasmBinary: binary });
|
||||
const initializedHandle = initializedModule._web_engine_create();
|
||||
if (initializedHandle <= 0) throw new Error("WebEngine handle creation failed");
|
||||
@@ -967,6 +973,7 @@ async function initialize(): Promise<WebEngineStatus> {
|
||||
})().catch((error: unknown) => {
|
||||
module = null;
|
||||
handle = 0;
|
||||
wasmModuleLoader = null;
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
initializationPromise = null;
|
||||
@@ -1017,8 +1024,24 @@ function callUnifiedJson<T>(cmd: string, params: Record<string, unknown> = {}, t
|
||||
}
|
||||
}
|
||||
|
||||
const READ_ONLY_JSON_COMMANDS: ReadonlySet<WebEngineJsonCommand> = new Set(["snapshot", "delta", "depsgraph", "memoryStats"]);
|
||||
const STATEFUL_JSON_COMMANDS: ReadonlySet<WebEngineJsonCommand> = new Set(["applyCommand", "undo", "redo"]);
|
||||
|
||||
function readSnapshot(): ReturnType<typeof parseSceneSnapshotIR> {
|
||||
return parseSceneSnapshotIR(callUnifiedJson<unknown>("snapshot"));
|
||||
const snapshot = parseSceneSnapshotIR(callUnifiedJson<unknown>("snapshot"));
|
||||
if (snapshot.editorWorkflowStatus === "AVAILABLE" && !snapshot.editorWorkflow) {
|
||||
throw report("EDITOR_LAYOUT_INVALID", "SceneIR editor workflow is marked AVAILABLE but has no payload");
|
||||
}
|
||||
if (snapshot.editorWorkflow) {
|
||||
try {
|
||||
parseEditorWorkflow(snapshot.editorWorkflow);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof EditorWorkflowValidationError) throw report(error.code, error.message);
|
||||
throw report("EDITOR_LAYOUT_INVALID", "SceneIR editor workflow validation failed");
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function readDelta(): SceneDelta {
|
||||
@@ -1279,7 +1302,7 @@ function cloneNonMeshGeometryBuffers(chunks: readonly NonMeshGeometryChunk[]): N
|
||||
}
|
||||
|
||||
function baseResult(): WebEngineResult {
|
||||
return { status: status() };
|
||||
return { status: status(), wasmModules: wasmModuleLoader?.state() };
|
||||
}
|
||||
|
||||
function publishFullScene(scene: SceneReadResult): Pick<WebEngineResult, "snapshot" | "geometryBuffers" | "nonMeshGeometryBuffers"> {
|
||||
@@ -1399,6 +1422,13 @@ async function handleRequest(event: MessageEvent<WebEngineRequest>): Promise<voi
|
||||
await initialize();
|
||||
result = baseResult();
|
||||
break;
|
||||
case "loadWasmModule": {
|
||||
await initialize();
|
||||
if (!wasmModuleLoader) throw report("WASM_INIT_FAILED", "WASM module manifest is unavailable", true);
|
||||
await wasmModuleLoader.loadDebug(request.command.id);
|
||||
result = { ...baseResult(), wasmModules: wasmModuleLoader.state() };
|
||||
break;
|
||||
}
|
||||
case "openBlend": {
|
||||
const resource = { liveInputBytes: request.command.buffer.byteLength, liveNativeHandles: 0, liveStagingFiles: 0 };
|
||||
activeOpenResources.set(request.requestId, resource);
|
||||
@@ -1478,6 +1508,35 @@ async function handleRequest(event: MessageEvent<WebEngineRequest>): Promise<voi
|
||||
result = { status: status(), ...publishFullScene(scene) };
|
||||
}
|
||||
break;
|
||||
case "jsonCommand": {
|
||||
await initialize();
|
||||
if (!READ_ONLY_JSON_COMMANDS.has(request.command.cmd) && !STATEFUL_JSON_COMMANDS.has(request.command.cmd)) {
|
||||
throw report("INVALID_ARGUMENT", `Unsupported JSON command: ${request.command.cmd}`, true);
|
||||
}
|
||||
if (STATEFUL_JSON_COMMANDS.has(request.command.cmd) && request.command.cmd === "applyCommand" &&
|
||||
(!request.command.params || typeof request.command.params.command !== "object" || request.command.params.command === null)) {
|
||||
throw report("INVALID_ARGUMENT", "cmd json applyCommand requires params.command", true);
|
||||
}
|
||||
const jsonResult = callUnifiedJson<unknown>(request.command.cmd, request.command.params ?? {});
|
||||
if (STATEFUL_JSON_COMMANDS.has(request.command.cmd)) {
|
||||
const scene = await readSnapshotResult();
|
||||
currentSnapshot = scene.snapshot;
|
||||
result = {
|
||||
status: status(),
|
||||
json: jsonResult,
|
||||
snapshot: scene.snapshot,
|
||||
delta: readDelta(),
|
||||
geometryBuffers: scene.geometryBuffers,
|
||||
nonMeshGeometryBuffers: scene.nonMeshGeometryBuffers,
|
||||
};
|
||||
break;
|
||||
}
|
||||
result = {
|
||||
status: status(),
|
||||
json: jsonResult,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "delta":
|
||||
await initialize();
|
||||
result = { status: status(), delta: readDelta() };
|
||||
|
||||
69
web/protocol/wasm-modules.ts
Normal file
69
web/protocol/wasm-modules.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export const WASM_MODULE_MANIFEST_SCHEMA = 1 as const;
|
||||
|
||||
export type WasmModuleLifecycle = "complete" | "debug";
|
||||
export type WasmModuleVariant = "single" | "pthread";
|
||||
|
||||
export interface WasmModuleResource {
|
||||
fileName: string;
|
||||
url: string;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface WasmModuleEntry {
|
||||
id: string;
|
||||
lifecycle: WasmModuleLifecycle;
|
||||
preload: boolean;
|
||||
resources: Record<WasmModuleVariant, WasmModuleResource>;
|
||||
}
|
||||
|
||||
export interface WasmModuleManifest {
|
||||
schemaVersion: typeof WASM_MODULE_MANIFEST_SCHEMA;
|
||||
releaseId: string;
|
||||
modules: WasmModuleEntry[];
|
||||
}
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function resource(value: unknown, path: string): WasmModuleResource {
|
||||
if (!record(value) || typeof value.fileName !== "string" || !NAME.test(value.fileName) || !value.fileName.endsWith(".wasm") ||
|
||||
typeof value.url !== "string" || !value.url.startsWith("/") || /[?#\\]/.test(value.url) ||
|
||||
typeof value.sha256 !== "string" || !SHA256.test(value.sha256)) {
|
||||
throw new Error(`WASM_MODULE_MANIFEST_INVALID: ${path}`);
|
||||
}
|
||||
const segments = value.url.slice(1).split("/");
|
||||
if (segments.some((segment) => !NAME.test(segment)) || segments.at(-1) !== value.fileName) {
|
||||
throw new Error(`WASM_MODULE_MANIFEST_INVALID: ${path}.url`);
|
||||
}
|
||||
return { fileName: value.fileName, url: value.url, sha256: value.sha256 };
|
||||
}
|
||||
|
||||
export function validateWasmModuleManifest(value: unknown): WasmModuleManifest {
|
||||
if (!record(value) || value.schemaVersion !== WASM_MODULE_MANIFEST_SCHEMA || typeof value.releaseId !== "string" || !NAME.test(value.releaseId) || !Array.isArray(value.modules)) {
|
||||
throw new Error("WASM_MODULE_MANIFEST_INVALID");
|
||||
}
|
||||
const ids = new Set<string>();
|
||||
const modules = value.modules.map((entry, index): WasmModuleEntry => {
|
||||
if (!record(entry) || typeof entry.id !== "string" || !NAME.test(entry.id) || ids.has(entry.id) ||
|
||||
(entry.lifecycle !== "complete" && entry.lifecycle !== "debug") || typeof entry.preload !== "boolean" ||
|
||||
!record(entry.resources) || !entry.resources.single || !entry.resources.pthread) {
|
||||
throw new Error(`WASM_MODULE_MANIFEST_INVALID: modules[${index}]`);
|
||||
}
|
||||
ids.add(entry.id);
|
||||
if (entry.lifecycle === "debug" && entry.preload) throw new Error(`WASM_MODULE_MANIFEST_INVALID: modules[${index}].preload`);
|
||||
return {
|
||||
id: entry.id,
|
||||
lifecycle: entry.lifecycle,
|
||||
preload: entry.preload,
|
||||
resources: {
|
||||
single: resource(entry.resources.single, `modules[${index}].resources.single`),
|
||||
pthread: resource(entry.resources.pthread, `modules[${index}].resources.pthread`),
|
||||
},
|
||||
};
|
||||
});
|
||||
return { schemaVersion: WASM_MODULE_MANIFEST_SCHEMA, releaseId: value.releaseId, modules };
|
||||
}
|
||||
48
web/tests/unit/wasm-module-loader.test.mjs
Normal file
48
web/tests/unit/wasm-module-loader.test.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "../../..");
|
||||
const loaderPath = path.join(root, "web/app/src/engine-client/wasm-module-loader.ts");
|
||||
const protocolPath = path.join(root, "web/protocol/wasm-modules.ts");
|
||||
const transpile = (file) => ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: file,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
const protocol = transpile(protocolPath);
|
||||
assert.deepEqual(protocol.diagnostics, []);
|
||||
const loader = transpile(loaderPath);
|
||||
assert.deepEqual(loader.diagnostics, []);
|
||||
const loaderSource = loader.outputText.replace('from "../../../protocol/wasm-modules"', `from "data:text/javascript;base64,${Buffer.from(protocol.outputText).toString("base64")}"`);
|
||||
const loaderModule = await import(`data:text/javascript;base64,${Buffer.from(loaderSource).toString("base64")}`);
|
||||
|
||||
const bytes = new TextEncoder().encode("completed");
|
||||
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
||||
const sha256 = [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
releaseId: "blender-wasm-0.1.0-rc.1",
|
||||
modules: [
|
||||
{ id: "complete", lifecycle: "complete", preload: true, resources: { single: { fileName: "complete.wasm", url: "/complete.wasm", sha256 }, pthread: { fileName: "complete.wasm", url: "/complete.wasm", sha256 } } },
|
||||
{ id: "debug", lifecycle: "debug", preload: false, resources: { single: { fileName: "debug.wasm", url: "/debug.wasm", sha256 }, pthread: { fileName: "debug.wasm", url: "/debug.wasm", sha256 } } },
|
||||
],
|
||||
};
|
||||
const fetcher = async (url) => new Response(bytes, { status: 200 });
|
||||
|
||||
test("completed modules preload once and debug modules load independently", async () => {
|
||||
const loader = loaderModule.createWasmModuleLoader(manifest, "single", fetcher);
|
||||
const states = await loader.preloadCompleted();
|
||||
assert.equal(states.find((state) => state.id === "complete")?.status, "loaded");
|
||||
assert.equal(states.find((state) => state.id === "debug")?.status, "pending");
|
||||
assert.equal((await loader.load("complete")).byteLength, bytes.byteLength);
|
||||
assert.equal((await loader.loadDebug("debug")).byteLength, bytes.byteLength);
|
||||
assert.equal(loader.state().find((state) => state.id === "debug")?.status, "loaded");
|
||||
});
|
||||
|
||||
test("debug loading rejects a completed module id", async () => {
|
||||
const loader = loaderModule.createWasmModuleLoader(manifest, "single", fetcher);
|
||||
await assert.rejects(() => loader.loadDebug("complete"), /WASM_MODULE_DEBUG_NOT_FOUND/);
|
||||
});
|
||||
Reference in New Issue
Block a user