Initial Commit
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user