# Connection Singletons _(no submodules)_ ## Purpose `getSingleton`, `closeSingleton`, `hasSingleton`, and `closeAllSingletons` provide a lightweight singleton registry for `DatabaseAdapter` instances. The primary purpose is to avoid opening multiple connections to the same database within a single process — `getSingleton` is idempotent: the first call creates and connects the adapter, subsequent calls with the same name return the cached reference without reconnecting. By accepting an optional `registry` parameter (a plain `Map`), the helpers are safe for test isolation: each test suite can pass its own private map and have zero cross-test bleed. In production, omitting `registry` uses a module-level default map, replicating the familiar global-singleton pattern without requiring any global state management from callers. ## Features - Idempotent `getSingleton` — second call with same name returns cached adapter - `DatabaseConfigSchema` validation before `connect` is called - Wraps `connect` failures as `DatabaseError` with `DB.CONNECTION_FAILED` - `closeSingleton` — graceful disconnect + registry removal (no-op for unknown names) - `closeAllSingletons` — drains the entire registry; collects disconnect errors into `AggregateError` - Injectable `registry` parameter — full test isolation without global state - `AbortSignal` threading via `DbOperationOptions` ## Usage: Basic Example ```ts import { getSingleton, closeSingleton } from "@suaveplan/db"; import { SqliteAdapter } from "@suaveplan/db-sqlite"; // Get or create the default "app" connection const db = await getSingleton("app", () => new SqliteAdapter({ filename: "./app.db" })); const rows = await db.query<{ id: number }>("SELECT id FROM users"); // Clean up at process exit await closeSingleton("app"); ``` ## Usage: Advanced Example ```ts import { getSingleton, closeAllSingletons, hasSingleton, } from "@suaveplan/db"; import type { DatabaseAdapter } from "@suaveplan/db"; // Test-isolated registry — no cross-test bleed const testRegistry = new Map(); beforeEach(async () => { await getSingleton( "test-db", () => new SqliteAdapter({ filename: ":memory:" }), undefined, undefined, testRegistry ); }); afterEach(async () => { await closeAllSingletons(undefined, testRegistry); }); it("has singleton after setup", () => { expect(hasSingleton("test-db", testRegistry)).toBe(true); }); ``` ## API Reference ### `getSingleton` ```ts getSingleton( name: string, factory: () => DatabaseAdapter, config?: Partial, opts?: DbOperationOptions, registry?: Map ): Promise ``` Returns the cached adapter for `name`, or creates one via `factory`, validates `config`, connects it, and caches it. Throws `DatabaseError(DB.CONNECTION_FAILED)` if `connect` fails. ### `closeSingleton` ```ts closeSingleton( name: string, opts?: DbOperationOptions, registry?: Map ): Promise ``` Calls `disconnect` on the named adapter and removes it from the registry. No-op if `name` is not registered. ### `hasSingleton` ```ts hasSingleton(name: string, registry?: Map): boolean ``` Returns `true` if an adapter is registered under `name`. ### `closeAllSingletons` ```ts closeAllSingletons( opts?: DbOperationOptions, registry?: Map ): Promise ``` Clears the registry immediately, then disconnects all adapters concurrently. If one or more disconnects fail, an `AggregateError` is thrown after all attempts complete. ## Implementation Notes The registry is cleared before `disconnect` is called in `closeAllSingletons`. This ensures that even if a `disconnect` rejects, the registry is empty and a subsequent `getSingleton` call will create a fresh adapter rather than returning a half-closed one. The AggregateError is thrown after all disconnect attempts so callers get complete failure information rather than a short-circuit on the first error. Config validation uses `DatabaseConfigSchema.parse` (not `.safeParse`) so invalid config throws a `ZodError` immediately — fail-fast at the boundary is preferable to a cryptic driver-level error.