fix(db): share registry across bundled copies
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user