-
Notifications
You must be signed in to change notification settings - Fork 2
Drivers BaseEngine
Abstract base class for every driver engine. Owns connection lifecycle, inline pool, SSL loading, and lifecycle events.
- Overview
- Quick Start (subclassing)
- Configuration
- Lifecycle
- Pool semantics
- Hooks for subclasses
- Events
- Errors
BaseEngine is abstract — concrete drivers (PostgresEngine,
RedisEngine, …) extend it. The base supplies:
- A connection state machine (
CLOSED → CONNECTING → READY → CLOSED) - An inline connection pool with min/max, idle eviction, acquire timeout
- SSL/TLS option processing (PEM strings or file paths)
- Lifecycle events (
connect,disconnect,connectionFailed,error,warn,notice) - Statistics (
poolStats) - An
instanceIdof the form"<Engine>::<Name>"
The engine composes an internal ConnectionPool<T> — created and owned as
this._pool, not part of the public API, so you never construct or pass a
Pool object yourself. With no pool option configured, the engine runs in
single-connection mode (one warm connection, no idle eviction, queueing is
still in place). Configure pool: { min, max, ... } for multi-connection.
The abstract bases ship on their own sub-path, @tundralibs/drivers/base,
alongside the types you need to declare an engine. Prefer it when all you
are doing is subclassing: it is the narrowest surface that gets you there,
and it reaches no concrete engine at all.
The package root, @tundralibs/drivers, re-exports the same four bases (plus
the errors and the shared types) and is equally safe to bundle — it carries no
engine either. The barrel used to re-export all nine, including the native
SQLiteEngine whose adapter loads a per-runtime binding (bun:sqlite,
jsr:@db/sqlite, better-sqlite3); those specifiers broke any bundle aimed at
an edge or browser runtime, which is why the engines now live one sub-path
down. Concrete engines come from @tundralibs/drivers/<engine> — or, on a
server where you want all of them at once, @tundralibs/drivers/engines.
import {
BaseEngine, // = PooledConnectionEngine — pooled, generic
ConnectionEngine, // pool-free, generic
PooledConnectionEngine,
SQLConnectionEngine, // pool-free, SQL surface
SQLEngine, // pooled, SQL surface
} from '@tundralibs/drivers/base';import { BaseEngine } from '@tundralibs/drivers/base';
import type {
EngineCapabilities,
EngineEvents,
EngineOptions,
} from '@tundralibs/drivers/types';
// Needs a separate install: deno add @tundralibs/utils
import type { EventOptionKeys } from '@tundralibs/utils';
// Whatever your protocol client looks like.
type MyConnection = {
closed: boolean;
send(command: string): Promise<string>;
close(): Promise<void>;
};
declare function connectToServer(
host: string,
port: number,
): Promise<MyConnection>;
type MyOptions = EngineOptions & {
host: string;
port?: number;
};
class MyEngine extends BaseEngine<MyConnection, MyOptions> {
public readonly Engine = 'MYDB';
public readonly Capabilities: EngineCapabilities = {
pooledConnections: true,
transactions: false,
preparedStatements: false,
};
constructor(
name: string,
options: EventOptionKeys<MyOptions, EngineEvents>,
) {
super(name, options, { port: 1234 });
// Throws `EngineError('MISSING_CONFIG_VALUE')` on the first name in the
// list that wasn't supplied. Call after `super()` for every option a
// concrete engine can't function without.
this._requireOptions(['host']);
}
protected async _createResource(): Promise<MyConnection> {
return await connectToServer(
this._getOption('host')!,
this._getOption('port')!,
);
}
protected async _destroyResource(c: MyConnection): Promise<void> {
await c.close();
}
protected override _validateResource(c: MyConnection): boolean {
return !c.closed;
}
protected async _ping(c: MyConnection): Promise<boolean> {
try {
await c.send('PING');
return true;
} catch {
return false;
}
}
// Public methods use the inline pool helpers.
public async hello(): Promise<string> {
if (this._status !== 'READY') await this.connect();
const c = await this._acquire();
try {
return await c.send('HELLO');
} finally {
this._release(c);
}
}
}EngineOptions is the base option shape. Subclass option types extend it.
| Option | Type | Default | Notes |
|---|---|---|---|
host |
string |
— | Optional at the base level; subclasses enforce. |
port |
number |
— | Integer 1..65535. |
username |
string |
— | Optional. |
password |
string |
— | Optional. |
database |
string | number |
— | Most engines use string DB name; Redis uses numeric index. |
pool |
{ min, max, idleTimeoutSeconds, acquireTimeoutSeconds } |
unset → single-conn | See Pool semantics. |
ssl |
boolean | { ca, cert, key, certFile, keyFile, caFile, rejectUnauthorized, enforce } |
— |
compat TLSOptions plus engine-only enforce (default true). Inline PEM via cert/key/ca (ca is string[]) or paths via certFile/keyFile/caFile. enforce: false falls back to plaintext on TLS failure. |
idGenerator |
(prefix?: string) => string |
ULID with prefix | Used for query / transaction ids. |
enforce: false("fall back to plaintext on TLS failure") is not uniform across engines — it only works where the engine owns its wire protocol and can retry the socket itself:
Engine enforce: falsebehaviorPostgres / Redis / Memcached Retries the connection in plaintext, emits notice.MariaDB Ignored — npm:mariadbhas no downgrade path.MongoDB Ignored — configure TLS in the connection URI. SQLite Ignored — embedded, no network. Setting
enforce: falseagainst MariaDB/MongoDB/SQLite silently does nothing;enforce: true(the default) throws on TLS failure everywhere.
State machine: CLOSED → CONNECTING → READY → CLOSED. There is no
WAITING state — pool saturation is reflected in poolStats.waiting.
| Method | Behavior |
|---|---|
connect() |
Idempotent. Creates min warm connections in parallel via _ensureMin. Fires connect on success, connectionFailed on error. |
disconnect() |
Idempotent. Drains the pool: rejects pending waiters, destroys idle resources, lets active resources self-destroy on _release. |
ping() |
Returns false (rather than throws) when the engine is CLOSED or the underlying ping fails. Acquires/releases internally. |
status |
Read-only getter. |
poolStats |
{ total, active, idle, waiting } snapshot. |
The pool lives inline on the engine. Two modes:
Single-connection (default — no pool option):
-
min: 1, max: 1, idleTimeoutMs: 0(no eviction) - One warm connection. A second caller wanting it while it's checked out
queues for a fixed 30s before rejecting with
POOL_ACQUIRE_TIMEOUT— this default is hard-coded for the no-poolcase and cannot be changed from it. - Right when sitting behind PgBouncer / pgcat / RDS Proxy — no pool-on-pool.
Need a different acquire timeout (including unbounded,
0) on a single connection? You must configurepoolexplicitly —pool: { min: 1, max: 1, acquireTimeoutSeconds: 0 }— since the default (unconfigured) single-connection path always uses the fixed 30s value.
Multi-connection (with pool option):
-
pool.minwarm connections kept;pool.maxcap; idle ones evicted afterpool.idleTimeoutSeconds(won't drop below min) - New
_acquirecalls pastmaxqueue withpool.acquireTimeoutSeconds - Validates each idle resource via
_validateResourcebefore handing it back; on failure, destroys and tries the next idle - A freed connection handed directly to a queued waiter is validated the same way — a connection that died while checked out is destroyed and the waiter is given a freshly created one, never the corpse
-
pool.maxis never exceeded, including while a freed connection is mid-validation on its way to a waiter: it stays counted against the cap for that whole window, so a concurrent_acquirequeues instead of opening a surplus connection - Destroying a connection (
_destroy, or a failed validation) frees a pool slot, so a queued waiter is backfilled with a new connection rather than left to time out
import type { EngineOptions } from '@tundralibs/drivers/types';
// The engine from Quick Start above.
declare class MyEngine {
constructor(name: string, options: EngineOptions & { host: string });
}
const single = new MyEngine('one', { host: '...' }); // 1 conn
const pooled = new MyEngine('many', {
host: '...',
pool: { min: 2, max: 10, idleTimeoutSeconds: 60 },
});| Hook | Required | Default | Purpose |
|---|---|---|---|
_createResource |
yes | — | Open and return one fresh connection. Called by _acquire / _ensureMin. |
_destroyResource |
yes | — | Close one connection. Called by _release/_destroy/_drain. Errors are swallowed. |
_ping |
yes | — | Liveness check on a given resource. |
_validateResource |
no | true |
Health check before an idle resource is reused or handed to a waiter. Return false to destroy + replace. |
Call this._requireOptions(['host', 'database']) from the constructor, after
super(...), to enforce required connection config: it throws
EngineError('MISSING_CONFIG_VALUE') naming the first key in the list that
wasn't supplied. See Quick Start above for a
worked example.
Inside subclass methods, use the protected pool helpers:
| Helper | Purpose |
|---|---|
_acquire(timeoutMs?) |
Get a connection (queue if pool saturated). |
_release(resource) |
Return a connection to the pool (or hand to a waiter). |
_destroy(resource) |
Force-destroy a broken connection (don't return). |
Subscribe via engine.on('eventName', handler) or supply via the
_on<event> option key at construction.
| Event | Payload | When |
|---|---|---|
connect |
(instanceId) |
After connect() succeeds. |
disconnect |
(instanceId) |
After disconnect() succeeds. |
connectionFailed |
(instanceId, error) |
When connect() fails. Handler type is Error; always an EngineError instance at runtime. |
error |
(instanceId, error) |
When disconnect() fails. Handler type is Error; always an EngineError instance at runtime. |
warn |
(instanceId, message) |
Misc warnings — "we noticed something off". |
notice |
(instanceId, message) |
Server-side notice / informational message (Postgres NOTICE, MariaDB warning, Redis/Memcached TLS-downgrade notices). Distinct from warn — "the server told us something". |
import type { EngineError } from '@tundralibs/drivers/errors';
import type { EngineEvents, EngineOptions } from '@tundralibs/drivers/types';
// Needs a separate install: deno add @tundralibs/utils
import type { EventOptionKeys } from '@tundralibs/utils';
// The engine from Quick Start above.
declare class MyEngine {
constructor(
name: string,
options: EventOptionKeys<EngineOptions & { host: string }, EngineEvents>,
);
}
const engine = new MyEngine('app', {
host: '...',
_onconnect: (id) => console.log('connected', id),
// Handler type is `Error`; always an `EngineError` at runtime.
_onconnectionFailed: (id, err) =>
console.error(id, (err as EngineError).code, err.message),
});EngineError is the only error class drivers throw. Code is one of the
values in EngineErrorCodes. See
Drivers.md → Standardized SQL error codes
for the SQL-engine-specific subset.
import { EngineError } from '@tundralibs/drivers/errors';
import { MemcachedEngine } from '@tundralibs/drivers/memcached';
const engine = new MemcachedEngine('app-cache', { host: 'localhost' });
try {
await engine.connect();
} catch (e) {
if (e instanceof EngineError && e.code === 'CONNECTION_FAILED') {
// ...
}
}