207 lines
6.2 KiB
TypeScript
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" });
|
|
});
|
|
});
|