diff --git a/src/functions/grab-mariadb-client.test.ts b/src/functions/grab-mariadb-client.test.ts new file mode 100644 index 0000000..2bff426 --- /dev/null +++ b/src/functions/grab-mariadb-client.test.ts @@ -0,0 +1,38 @@ +import { afterEach, expect, test } from "bun:test"; +import grabMariaDBClient from "./grab-mariadb-client"; +import setMariaDBClient from "./set-mariadb-client"; + +// Clients connect lazily, so none of these open a connection +const config = { db_name: "bmt_client_cache", ssl_reject_unauthorized: false }; + +const originalConfig = global.CONFIG; + +afterEach(async () => { + await global.MARIADB_CLIENT?.close(); + global.CONFIG = originalConfig; +}); + +test("reuses the client for the same config", () => { + expect(grabMariaDBClient({ config })).toBe(grabMariaDBClient({ config: { ...config } })); +}); + +test("a closed client is dropped from the cache and the global slot", async () => { + setMariaDBClient({ config }); + const first = global.MARIADB_CLIENT!; + + await first.close(); + expect(global.MARIADB_CLIENT).toBeUndefined(); + + const second = grabMariaDBClient({ config }); + expect(second).toBeDefined(); + expect(second).not.toBe(first); +}); + +test("the global client is not reused for a different config", () => { + setMariaDBClient({ config }); + global.CONFIG = { db_name: "bmt_other" }; + + const other = grabMariaDBClient({ config: global.CONFIG }); + expect(other).not.toBe(global.MARIADB_CLIENT); + other?.close(); +}); diff --git a/src/functions/grab-mariadb-client.ts b/src/functions/grab-mariadb-client.ts index 762d6cd..fc3743e 100644 --- a/src/functions/grab-mariadb-client.ts +++ b/src/functions/grab-mariadb-client.ts @@ -10,6 +10,9 @@ type Params = { /** Reuse one client per distinct connection config to avoid exhausting MariaDB connections. */ const clientCache = new Map(); +/** Cache key each client was created with */ +const clientKeys = new WeakMap(); + /** * Cache key covering everything that affects the connection, not just the * database name. @@ -36,8 +39,7 @@ export default function grabMariaDBClient({ // Prefer the process-wide client when it was created from the same config if ( global.MARIADB_CLIENT && - global.CONFIG && - mariadbClientCacheKey(global.CONFIG) === cacheKey + clientKeys.get(global.MARIADB_CLIENT) === cacheKey ) { clientCache.set(cacheKey, global.MARIADB_CLIENT); return global.MARIADB_CLIENT; @@ -71,6 +73,22 @@ export default function grabMariaDBClient({ adapter: "mariadb", } as Bun.SQL.Options); + /** + * A closed client can't run queries, so forget it on close: the next + * grab (or `ensureInit`) creates a fresh one. + */ + const close = MariaDBClient.close.bind(MariaDBClient); + MariaDBClient.close = (...args: Parameters) => { + if (clientCache.get(cacheKey) === MariaDBClient) { + clientCache.delete(cacheKey); + } + if (global.MARIADB_CLIENT === MariaDBClient) { + global.MARIADB_CLIENT = undefined; + } + return close(...args); + }; + + clientKeys.set(MariaDBClient, cacheKey); clientCache.set(cacheKey, MariaDBClient); return MariaDBClient; } catch (error: any) { diff --git a/src/functions/init.ts b/src/functions/init.ts index 1a5ecf6..e0d2ac2 100644 --- a/src/functions/init.ts +++ b/src/functions/init.ts @@ -19,7 +19,8 @@ import { declare global { var CONFIG: BunMariaDBConfig; var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType; - var MARIADB_CLIENT: Bun.SQL; + /** Unset after the client is closed */ + var MARIADB_CLIENT: Bun.SQL | undefined; var CONFIG_FILE_PATH: string; var SCHEMA_FILE_PATH: string; } diff --git a/src/lib/db-handler.ts b/src/lib/db-handler.ts index 721bef4..0de5fcf 100644 --- a/src/lib/db-handler.ts +++ b/src/lib/db-handler.ts @@ -1,6 +1,4 @@ -import grabMariaDBClient, { - mariadbClientCacheKey, -} from "../functions/grab-mariadb-client"; +import grabMariaDBClient from "../functions/grab-mariadb-client"; import type { BunMariaDBConfig, DBInsertReturn, @@ -29,16 +27,7 @@ export function resolveClient(config?: BunMariaDBConfig): Bun.SQL | undefined { return global.MARIADB_CLIENT; } - // Same connection config as the global client → reuse it - if ( - global.MARIADB_CLIENT && - (!global.CONFIG || - mariadbClientCacheKey(config) === - mariadbClientCacheKey(global.CONFIG)) - ) { - return global.MARIADB_CLIENT; - } - + // Returns the global client when it was created from the same config return grabMariaDBClient({ config }); } diff --git a/tests/queue.test.ts b/tests/queue.test.ts index 44eb7bc..b6ef671 100644 --- a/tests/queue.test.ts +++ b/tests/queue.test.ts @@ -52,20 +52,10 @@ describe.skipIf(!TEST_DB)("Queue tables", () => { const createDBSchema = (await import("../src/lib/schema/create-db-schema")).default; const appendDefaults = (await import("../src/utils/append-default-fields-to-db-schema")).default; - /** - * The test container uses a self-signed certificate. `db_config` gives - * this file its own client, since other test files close theirs. The - * client is created before `global.CONFIG` changes, so the previous - * file's closed global client isn't reused. - */ - const config = { - db_name: TEST_DB!, - ssl_reject_unauthorized: false, - db_config: { max: 10 }, - }; - setMariaDBClient({ config }); - global.CONFIG = config; + // The test container uses a self-signed certificate + global.CONFIG = { db_name: TEST_DB!, ssl_reject_unauthorized: false }; global.DB_SCHEMA = schema; + setMariaDBClient({ config: global.CONFIG }); DB = (await import("../src/index")).default;