Advance M7 workflows and release operations
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

This commit is contained in:
mes123456
2026-08-15 17:43:53 -04:00
parent 17ab961485
commit 7c16b279ae
103 changed files with 8064 additions and 429 deletions

View File

@@ -8,6 +8,7 @@ import type {
WebEngineRequest,
WebEngineResponse,
WebEngineResult,
WebEngineOpenResourceStatus,
WebEngineStatus,
} from "../../../protocol/web-engine";
import type { LODGenerationRequest } from "../../../protocol/lod";
@@ -25,6 +26,7 @@ interface PendingRequest {
reject: (error: ErrorReport) => void;
onProgress?: (progress: ProgressEvent) => void;
timer: ReturnType<typeof setTimeout>;
cleanup?: () => void;
}
export interface WebEngineClientOptions {
@@ -61,11 +63,11 @@ export class WebEngineClient {
return (await this.request({ type: "init" })).status;
}
async openBlend(buffer: ArrayBuffer, onProgress?: (progress: ProgressEvent) => void): Promise<BlendOpenResult> {
async openBlend(buffer: ArrayBuffer, onProgress?: (progress: ProgressEvent) => void, signal?: AbortSignal): Promise<BlendOpenResult> {
if (buffer.byteLength === 0) {
throw this.report("INVALID_ARGUMENT", "无法打开空的 .blend 文件", true);
}
const result = await this.request({ type: "openBlend", buffer }, [buffer], onProgress);
const result = await this.request({ type: "openBlend", buffer }, [buffer], onProgress, signal);
if (!result.snapshot) throw this.report("BLEND_READ_FAILED", "WebEngine 未返回 SceneIR", true);
this.geometryBuffers = result.geometryBuffers ?? [];
this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? [];
@@ -146,6 +148,12 @@ export class WebEngineClient {
return result.blend;
}
async openResourceStatus(): Promise<WebEngineOpenResourceStatus> {
const result = await this.request({ type: "openResourceStatus" });
if (!result.openResources) throw this.report("INVALID_ARGUMENT", "WebEngine open resource status missing", true);
return result.openResources;
}
terminate(): void {
this.failPending(this.report("WORKER_TERMINATED", "WebEngineWorker 已关闭", true));
this.worker?.terminate();
@@ -169,16 +177,29 @@ export class WebEngineClient {
command: WebEngineRequest["command"],
transfer: Transferable[] = [],
onProgress?: (progress: ProgressEvent) => void,
signal?: AbortSignal,
): Promise<WebEngineResult> {
const worker = this.start();
const requestId = `web-engine-${++this.requestCounter}`;
const request = { requestId, command } as WebEngineRequest;
return new Promise<WebEngineResult>((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.get(requestId)?.cleanup?.();
this.pending.delete(requestId);
reject(this.report("WORKER_TERMINATED", `WebEngine 请求超时: ${command.type}`, true));
}, this.timeoutMs);
this.pending.set(requestId, { resolve, reject, onProgress, timer });
const abort = (): void => {
worker.postMessage({
requestId: `web-engine-cancel-${++this.requestCounter}`,
command: { type: "cancelOpen", targetRequestId: requestId },
} satisfies WebEngineRequest);
};
const cleanup = signal ? () => signal.removeEventListener("abort", abort) : undefined;
this.pending.set(requestId, { resolve, reject, onProgress, timer, cleanup });
if (signal) {
signal.addEventListener("abort", abort, { once: true });
if (signal.aborted) abort();
}
worker.postMessage(request, transfer);
});
}
@@ -192,6 +213,7 @@ export class WebEngineClient {
}
this.pending.delete(response.requestId);
clearTimeout(pending.timer);
pending.cleanup?.();
if (response.ok) pending.resolve(response.result);
else pending.reject(response.error);
}
@@ -199,6 +221,7 @@ export class WebEngineClient {
private failPending(error: ErrorReport): void {
for (const pending of this.pending.values()) {
clearTimeout(pending.timer);
pending.cleanup?.();
pending.reject(error);
}
this.pending.clear();

View 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);
}

View File

@@ -0,0 +1,195 @@
import {
bindWebEngineRelease,
selectWebEngineVariant,
type WasmThreadingCapabilities,
type WebEngineReleaseBinding,
type WebEngineVariantPolicy,
} from "../../../protocol/engine-variant";
import type { CapabilityGateResult } from "../../../protocol/capability-gates";
import type { WebEngineManifestV2, WebEngineVariantV2 } from "../../../protocol/manifest";
export interface EngineVariantResourceState {
handles: number;
workers: number;
timers: number;
pendingRequests: number;
}
export interface EngineVariantSession<Project = ArrayBuffer> {
readonly variant: WebEngineVariantV2;
openProject(project: Project): Promise<void>;
dispose(): Promise<EngineVariantResourceState>;
resourceState(): EngineVariantResourceState;
testOnlySeedTrackedResources?(): void;
}
export interface EngineVariantFallbackReason {
code: "PTHREAD_INITIALIZATION_FAILED";
message: string;
}
export interface EngineVariantBootstrapResult {
status: "READY";
selected: "single" | "pthread";
attempted: Array<"single" | "pthread">;
fallbackReason: EngineVariantFallbackReason | null;
pthreadGate: CapabilityGateResult;
openCount: number;
failedAttemptCleanup: EngineVariantResourceState | null;
}
export interface EngineVariantBootstrapOutcome<Project = ArrayBuffer> {
result: EngineVariantBootstrapResult;
session: EngineVariantSession<Project>;
}
export type WebEngineRefreshRequired = Extract<WebEngineReleaseBinding, { status: "REFRESH_REQUIRED" }>;
export type WebEngineReleaseBootstrapOutcome<Project = ArrayBuffer> =
| EngineVariantBootstrapOutcome<Project>
| { result: WebEngineRefreshRequired; session: null };
export interface EngineVariantBootstrapDependencies<Project = ArrayBuffer> {
initialize(variant: WebEngineVariantV2): Promise<EngineVariantSession<Project>>;
}
export interface EngineVariantBootstrapTestHooks {
failPthreadAfterInitialize?: boolean;
seedTrackedPthreadResources?: boolean;
}
export class EngineVariantLoadError extends Error {
readonly code: "ENGINE_VARIANT_RESOURCE_REQUEST_FAILED" | "ENGINE_VARIANT_RESOURCE_HASH_MISMATCH";
constructor(code: EngineVariantLoadError["code"], message: string, options?: ErrorOptions) {
super(message, options);
this.name = "EngineVariantLoadError";
this.code = code;
}
}
export class EngineVariantBootstrapError extends Error {
readonly code: "ENGINE_VARIANT_INTEGRITY_FAILED";
readonly attempted: Array<"single" | "pthread">;
constructor(attempted: Array<"single" | "pthread">, cause: EngineVariantLoadError) {
super(`${cause.code}: ${cause.message}`, { cause });
this.name = "EngineVariantBootstrapError";
this.code = "ENGINE_VARIANT_INTEGRITY_FAILED";
this.attempted = [...attempted];
}
}
function failureMessage(error: unknown): string {
return error instanceof Error && error.message ? error.message : "pthread engine initialization failed";
}
function rejectIntegrityFailure(
error: unknown,
attempted: Array<"single" | "pthread">,
): void {
if (error instanceof EngineVariantLoadError && error.code === "ENGINE_VARIANT_RESOURCE_HASH_MISMATCH") {
throw new EngineVariantBootstrapError(attempted, error);
}
}
async function bootstrapSelectedWebEngineVariant<Project = ArrayBuffer>(
manifest: WebEngineManifestV2,
policy: WebEngineVariantPolicy,
capabilities: WasmThreadingCapabilities,
dependencies: EngineVariantBootstrapDependencies<Project>,
pendingProject?: Project,
testHooks?: EngineVariantBootstrapTestHooks,
): Promise<EngineVariantBootstrapOutcome<Project>> {
const selection = selectWebEngineVariant(manifest, policy, capabilities);
if (!selection.selectedVariant) throw new Error("PTHREAD_REQUIRED_CAPABILITY_BLOCKED");
const attempted: Array<"single" | "pthread"> = [];
let failedAttemptCleanup: EngineVariantResourceState | null = null;
let fallbackReason: EngineVariantFallbackReason | null = null;
let session: EngineVariantSession<Project> | null = null;
const initialize = async (variant: WebEngineVariantV2): Promise<EngineVariantSession<Project>> => {
attempted.push(variant.id);
return dependencies.initialize(variant);
};
try {
session = await initialize(selection.selectedVariant);
if (selection.selectedVariant.id === "pthread") {
if (testHooks?.seedTrackedPthreadResources) session.testOnlySeedTrackedResources?.();
if (testHooks?.failPthreadAfterInitialize) throw new Error("TEST_PTHREAD_INITIALIZATION_FAILURE");
}
}
catch (error) {
const failedSession = session;
if (failedSession) failedAttemptCleanup = await failedSession.dispose();
rejectIntegrityFailure(error, attempted);
if (policy !== "AUTO" || selection.selectedVariant.id !== "pthread") throw error;
fallbackReason = { code: "PTHREAD_INITIALIZATION_FAILED", message: failureMessage(error) };
const single = manifest.variants.find((variant) => variant.id === "single");
if (!single) throw new Error("ENGINE_MANIFEST_VARIANTS_MISSING", { cause: error });
try {
session = await initialize(single);
}
catch (fallbackError) {
rejectIntegrityFailure(fallbackError, attempted);
throw fallbackError;
}
}
if (!session) throw new Error("ENGINE_VARIANT_INITIALIZATION_FAILED");
let openCount = 0;
if (pendingProject !== undefined) {
try {
await session.openProject(pendingProject);
openCount = 1;
}
catch (error) {
await session.dispose();
throw error;
}
}
return {
result: {
status: "READY",
selected: session.variant.id,
attempted,
fallbackReason,
pthreadGate: selection.pthreadGate,
openCount,
failedAttemptCleanup,
},
session,
};
}
export async function bootstrapWebEngineRelease<Project = ArrayBuffer>(
expectedReleaseId: string,
manifest: WebEngineManifestV2,
policy: WebEngineVariantPolicy,
capabilities: WasmThreadingCapabilities,
dependencies: EngineVariantBootstrapDependencies<Project>,
pendingProject?: Project,
testHooks?: EngineVariantBootstrapTestHooks,
): Promise<WebEngineReleaseBootstrapOutcome<Project>> {
const binding = bindWebEngineRelease(expectedReleaseId, manifest);
if (binding.status === "REFRESH_REQUIRED") {
return { result: binding, session: null };
}
return bootstrapSelectedWebEngineVariant(
binding.manifest,
policy,
capabilities,
dependencies,
pendingProject,
testHooks,
);
}
export function engineVariantStatusLabel(result: EngineVariantBootstrapResult): string {
if (result.selected === "pthread") return "Runtime: pthread";
return result.fallbackReason
? `Runtime: single (pthread fallback: ${result.fallbackReason.code})`
: "Runtime: single";
}