feat(web): preload completed and load debug wasm modules on demand
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user