Files
KDL_WORK/kdl-wasm/web/tests/kdl/rpc.test.ts
2026-06-27 08:45:38 -04:00

207 lines
6.2 KiB
TypeScript

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<KdlRpcRequest<unknown[]>> = [];
terminated = false;
private messageListeners = new Set<(event: MessageEvent<KdlRpcResponse>) => void>();
private errorListeners = new Set<(event: ErrorEvent) => void>();
postMessage(message: KdlRpcRequest<unknown[]>): void {
this.sent.push(message);
}
terminate(): void {
this.terminated = true;
}
addEventListener(type: "message", listener: (event: MessageEvent<KdlRpcResponse>) => 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<KdlRpcResponse>) => void);
return;
}
this.errorListeners.add(listener as (event: ErrorEvent) => void);
}
removeEventListener(type: "message", listener: (event: MessageEvent<KdlRpcResponse>) => 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<KdlRpcResponse>) => void);
return;
}
this.errorListeners.delete(listener as (event: ErrorEvent) => void);
}
emitResponse(response: KdlRpcResponse): void {
const event = { data: response } as MessageEvent<KdlRpcResponse>;
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" });
});
});