Advance M7 workflows and release operations
This commit is contained in:
139
web/app/src/engine-client/browser-engine-variant-session.ts
Normal file
139
web/app/src/engine-client/browser-engine-variant-session.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user