first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# Connection Singletons
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<string, DatabaseAdapter>`), 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<string, DatabaseAdapter>();
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<DatabaseConfig>,
opts?: DbOperationOptions,
registry?: Map<string, DatabaseAdapter>
): Promise<DatabaseAdapter>
```
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<string, DatabaseAdapter>
): Promise<void>
```
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<string, DatabaseAdapter>): boolean
```
Returns `true` if an adapter is registered under `name`.
### `closeAllSingletons`
```ts
closeAllSingletons(
opts?: DbOperationOptions,
registry?: Map<string, DatabaseAdapter>
): Promise<void>
```
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.