fix(db): share registry across bundled copies
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-13 19:10:51 +05:30
parent 5106256401
commit f0447fddb0
3 changed files with 32 additions and 7 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.8.12", "version": "0.8.13",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
+18 -6
View File
@@ -14,7 +14,18 @@ import type { Db } from "./driver.ts";
const DEFAULT = "default"; const DEFAULT = "default";
type DbFactory = () => Db; type DbFactory = () => Db;
type RegistryEntry = { db?: Db; factory?: DbFactory }; type RegistryEntry = { db?: Db; factory?: DbFactory };
const registry = new Map<string, RegistryEntry>(); const REGISTRY_KEY = Symbol.for("@wrnexus/db:registry:v1");
type RegistryGlobal = typeof globalThis & { [REGISTRY_KEY]?: Map<string, RegistryEntry> };
// Production bundlers can include @wrnexus/db more than once when an app and
// the server runtime resolve compatible but distinct package installations.
// A module-local Map splits configuration from consumers in that case. Store
// the registry on globalThis under a stable symbol so every bundled copy in
// the process observes the same default and named connections.
function databaseRegistry(): Map<string, RegistryEntry> {
const scope = globalThis as RegistryGlobal;
return (scope[REGISTRY_KEY] ??= new Map<string, RegistryEntry>());
}
/** Set the default database (called by the runtime at startup). */ /** Set the default database (called by the runtime at startup). */
export function setDb(db: Db): Db; export function setDb(db: Db): Db;
@@ -23,7 +34,7 @@ export function setDb(name: string, db: Db): Db;
export function setDb(a: string | Db, b?: Db): Db { export function setDb(a: string | Db, b?: Db): Db {
const name = typeof a === "string" ? a : DEFAULT; const name = typeof a === "string" ? a : DEFAULT;
const db = typeof a === "string" ? b! : a; const db = typeof a === "string" ? b! : a;
registry.set(name, { db }); databaseRegistry().set(name, { db });
return db; return db;
} }
@@ -37,12 +48,12 @@ export function registerDb(name: string, db: Db): Db {
* `getDb(name)` call creates and caches the connection. * `getDb(name)` call creates and caches the connection.
*/ */
export function registerLazyDb(name: string, factory: DbFactory): void { export function registerLazyDb(name: string, factory: DbFactory): void {
registry.set(name, { factory }); databaseRegistry().set(name, { factory });
} }
/** The default database, or a named one. Throws if it isn't configured. */ /** The default database, or a named one. Throws if it isn't configured. */
export function getDb(name = DEFAULT): Db { export function getDb(name = DEFAULT): Db {
const entry = registry.get(name); const entry = databaseRegistry().get(name);
if (!entry) { if (!entry) {
throw new Error( throw new Error(
name === DEFAULT name === DEFAULT
@@ -60,16 +71,17 @@ export function getDb(name = DEFAULT): Db {
/** Whether the default (or a named) database has been configured. */ /** Whether the default (or a named) database has been configured. */
export function hasDb(name = DEFAULT): boolean { export function hasDb(name = DEFAULT): boolean {
return registry.has(name); return databaseRegistry().has(name);
} }
/** Names of all configured databases (the default appears as "default"). */ /** Names of all configured databases (the default appears as "default"). */
export function databaseNames(): string[] { export function databaseNames(): string[] {
return [...registry.keys()]; return [...databaseRegistry().keys()];
} }
/** Close every configured database and clear the registry. */ /** Close every configured database and clear the registry. */
export async function closeDatabases(): Promise<void> { export async function closeDatabases(): Promise<void> {
const registry = databaseRegistry();
const databases = [...registry.values()].flatMap((entry) => (entry.db ? [entry.db] : [])); const databases = [...registry.values()].flatMap((entry) => (entry.db ? [entry.db] : []));
registry.clear(); registry.clear();
const results = await Promise.allSettled(databases.map((db) => db.close())); const results = await Promise.allSettled(databases.map((db) => db.close()));
+13
View File
@@ -92,3 +92,16 @@ test("registry closes every database and clears itself when one close fails", as
expect(secondClosed).toBe(true); expect(secondClosed).toBe(true);
expect(databaseNames()).toEqual([]); expect(databaseNames()).toEqual([]);
}); });
test("separately evaluated package copies share the process-wide registry", async () => {
await closeDatabases();
const secondCopy = await import(`../src/client.ts?copy=${crypto.randomUUID()}`);
const main = createDb(sqlite(":memory:"));
setDb(main);
expect(secondCopy.hasDb()).toBe(true);
expect(secondCopy.getDb()).toBe(main);
await secondCopy.closeDatabases();
expect(hasDb()).toBe(false);
});