203 lines
6.5 KiB
TypeScript
203 lines
6.5 KiB
TypeScript
import { describe, expect, it, vi } from "@suaveplan/testing/runner";
|
|
import { SqliteError } from "../errors/index.js";
|
|
import { createWasmDriver } from "./wasm-driver.js";
|
|
|
|
// Injectable worker factory avoids spawning real Workers in Node/Vitest environment
|
|
function makeWorkerFactory(
|
|
handler: (
|
|
msg: unknown,
|
|
post: (data: unknown) => void
|
|
) => void | Promise<void>
|
|
): () => Worker {
|
|
return () => {
|
|
let onmessage: ((ev: MessageEvent) => void) | null = null;
|
|
let onerror: ((ev: ErrorEvent) => void) | null = null;
|
|
|
|
const post = (data: unknown) => {
|
|
if (onmessage) {
|
|
onmessage({ data } as MessageEvent);
|
|
}
|
|
};
|
|
|
|
const worker = {
|
|
get onmessage() {
|
|
return onmessage;
|
|
},
|
|
set onmessage(fn: ((ev: MessageEvent) => void) | null) {
|
|
onmessage = fn;
|
|
},
|
|
get onerror() {
|
|
return onerror;
|
|
},
|
|
set onerror(fn: ((ev: ErrorEvent) => void) | null) {
|
|
onerror = fn;
|
|
},
|
|
postMessage: (msg: unknown) => {
|
|
void Promise.resolve().then(() => handler(msg, post));
|
|
},
|
|
terminate: vi.fn(),
|
|
};
|
|
|
|
return worker as unknown as Worker;
|
|
};
|
|
}
|
|
|
|
function makeSuccessWorker(): () => Worker {
|
|
return makeWorkerFactory((msg, post) => {
|
|
const m = msg as {
|
|
type: string;
|
|
id: number;
|
|
sql?: string;
|
|
params?: unknown[];
|
|
};
|
|
if (m.type === "init") {
|
|
post({ id: m.id, ok: true, result: { type: "ready" } });
|
|
} else if (m.type === "query") {
|
|
post({ id: m.id, ok: true, result: [["value1"], ["value2"]] });
|
|
} else if (m.type === "execute") {
|
|
post({ id: m.id, ok: true, result: { rowsAffected: 1 } });
|
|
} else if (m.type === "close") {
|
|
post({ id: m.id, ok: true, result: { type: "closed" } });
|
|
}
|
|
});
|
|
}
|
|
|
|
describe("createWasmDriver", () => {
|
|
it("creates driver and resolves on successful init", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
const { db, close } = await createWasmDriver(
|
|
undefined,
|
|
makeSuccessWorker()
|
|
);
|
|
expect(db).toBeDefined();
|
|
await close();
|
|
});
|
|
|
|
it("query returns rows from worker", { timeout: 30000 }, async () => {
|
|
const { db, close } = await createWasmDriver(
|
|
undefined,
|
|
makeSuccessWorker()
|
|
);
|
|
const rows = await db.query("SELECT * FROM t");
|
|
expect(rows).toEqual([["value1"], ["value2"]]);
|
|
await close();
|
|
});
|
|
|
|
it("execute returns rowsAffected from worker", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
const { db, close } = await createWasmDriver(
|
|
undefined,
|
|
makeSuccessWorker()
|
|
);
|
|
const result = await db.execute("INSERT INTO t VALUES (?)", [1]);
|
|
expect(result.rowsAffected).toBe(1);
|
|
await close();
|
|
});
|
|
|
|
it("close() is idempotent", { timeout: 30000 }, async () => {
|
|
const { close } = await createWasmDriver(
|
|
undefined,
|
|
makeSuccessWorker()
|
|
);
|
|
await close();
|
|
await expect(close()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("rejects pending requests when close() is called", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
let postFn: ((data: unknown) => void) | null = null;
|
|
|
|
const slowWorker = makeWorkerFactory((msg, post) => {
|
|
const m = msg as { type: string; id: number };
|
|
if (m.type === "init") {
|
|
post({ id: m.id, ok: true, result: { type: "ready" } });
|
|
} else if (m.type === "close") {
|
|
post({ id: m.id, ok: true, result: { type: "closed" } });
|
|
}
|
|
// query messages hang — capture postFn for manual resolution
|
|
if (m.type === "query") {
|
|
postFn = post;
|
|
}
|
|
});
|
|
|
|
const { db, close } = await createWasmDriver(undefined, slowWorker);
|
|
|
|
// Fire a query that will hang — capture the promise
|
|
const queryPromise = db.query("SELECT 1");
|
|
// Ensure the query is actually sent before closing
|
|
await Promise.resolve();
|
|
|
|
// close() should drain pending
|
|
await close();
|
|
|
|
// The hanging query should now reject
|
|
await expect(queryPromise).rejects.toBeInstanceOf(SqliteError);
|
|
|
|
// Suppress unused variable warning
|
|
void postFn;
|
|
});
|
|
|
|
it("throws SqliteError on worker init failure", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
const failWorker = makeWorkerFactory((msg, post) => {
|
|
const m = msg as { type: string; id: number };
|
|
if (m.type === "init") {
|
|
post({
|
|
id: m.id,
|
|
ok: false,
|
|
error: "sqlite-wasm init failed: load error",
|
|
});
|
|
} else if (m.type === "close") {
|
|
post({ id: m.id, ok: true, result: { type: "closed" } });
|
|
}
|
|
});
|
|
|
|
await expect(
|
|
createWasmDriver(undefined, failWorker)
|
|
).rejects.toBeInstanceOf(SqliteError);
|
|
await expect(
|
|
createWasmDriver(undefined, failWorker)
|
|
).rejects.toMatchObject({ code: "SQLITE.DRIVER_INIT_FAILED" });
|
|
});
|
|
|
|
it("worker error event rejects pending requests", {
|
|
timeout: 30000,
|
|
}, async () => {
|
|
let fireError: ((ev: ErrorEvent) => void) | null = null;
|
|
|
|
const hangingWorker = makeWorkerFactory((msg, post) => {
|
|
const m = msg as { type: string; id: number };
|
|
if (m.type === "init") {
|
|
post({ id: m.id, ok: true, result: { type: "ready" } });
|
|
}
|
|
// query messages hang — worker "crashes" below
|
|
});
|
|
|
|
const patchedFactory = () => {
|
|
const worker = hangingWorker();
|
|
Object.defineProperty(worker, "onerror", {
|
|
set: (fn: (ev: ErrorEvent) => void) => {
|
|
fireError = fn;
|
|
},
|
|
get: () => fireError,
|
|
configurable: true,
|
|
});
|
|
return worker;
|
|
};
|
|
|
|
const { db } = await createWasmDriver(undefined, patchedFactory);
|
|
|
|
const queryPromise = db.query("SELECT 1");
|
|
await Promise.resolve();
|
|
|
|
// Simulate worker crash — onerror should reject all pending promises
|
|
fireError?.({ message: "Worker crashed" } as ErrorEvent);
|
|
|
|
await expect(queryPromise).rejects.toBeInstanceOf(SqliteError);
|
|
});
|
|
});
|