import { CONTEXT_ID, PROTOCOL_SCHEMA, isEnvelope, type MessageEnvelope } from "./protocol"; type Listener = (message: MessageEnvelope) => void; function commandError(message: MessageEnvelope): Error { const payload = message.payload && typeof message.payload === "object" ? message.payload as Record : {}; const error = new Error(String(payload.reason ?? payload.gate_reason ?? payload.message_key ?? payload.code ?? "worker_error")); Object.assign(error, { code: String(payload.code ?? "worker_error"), reason: payload.reason ?? payload.gate_reason ?? null, message_key: payload.message_key ?? null, response: message, }); return error; } export class ReactWorkerClient { private readonly worker: Worker; private readonly contextId: string; private readonly sessionId: string; private sequence = 0; private uiSequence = 2000000000; private listeners = new Set(); private pending = new Map void; reject: (error: Error) => void }>(); private readyPromise: Promise | null = null; constructor({ worker, contextId = CONTEXT_ID, sessionId = `react-${Date.now()}` }: { worker?: Worker; contextId?: string; sessionId?: string } = {}) { // The entry imports the source-compatible SimSession; keep it an ES module // so Vite does not wrap it as a classic worker and drop its imports. this.worker = worker ?? new Worker(new URL("./sim-worker-entry.ts", import.meta.url), { type: "module" }); this.contextId = contextId; this.sessionId = sessionId; this.worker.onmessage = (event: MessageEvent) => { if (!isEnvelope(event.data)) return; const uiStateResponse = event.data.kind === "UI_STATE_RESULT"; if (!uiStateResponse) for (const listener of this.listeners) listener(event.data); const pending = this.pending.get(event.data.seq); if (pending) { this.pending.delete(event.data.seq); if (event.data.kind === "ERROR" || event.data.payload && typeof event.data.payload === "object" && (event.data.payload as { status?: string }).status === "blocked") pending.reject(commandError(event.data)); else pending.resolve(event.data); } }; this.worker.onerror = (event) => { const message: MessageEnvelope = { schema_version: PROTOCOL_SCHEMA, kind: "ERROR", context_id: this.contextId, session_id: this.sessionId, seq: ++this.sequence, payload: { code: "worker_transport_error", message_key: event.message || "worker_failed_to_load" }, }; for (const listener of this.listeners) listener(message); for (const pending of this.pending.values()) pending.reject(new Error("worker_transport_error")); this.pending.clear(); }; } subscribe(listener: Listener): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); } send(kind: string, payload: unknown = {}, operationId?: string): Promise { const message: MessageEnvelope = { schema_version: PROTOCOL_SCHEMA, kind, context_id: this.contextId, session_id: this.sessionId, seq: ++this.sequence, ...(operationId ? { operation_id: operationId } : {}), payload }; return new Promise((resolve, reject) => { this.pending.set(message.seq, { resolve, reject }); this.worker.postMessage(message); }); } private sendUiState(kind: "UI_STATE_SAVE" | "UI_STATE_LOAD" | "UI_STATE_CLEAR", payload: Record): Promise { const seq = ++this.uiSequence; const message: MessageEnvelope = { schema_version: PROTOCOL_SCHEMA, kind, context_id: this.contextId, session_id: this.sessionId, seq, operation_id: `ui-cache-${seq}`, payload }; return new Promise((resolve, reject) => { this.pending.set(seq, { resolve, reject }); this.worker.postMessage(message); }); } saveUiState(state: Record, key = "xyzac-trt"): Promise { return this.sendUiState("UI_STATE_SAVE", { key, state }); } loadUiState(key = "xyzac-trt"): Promise { return this.sendUiState("UI_STATE_LOAD", { key }); } clearUiState(key = "xyzac-trt"): Promise { return this.sendUiState("UI_STATE_CLEAR", { key }); } hello(): Promise { return this.send("HELLO", { supported_schema_versions: [PROTOCOL_SCHEMA], requested_features: ["snapshot", "trajectory"] }); } attach(): Promise { return this.send("SESSION_ATTACH", {}, "react-session-attach"); } ensureSession(): Promise { if (!this.readyPromise) this.readyPromise = this.hello().then(() => this.attach()).then(() => undefined); return this.readyPromise; } close(): void { this.worker.terminate(); } }