first commit
This commit is contained in:
120
docs/persistence/database/db/src/connection/singleton.md
Normal file
120
docs/persistence/database/db/src/connection/singleton.md
Normal 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.
|
||||
Reference in New Issue
Block a user