Skip to content

Drivers BaseEngine

GitHub Actions edited this page Sep 18, 2026 · 1 revision

BaseEngine

Abstract base class for every driver engine. Owns connection lifecycle, inline pool, SSL loading, and lifecycle events.

Deno Bun Node.js

Table of Contents

Overview

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 instanceId of 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.

Where to import the bases from

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';

Quick Start (subclassing)

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);
    }
  }
}

Configuration

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: false behavior
Postgres / Redis / Memcached Retries the connection in plaintext, emits notice.
MariaDB Ignored — npm:mariadb has no downgrade path.
MongoDB Ignored — configure TLS in the connection URI.
SQLite Ignored — embedded, no network.

Setting enforce: false against MariaDB/MongoDB/SQLite silently does nothing; enforce: true (the default) throws on TLS failure everywhere.

Lifecycle

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.

Pool semantics

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-pool case 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 configure pool explicitly — 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.min warm connections kept; pool.max cap; idle ones evicted after pool.idleTimeoutSeconds (won't drop below min)
  • New _acquire calls past max queue with pool.acquireTimeoutSeconds
  • Validates each idle resource via _validateResource before 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.max is 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 _acquire queues 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 },
});

Hooks for subclasses

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).

Events

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),
});

Errors

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') {
    // ...
  }
}

← Back to Drivers

Clone this wiki locally