Files
workinf_Blender_Wasm/web/app/src/engine-client/browser-engine-variant-session.ts
mes123456 7c16b279ae
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M7 workflows and release operations
2026-08-15 17:43:53 -04:00

140 lines
5.1 KiB
TypeScript

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<BrowserVariantModule>;
async function verifiedBytes(url: string, expectedSha256: string, controllers: Set<AbortController>): Promise<ArrayBuffer> {
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<ArrayBuffer> {
readonly variant: WebEngineVariantV2;
private readonly module: BrowserVariantModule;
private readonly pendingRequests: Set<AbortController>;
private readonly timers = new Set<ReturnType<typeof setTimeout>>();
private handle: number;
constructor(
variant: WebEngineVariantV2,
module: BrowserVariantModule,
handle: number,
pendingRequests: Set<AbortController>,
) {
this.variant = variant;
this.module = module;
this.handle = handle;
this.pendingRequests = pendingRequests;
}
async openProject(project: ArrayBuffer): Promise<void> {
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<EngineVariantResourceState> {
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<EngineVariantSession<ArrayBuffer>> {
const controllers = new Set<AbortController>();
const verifiedResources = new Map<string, ArrayBuffer>();
const verify = async (resource: { url: string; sha256: string }): Promise<ArrayBuffer> => {
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);
}