first commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# `pool/extended-adapter`
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines `PooledSurrealAdapter`, a thin subclass of `@suaveplan/surrealdb`'s `SurrealAdapter` that exposes the underlying `SurrealClientLike` via `getRawClient()`. The canonical `DatabaseAdapter.query(sql, params: unknown[])` only carries positional parameters (translated to `{ p0, p1, ... }` records inside the upstream adapter), which means the named `$name` / `$hash` / `$appliedAt` placeholders used by `@suaveplan/api-migrations`' SurrealQL adapter cannot ride over the canonical surface. The migration runner therefore needs access to the raw client.
|
||||
|
||||
Subclassing is the safest way to surface the otherwise-`protected` `_client` field without duplicating the parent's connection state machine. The class is otherwise transparent — every method inherited from `SurrealAdapter` works exactly the same, so pooled adapters continue to implement `DatabaseAdapter` and slot into `ServiceRegistry["db"]` unchanged.
|
||||
|
||||
## Features
|
||||
|
||||
- Extends `SurrealAdapter` 1:1 — no behaviour difference for `connect` / `disconnect` / `query` / `execute` / `transaction` / `create` / `select` / `update` / `delete` / `selectAll`.
|
||||
- Adds `getRawClient(): SurrealClientLike` for the migration runner.
|
||||
- Throws `SurrealError(NOT_CONNECTED)` when called before `connect()`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { PooledSurrealAdapter } from "@suaveplan/api-plugin-database/pool/extended-adapter";
|
||||
|
||||
const adapter = new PooledSurrealAdapter({
|
||||
url: "ws://localhost:8000/rpc",
|
||||
namespace: "app",
|
||||
database: "main",
|
||||
username: "root",
|
||||
password: "root",
|
||||
});
|
||||
await adapter.connect();
|
||||
const client = adapter.getRawClient();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ConnectionPool, PooledSurrealAdapter } from "@suaveplan/api-plugin-database";
|
||||
|
||||
const pool = new ConnectionPool({
|
||||
connection,
|
||||
pool,
|
||||
adapterFactory: (cfg) =>
|
||||
new PooledSurrealAdapter({
|
||||
url: cfg.url,
|
||||
namespace: cfg.namespace,
|
||||
database: cfg.database,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class PooledSurrealAdapter extends SurrealAdapter`
|
||||
|
||||
- `new PooledSurrealAdapter(options?, logger?, clientFactory?)` — pass-through constructor.
|
||||
- `getRawClient(): SurrealClientLike` — returns the live underlying client; throws `SurrealError(NOT_CONNECTED)` when disconnected.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The subclass touches only `_client` and `_state` — both `protected` on the parent. No invariants of the parent class are altered.
|
||||
- A future refactor could promote `getRawClient` onto `SurrealAdapter` itself; until then, this subclass localises the access to the one package that needs it.
|
||||
- The class is intentionally NOT exposed through the default `db` slot — the slot type stays `DatabaseAdapter`. Consumers that need the raw client receive a `PooledSurrealAdapter` instance and call `getRawClient()` explicitly.
|
||||
95
docs/api-stack/api/api-plugin-database/src/pool/pool.md
Normal file
95
docs/api-stack/api/api-plugin-database/src/pool/pool.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# `pool`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `ConnectionPool`, a bounded pool of `SurrealAdapter` instances that backs the `db` service registered by `createDatabasePlugin`. The pool owns lifecycle (create, idle reap, drain) and concurrency control (waiter queue, acquire timeout); every adapter member implements the canonical `@suaveplan/db` `DatabaseAdapter` contract, so consumers receive a fully featured handle without any pool-specific wrapper type. This is the heart of the Path-A wrapper: instead of pooling raw `Surreal` clients (as the upstream package did), suaveplan pools whole adapters so each acquire yields a working, typed surface.
|
||||
|
||||
The pool emits OpenTelemetry instruments for every checkout, checkin, and saturation transition. Pool members are constructed through an injectable factory so tests can wire deterministic doubles without touching the surreal driver, and timers are injectable so the idle reaper and acquire-timeout paths can be exercised without `vi.useFakeTimers`.
|
||||
|
||||
## Features
|
||||
|
||||
- Bounded between `pool.min` and `pool.max`; lazy growth above `min` on demand.
|
||||
- Waiter queue with `acquireTimeoutMs` reject; surfaces `DATABASE.POOL_EXHAUSTED`.
|
||||
- Idle reaper drains members above `min` past `pool.idleMs` of inactivity.
|
||||
- `close()` drains every member and rejects every waiter with `DATABASE.POOL_CLOSED`.
|
||||
- OTel emission via `getDatabaseInstruments()` on every checkout / checkin / saturation event.
|
||||
- Injectable `adapterFactory`, `clock`, `setTimer`/`clearTimer`, `setIdleInterval`/`clearIdleInterval`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ConnectionPool } from "@suaveplan/api-plugin-database/pool/pool";
|
||||
import { SurrealAdapter } from "@suaveplan/surrealdb";
|
||||
|
||||
const pool = new ConnectionPool({
|
||||
connection: {
|
||||
url: "ws://localhost:8000/rpc",
|
||||
namespace: "app",
|
||||
database: "main",
|
||||
username: "root",
|
||||
password: "root",
|
||||
},
|
||||
pool: { min: 1, max: 10, idleMs: 30_000, acquireTimeoutMs: 5_000 },
|
||||
adapterFactory: (cfg) =>
|
||||
new SurrealAdapter({
|
||||
url: cfg.url,
|
||||
namespace: cfg.namespace,
|
||||
database: cfg.database,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
}),
|
||||
});
|
||||
await pool.init();
|
||||
|
||||
const adapter = await pool.acquire();
|
||||
try {
|
||||
await adapter.query("SELECT 1");
|
||||
} finally {
|
||||
pool.release(adapter);
|
||||
}
|
||||
await pool.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ConnectionPool } from "@suaveplan/api-plugin-database";
|
||||
|
||||
let elapsed = 0;
|
||||
const pool = new ConnectionPool({
|
||||
connection: { url, namespace, database, username, password },
|
||||
pool: { min: 2, max: 4, idleMs: 100, acquireTimeoutMs: 50 },
|
||||
adapterFactory: () => new FakeAdapter(),
|
||||
clock: () => elapsed,
|
||||
setTimer: (cb, ms) => setTimeout(cb, ms),
|
||||
setIdleInterval: (cb, ms) => setInterval(cb, ms),
|
||||
});
|
||||
await pool.init();
|
||||
expect(pool.size()).toEqual({ total: 2, idle: 2, inUse: 0, waiting: 0 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class ConnectionPool`
|
||||
|
||||
- `new ConnectionPool(options: ConnectionPoolOptions)` — builds an unstarted pool.
|
||||
- `init(): Promise<void>` — eagerly creates `pool.min` members and starts the idle reaper.
|
||||
- `acquire(): Promise<SurrealAdapter>` — returns an idle adapter, lazily grows up to `pool.max`, or queues a waiter (rejecting after `acquireTimeoutMs`).
|
||||
- `release(adapter: SurrealAdapter): void` — returns the adapter to the pool; hands it to the next waiter if any.
|
||||
- `close(): Promise<void>` — drains adapters and rejects every waiter with `DATABASE.POOL_CLOSED`.
|
||||
- `size(): PoolSize` — snapshot `{ total, idle, inUse, waiting }`.
|
||||
|
||||
### `interface ConnectionPoolOptions`
|
||||
|
||||
`connection: AdapterFactoryInput`, `pool: PoolConfig`, `adapterFactory: AdapterFactory`, optional `clock`, `setTimer`, `clearTimer`, `setIdleInterval`, `clearIdleInterval`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `release(adapter)` silently ignores unknown adapters so caller bugs (double-release, foreign adapter) don't poison the pool.
|
||||
- The waiter queue is FIFO; the next `release` always wakes the oldest waiter.
|
||||
- `close()` swallows adapter-disconnect errors — they are non-fatal at teardown and would only obscure the root cause of whatever triggered shutdown.
|
||||
- The pool does NOT enforce a `signal` parameter on `acquire()`; callers wrap the call in `withRetry` (which honours `AbortSignal`) when they need cancellation.
|
||||
Reference in New Issue
Block a user