196 lines
6.6 KiB
TypeScript
196 lines
6.6 KiB
TypeScript
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";
|
|
}
|