From f0447fddb0c516aeacb1825c935755dc1add7892 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 13 Aug 2026 19:10:51 +0530 Subject: [PATCH] fix(db): share registry across bundled copies --- packages/db/package.json | 2 +- packages/db/src/client.ts | 24 ++++++++++++++++++------ packages/db/test/registry.test.ts | 13 +++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/db/package.json b/packages/db/package.json index 93933978..18b8a0fa 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.8.12", + "version": "0.8.13", "private": true, "type": "module", "main": "./src/index.ts", diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index d52018c1..d04cc354 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -14,7 +14,18 @@ import type { Db } from "./driver.ts"; const DEFAULT = "default"; type DbFactory = () => Db; type RegistryEntry = { db?: Db; factory?: DbFactory }; -const registry = new Map(); +const REGISTRY_KEY = Symbol.for("@wrnexus/db:registry:v1"); +type RegistryGlobal = typeof globalThis & { [REGISTRY_KEY]?: Map }; + +// 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 { + const scope = globalThis as RegistryGlobal; + return (scope[REGISTRY_KEY] ??= new Map()); +} /** Set the default database (called by the runtime at startup). */ 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 { const name = typeof a === "string" ? a : DEFAULT; const db = typeof a === "string" ? b! : a; - registry.set(name, { db }); + databaseRegistry().set(name, { db }); return db; } @@ -37,12 +48,12 @@ export function registerDb(name: string, db: Db): Db { * `getDb(name)` call creates and caches the connection. */ 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. */ export function getDb(name = DEFAULT): Db { - const entry = registry.get(name); + const entry = databaseRegistry().get(name); if (!entry) { throw new Error( name === DEFAULT @@ -60,16 +71,17 @@ export function getDb(name = DEFAULT): Db { /** Whether the default (or a named) database has been configured. */ export function hasDb(name = DEFAULT): boolean { - return registry.has(name); + return databaseRegistry().has(name); } /** Names of all configured databases (the default appears as "default"). */ export function databaseNames(): string[] { - return [...registry.keys()]; + return [...databaseRegistry().keys()]; } /** Close every configured database and clear the registry. */ export async function closeDatabases(): Promise { + const registry = databaseRegistry(); const databases = [...registry.values()].flatMap((entry) => (entry.db ? [entry.db] : [])); registry.clear(); const results = await Promise.allSettled(databases.map((db) => db.close())); diff --git a/packages/db/test/registry.test.ts b/packages/db/test/registry.test.ts index 52f0facf..97e82c6e 100644 --- a/packages/db/test/registry.test.ts +++ b/packages/db/test/registry.test.ts @@ -92,3 +92,16 @@ test("registry closes every database and clears itself when one close fails", as expect(secondClosed).toBe(true); 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); +});