import { describe, expect, it } from "vitest"; import { KdlWorkerClient, type KdlWorkerLike } from "../../src/kdl/kdlClient.js"; import type { NativeKdlModule } from "../../src/kdl/nativeModule.js"; import { KdlStructuredError, type KdlRpcRequest, type KdlRpcResponse } from "../../src/kdl/rpc.js"; import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js"; import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js"; class FakeWorker implements KdlWorkerLike { readonly sent: Array> = []; terminated = false; private messageListeners = new Set<(event: MessageEvent) => void>(); private errorListeners = new Set<(event: ErrorEvent) => void>(); postMessage(message: KdlRpcRequest): void { this.sent.push(message); } terminate(): void { this.terminated = true; } addEventListener(type: "message", listener: (event: MessageEvent) => void): void; addEventListener(type: "error", listener: (event: ErrorEvent) => void): void; addEventListener(type: "message" | "error", listener: unknown): void { if (type === "message") { this.messageListeners.add(listener as (event: MessageEvent) => void); return; } this.errorListeners.add(listener as (event: ErrorEvent) => void); } removeEventListener(type: "message", listener: (event: MessageEvent) => void): void; removeEventListener(type: "error", listener: (event: ErrorEvent) => void): void; removeEventListener(type: "message" | "error", listener: unknown): void { if (type === "message") { this.messageListeners.delete(listener as (event: MessageEvent) => void); return; } this.errorListeners.delete(listener as (event: ErrorEvent) => void); } emitResponse(response: KdlRpcResponse): void { const event = { data: response } as MessageEvent; for (const listener of this.messageListeners) { listener(event); } } emitError(message: string): void { const event = { message, error: new Error(message) } as ErrorEvent; for (const listener of this.errorListeners) { listener(event); } } } describe("KDL Worker RPC", () => { it("loads the native WASM module during init when a loader is configured", async () => { const calls: unknown[][] = []; const native: NativeKdlModule = { ccall: (...args) => { calls.push(args); return 0; } }; const runtime = createKdlWorkerRuntime(async () => native); const response = await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{ wasmBuild: "native-test" }] }); expect(response.ok).toBe(true); expect(response.result).toMatchObject({ wasmBuild: "native-test" }); expect(calls).toEqual([["kdl_init", "number", ["string"], ['{"wasmBuild":"native-test"}']]]); }); it("normalizes native WASM initialization failures", async () => { const runtime = createKdlWorkerRuntime(async () => { throw new Error("cannot load kdl.js"); }); const response = await dispatchKdlRpcRequest(runtime, { id: 11, method: "init", payload: [{}] }); expect(response.ok).toBe(false); expect(response.error).toMatchObject({ code: "KDL_WASM_INIT_FAILED", diagnostics: [ { severity: "error", code: "KDL_WASM_INIT_FAILED" } ] }); }); it("dispatches init and dispose through structured responses", async () => { const runtime = createKdlWorkerRuntime(); const initResponse = await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{ wasmBuild: "test", useThreads: true }] }); expect(initResponse.ok).toBe(true); expect(initResponse.result).toMatchObject({ version: "0.1.0", wasmBuild: "test", supportsThreads: true }); const disposeResponse = await dispatchKdlRpcRequest(runtime, { id: 2, method: "dispose", payload: [] }); expect(disposeResponse).toMatchObject({ id: 2, ok: true }); }); it("returns a structured error when an implemented method is called before init", async () => { const response = await dispatchKdlRpcRequest(createKdlWorkerRuntime(), { id: 7, method: "fk", payload: [1, new Float64Array([0])] }); expect(response.ok).toBe(false); expect(response.error).toMatchObject({ code: "KDL_NOT_INITIALIZED", diagnostics: [ { severity: "error", code: "KDL_NOT_INITIALIZED" } ] }); }); it("uses unique request ids and resolves responses by id", async () => { const workers: FakeWorker[] = []; const client = new KdlWorkerClient(() => { const worker = new FakeWorker(); workers.push(worker); return worker; }); const first = client.call("init", { wasmBuild: "a" }); const second = client.call("dispose"); expect(workers).toHaveLength(1); expect(workers[0]?.sent.map((request) => request.id)).toEqual([1, 2]); workers[0]?.emitResponse({ id: 2, ok: true }); workers[0]?.emitResponse({ id: 1, ok: true, result: { version: "0.1.0", wasmBuild: "a", supportsThreads: false, supportsWasmFs: false } }); await expect(second).resolves.toBeUndefined(); await expect(first).resolves.toMatchObject({ wasmBuild: "a" }); }); it("rejects pending requests on worker failure and can create a fresh worker", async () => { const workers: FakeWorker[] = []; const client = new KdlWorkerClient(() => { const worker = new FakeWorker(); workers.push(worker); return worker; }); const pending = client.init(); workers[0]?.emitError("boom"); await expect(pending).rejects.toMatchObject({ code: "KDL_WORKER_CRASHED" }); expect(workers[0]?.terminated).toBe(true); const restarted = client.init({ wasmBuild: "restart" }); expect(workers).toHaveLength(2); workers[1]?.emitResponse({ id: 2, ok: true, result: { version: "0.1.0", wasmBuild: "restart", supportsThreads: false, supportsWasmFs: false } }); await expect(restarted).resolves.toMatchObject({ wasmBuild: "restart" }); }); });