Files
suaveplan/packages/persistence/database/surrealdb/tests/integration/singleton.integration.test.ts
2026-05-27 23:47:53 +01:00

119 lines
4.8 KiB
TypeScript

/**
* Singleton lifecycle integration tests — exercise the real surrealdb singleton
* helpers against a live SurrealDB v3.0.4 server.
*
* Spec: openspec/changes/surrealdb-integration-tests/specs/database-surrealdb-integration/spec.md
* "Singleton lifecycle verified against the real engine".
*
* The spec is written against a generic `getSingleton(name, factory, _, _, registry)`
* shape; the real `@suaveplan/database/surrealdb` API is
* getSurrealSingleton(options, name, clientFactory) — using a module-level Map.
* These tests adapt the spec scenarios to that real shape: same-instance reuse,
* close-observable-via-NOT_CONNECTED, closeAll closes every registered name,
* and named-singleton isolation (closing "a" leaves "b" connected).
*
* @module
*/
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SURREAL_CODES } from "../../src/errors/index.js";
import {
closeAllSurrealSingletons,
closeSurrealSingleton,
getSurrealSingleton,
hasSurrealSingleton,
} from "../../src/singleton/index.js";
import { inject, surrealUnavailable } from "./integration-setup.js";
function adapterOptions(): {
url: string;
namespace: string;
database: string;
username: string;
password: string;
} {
const config = inject("surrealdb");
return {
url: config.url,
namespace: config.namespace,
database: config.database,
username: config.username,
password: config.password,
};
}
describe.skipIf(surrealUnavailable)(
"SurrealDB singleton lifecycle (real SurrealDB v3.0.4)",
() => {
it("getSurrealSingleton returns the same connected instance on second call", async () => {
const opts = adapterOptions();
const name = `singleton-same-${Date.now()}`;
try {
const a = await getSurrealSingleton(opts, name);
const b = await getSurrealSingleton(opts, name);
expect(b).toBe(a);
expect(a.isConnected).toBe(true);
const rows = await a.query<{ n: number }>("RETURN { n: 1 }");
expect(rows).toEqual([{ n: 1 }]);
} finally {
await closeSurrealSingleton(name);
}
}, 30000);
it("closeSurrealSingleton closes the underlying WebSocket — query throws NOT_CONNECTED on the previously-returned reference", async () => {
const opts = adapterOptions();
const name = `singleton-close-${Date.now()}`;
const adapter = await getSurrealSingleton(opts, name);
expect(adapter.isConnected).toBe(true);
await closeSurrealSingleton(name);
expect(hasSurrealSingleton(name)).toBe(false);
expect(adapter.isConnected).toBe(false);
await expect(adapter.query("SELECT 1")).rejects.toMatchObject({
code: SURREAL_CODES.NOT_CONNECTED,
});
}, 30000);
it("closeAllSurrealSingletons closes every registered singleton", async () => {
const opts = adapterOptions();
const stamp = Date.now();
const names = [
`closeall-a-${stamp}`,
`closeall-b-${stamp}`,
`closeall-c-${stamp}`,
];
const adapters = await Promise.all(
names.map((n) => getSurrealSingleton(opts, n))
);
for (const a of adapters) expect(a.isConnected).toBe(true);
await closeAllSurrealSingletons();
for (const n of names) expect(hasSurrealSingleton(n)).toBe(false);
for (const a of adapters) expect(a.isConnected).toBe(false);
for (const a of adapters) {
await expect(a.query("SELECT 1")).rejects.toMatchObject({
code: SURREAL_CODES.NOT_CONNECTED,
});
}
}, 60000);
it("named singletons are isolated — closing one leaves others connected", async () => {
const opts = adapterOptions();
const stamp = Date.now();
const nameA = `isolated-a-${stamp}`;
const nameB = `isolated-b-${stamp}`;
const a = await getSurrealSingleton(opts, nameA);
const b = await getSurrealSingleton(opts, nameB);
expect(b).not.toBe(a);
try {
await closeSurrealSingleton(nameA);
expect(hasSurrealSingleton(nameA)).toBe(false);
expect(hasSurrealSingleton(nameB)).toBe(true);
expect(a.isConnected).toBe(false);
expect(b.isConnected).toBe(true);
const rows = await b.query<{ n: number }>("RETURN { n: 1 }");
expect(rows).toEqual([{ n: 1 }]);
} finally {
await closeSurrealSingleton(nameB);
}
}, 30000);
}
);