import type { HostToKernelMessage, KernelToHostMessage, } from "../../bridge/protocol.ts"; import type { KernelInterruptHandle } from "../../tool/types.ts"; import { kernelInterruptMessage, kernelTimeoutMessage, } from "../shared/messages.ts"; import { createInlineWorker, type WorkerLike } from "./inline-worker.ts"; import { assertJavaScriptKernelOpen, type JavaScriptKernelMode, type JavaScriptRunInput, type LifecycleState, type ResultMessage, } from "./kernel-contract.ts"; import { type JavaScriptKernelOptions, LocalModuleLoader, localBridgeConnection, } from "./local-module-loader.ts"; import { JavaScriptRunQueue, type PendingJavaScriptRun, stoppedResult, } from "./run-queue.ts"; import { bridgeError, spawnJitlessWorker, spawnNodeWorker, WorkerStartupCancelledError, waitForReady, } from "./worker-host.ts"; export { JavaScriptKernelClosedError, type JavaScriptKernelMode, type JavaScriptRunInput, } from "./kernel-contract.ts"; export type { JavaScriptKernelOptions } from "./local-module-loader.ts"; export class JavaScriptKernel { readonly #options: JavaScriptKernelOptions; readonly #moduleLoader: LocalModuleLoader; #worker: WorkerLike | null = null; #mode: JavaScriptKernelMode = "worker"; #lifecycle: LifecycleState = "open"; #ready: Promise | null = null; #startupAbort: AbortController | null = null; #generation = 0; #activation: Promise | null = null; #recovery: Promise | null = null; #closePromise: Promise | null = null; readonly #runs = new JavaScriptRunQueue(); #timeout: NodeJS.Timeout | null = null; constructor(options: JavaScriptKernelOptions) { this.#options = options; this.#moduleLoader = new LocalModuleLoader(options); } get mode(): JavaScriptKernelMode { return this.#mode; } async run(input: JavaScriptRunInput): Promise { assertJavaScriptKernelOpen(this.#lifecycle, "run"); const promise = this.#runs.enqueue(input); this.#activate(); return await promise; } async interrupt(reason = "interrupted"): Promise { assertJavaScriptKernelOpen(this.#lifecycle, "interrupt"); const active = this.#runs.active; const target = this.#runs.takeInterruptTarget(); if (!target) { return { stateRetained: Promise.resolve(true) }; } if (target === active) { this.#clearTimeout(); } this.#runs.settle( target, stoppedResult( target.input.cellId, kernelInterruptMessage("JS cell", reason) ) ); await this.#restartAfterStop(); // A restart always replaces the worker VM, so no user global survives. return { stateRetained: Promise.resolve(false) }; } async reset(): Promise { assertJavaScriptKernelOpen(this.#lifecycle, "reset"); await this.#terminate(); assertJavaScriptKernelOpen(this.#lifecycle, "reset"); await this.#ensureReady(); this.#startNext(); } deliverToolReply( message: Extract ): void { if (this.#lifecycle === "open") { this.#worker?.postMessage(message); } } async close(): Promise { if (this.#closePromise) { return await this.#closePromise; } this.#worker?.postMessage({ type: "close" }); this.#lifecycle = "closing"; this.#runs.settleAll("JS kernel closed"); const recovery = this.#recovery; const closePromise = (async () => { if (recovery) { await recovery; } await this.#terminate(); })().finally(() => { this.#lifecycle = "closed"; }); this.#closePromise = closePromise; return await closePromise; } #activate(): void { if ( this.#activation || this.#lifecycle !== "open" || this.#runs.active || !this.#runs.hasWaiting ) { return; } const activation = this.#activateWhenReady(); this.#activation = activation; void activation.then(() => { if (this.#activation === activation) { this.#activation = null; } if ( this.#lifecycle === "open" && !this.#runs.active && this.#runs.hasWaiting ) { this.#activate(); } }); } async #activateWhenReady(): Promise { try { await this.#ensureReady(); if (this.#lifecycle === "open") { this.#startNext(); } } catch (error) { if (error instanceof WorkerStartupCancelledError) { return; } this.#runs.rejectWaiting( error instanceof Error ? error : new Error(String(error)) ); } } async #ensureReady(): Promise { assertJavaScriptKernelOpen(this.#lifecycle, "run"); if (!this.#ready) { this.#generation += 1; const generation = this.#generation; const controller = new AbortController(); this.#startupAbort = controller; const ready = this.#startWorker(generation, controller.signal); this.#ready = ready; void ready.then( () => { if (this.#ready === ready) { this.#startupAbort = null; } }, () => { if (this.#ready === ready) { this.#ready = null; this.#startupAbort = null; } } ); } return await this.#ready; } async #startWorker(generation: number, signal: AbortSignal): Promise { let worker = this.#spawnWorker(); this.#publishWorker(worker, generation); try { await this.#initializeWorker(worker, signal); return; } catch (error) { if ( !this.#isCurrent(worker, generation) || error instanceof WorkerStartupCancelledError ) { await worker.terminate(); throw new WorkerStartupCancelledError({ cause: error }); } if (worker.mode === "worker") { this.#worker = null; await worker.terminate(); } else { // Inline and jitless spawns must not be silently replaced: an inline // error is fatal, and falling back to a JIT worker would quietly // disable an explicit jitless setting. throw error; } } if (this.#lifecycle !== "open" || generation !== this.#generation) { throw new WorkerStartupCancelledError(); } worker = createInlineWorker( this.#options.cwd, this.#options.parallelPoolWidth, this.#options.hardenedCells ); this.#publishWorker(worker, generation); await this.#initializeWorker(worker, signal); } #spawnWorker(): WorkerLike { if (this.#options.jitless) { // V8 flags are fixed at process start: only a --jitless child process // can run the kernel without a JIT. Errors must surface, not fall back // to a JIT worker, so this branch bypasses the inline fallback. return spawnJitlessWorker( this.#options.cwd, this.#options.parallelPoolWidth, this.#options.hardenedCells ); } try { const url = this.#options.workerEntryUrl ?? new URL("./worker-entry.js", import.meta.url); return spawnNodeWorker( url, this.#options.cwd, this.#options.parallelPoolWidth, "worker", this.#options.hardenedCells ); } catch (error) { if (!(error instanceof Error)) { throw error; } return createInlineWorker( this.#options.cwd, this.#options.parallelPoolWidth, this.#options.hardenedCells ); } } #publishWorker(worker: WorkerLike, generation: number): void { if (this.#lifecycle !== "open" || generation !== this.#generation) { throw new WorkerStartupCancelledError(); } this.#worker = worker; this.#mode = worker.mode; worker.onMessage((message) => { if (this.#isCurrent(worker, generation)) { this.#handleMessage(message); } }); worker.onError((error) => { if (this.#isCurrent(worker, generation)) { this.#handleCrash(error); } }); } async #initializeWorker( worker: WorkerLike, signal: AbortSignal ): Promise { const ready = waitForReady(worker, signal); worker.postMessage({ type: "init", sessionId: this.#options.sessionId, connection: localBridgeConnection(this.#options), }); await ready; } #isCurrent(worker: WorkerLike, generation: number): boolean { return ( this.#lifecycle === "open" && this.#worker === worker && this.#generation === generation ); } #startNext(): void { if (this.#lifecycle !== "open" || this.#runs.active || !this.#worker) { return; } const next = this.#runs.startNext(performance.now()); if (!next) { return; } if (next.input.timeoutMs) { this.#timeout = setTimeout( () => void this.#timeoutActive(next), next.input.timeoutMs ); } this.#worker.postMessage({ type: "run", cellId: next.input.cellId, code: this.#moduleLoader.prepareCell(next.input.code), timeoutMs: next.input.timeoutMs, ...(next.input.strings === undefined ? {} : { strings: next.input.strings }), }); } async #timeoutActive(run: PendingJavaScriptRun): Promise { if (!this.#runs.releaseActive(run)) { return; } const durationMs = run.input.timeoutMs ?? 0; this.#runs.settle(run, { type: "result", cellId: run.input.cellId, ok: false, error: { message: kernelTimeoutMessage("JS cell", durationMs) }, durationMs, }); await this.#restartAfterStop(); } async #restartAfterStop(): Promise { if (this.#recovery) { return await this.#recovery; } const recovery = this.#performRestartAfterStop(); this.#recovery = recovery; try { await recovery; } finally { if (this.#recovery === recovery) { this.#recovery = null; } } } async #performRestartAfterStop(): Promise { try { await this.#terminate(); if (this.#lifecycle !== "open") { return; } await this.#ensureReady(); if (this.#lifecycle === "open") { this.#startNext(); } } catch (error) { if (error instanceof WorkerStartupCancelledError) { return; } this.#runs.rejectWaiting( error instanceof Error ? error : new Error(String(error)) ); } } #handleMessage(message: KernelToHostMessage): void { this.#options.onMessage?.(message); this.#runs.active?.input.onMessage?.(message); if (message.type !== "result") { return; } const active = this.#runs.active; if (!active || active.input.cellId !== message.cellId) { return; } this.#clearTimeout(); this.#runs.releaseActive(active); this.#runs.settle(active, message); this.#startNext(); } #handleCrash(error: Error): void { const active = this.#runs.active; if (!active && this.#startupAbort) { return; } this.#clearTimeout(); if (active) { this.#runs.releaseActive(active); this.#runs.settle(active, { type: "result", cellId: active.input.cellId, ok: false, error: bridgeError(error), durationMs: this.#runs.durationMs(active, performance.now()), }); } void this.#restartAfterStop(); } #clearTimeout(): void { if (this.#timeout) { clearTimeout(this.#timeout); } this.#timeout = null; } async #terminate(): Promise { this.#clearTimeout(); this.#generation += 1; this.#startupAbort?.abort(); this.#startupAbort = null; this.#ready = null; const worker = this.#worker; this.#worker = null; if (worker) { await worker.terminate(); } } }