import { WORKER_PROTOCOL_VERSION, type WorkerRequest, type WorkerResponse, } from "./worker-protocol"; export interface WorkerLike { onmessage: ((event: MessageEvent) => void) | null; onerror: ((event: ErrorEvent) => void) | null; onmessageerror: ((event: MessageEvent) => void) | null; postMessage(message: unknown): void; terminate(): void; } export type WorkerFactory = () => WorkerLike; export class WorkerRequestError extends Error { readonly kind: "timeout" | "crash" | "cancelled" | "worker-error"; constructor( kind: WorkerRequestError["kind"], message: string, options?: ErrorOptions, ) { super(message, options); this.name = "WorkerRequestError"; this.kind = kind; } } interface ActiveRequest { readonly requestId: number; readonly generation: number; readonly resolve: (result: TResult) => void; readonly reject: (error: Error) => void; readonly timeout: ReturnType; } export class WorkerSupervisor { private readonly label: string; private readonly workerFactory: WorkerFactory; private worker: WorkerLike | null = null; private generation = 0; private nextRequestId = 1; private active: ActiveRequest | null = null; private disposed = false; constructor(label: string, workerFactory: WorkerFactory) { this.label = label; this.workerFactory = workerFactory; } get currentGeneration(): number { return this.generation; } get isRunning(): boolean { return this.active !== null; } get hasWorker(): boolean { return this.worker !== null; } run( operation: TOperation, timeoutMs: number, options: { readonly supersede?: boolean } = {}, ): Promise { if (this.disposed) { return Promise.reject( new WorkerRequestError( "worker-error", `${this.label} supervisor has been disposed`, ), ); } if (this.active) { if (!options.supersede) { return Promise.reject( new WorkerRequestError( "worker-error", `${this.label} already has an active request`, ), ); } this.restart( new WorkerRequestError( "cancelled", `${this.label} request was superseded`, ), ); } const worker = this.ensureWorker(); const requestId = this.nextRequestId++; const generation = this.generation; return new Promise((resolve, reject) => { const timeout = globalThis.setTimeout(() => { if ( this.active?.requestId !== requestId || this.active.generation !== generation ) { return; } this.restart( new WorkerRequestError( "timeout", `${this.label} exceeded its ${timeoutMs} ms time limit`, ), ); }, timeoutMs); this.active = { requestId, generation, resolve, reject, timeout, }; const message: WorkerRequest = { protocolVersion: WORKER_PROTOCOL_VERSION, requestId, generation, payload: operation, }; worker.postMessage(message); }); } cancel(): void { if (!this.active) return; this.restart( new WorkerRequestError( "cancelled", `${this.label} request was cancelled`, ), ); } dispose(): void { if (this.disposed) return; this.disposed = true; this.restart( new WorkerRequestError("cancelled", `${this.label} was disposed`), ); } private ensureWorker(): WorkerLike { if (this.worker) return this.worker; const worker = this.workerFactory(); this.generation += 1; const generation = this.generation; worker.onmessage = (event) => { this.handleResponse(event.data, generation); }; worker.onerror = (event) => { this.handleCrash( new WorkerRequestError( "crash", `${this.label} crashed: ${event.message || "unknown worker error"}`, ), generation, ); }; worker.onmessageerror = () => { this.handleCrash( new WorkerRequestError( "crash", `${this.label} returned an unreadable message`, ), generation, ); }; this.worker = worker; return worker; } private handleResponse(value: unknown, generation: number): void { const response = value as Partial>; if ( response.protocolVersion !== WORKER_PROTOCOL_VERSION || typeof response.requestId !== "number" || response.generation !== generation || !this.active || this.active.requestId !== response.requestId || this.active.generation !== generation ) { return; } const active = this.active; this.active = null; globalThis.clearTimeout(active.timeout); if (response.ok === true && "payload" in response) { active.resolve(response.payload as TResult); return; } const details = "error" in response && response.error ? response.error : { name: "Error", message: "Unknown worker failure" }; active.reject( new WorkerRequestError( "worker-error", `${details.name}: ${details.message}`, ), ); } private handleCrash(error: WorkerRequestError, generation: number): void { if (generation !== this.generation) return; this.restart(error); } private restart(error: WorkerRequestError): void { if (this.active) { globalThis.clearTimeout(this.active.timeout); this.active.reject(error); this.active = null; } if (this.worker) { this.worker.onmessage = null; this.worker.onerror = null; this.worker.onmessageerror = null; this.worker.terminate(); this.worker = null; } } }