SIAX Technology (sax3l)

@siax/pg-pool-factory (0.1.1)

Published 2026-09-16 22:50:28 +00:00 by admin

Installation

@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/
npm install @siax/pg-pool-factory@0.1.1
"@siax/pg-pool-factory": "0.1.1"

About this package

createPgPool() -- the ONE constructor this estate uses for a node-postgres Pool, generalized out of b00k's real pg-pool.ts crash fix (git.siax.io/sax3l/b00k, 2026-09-14/15). Mandatory built-in '.on("error", ...)' listener attached synchronously before the pool is returned, so an idle client that loses its backend connection (Postgres restart/failover, pg_terminate_backend, a network blip) never re-throws as an uncaught exception and crashes the whole process -- the identical bug found and fixed independently 11 times across the estate on 2026-09-13/15 (st0re, act0, h0st, cl0ud x2, aud0, n0d, api0, b00k, SUP0, HARD0, NET0). Degrade-and-continue by default (log + optional onError callback, never process.exit, never rethrows), sensible defaults, TypeScript types. The ONE package in this workspace with a real external dependency (pg) by design -- see README.md and ADR-019 for why a dependency-injected zero-dep design (the @siax/idempotency/@siax/outbox pattern) is not possible here.

@siax/pg-pool-factory

createPgPool() — the ONE constructor this estate uses for a node-postgres Pool. Generalized out of b00k's real packages/b00k-service/src/pg-pool.ts fix (git.siax.io/sax3l/b00k, 2026-09-14/15) into a shared, published package, so the fix exists in exactly one place instead of eleven.

The bug this closes

pg's Pool is a Node EventEmitter, and Node gives the 'error' event special treatment: an EventEmitter that emits 'error' with no listener attached re-throws it as an uncaught exception, which crashes the entire process. Pool emits exactly that event whenever an idle client sitting in the pool loses its backend connection for a reason unrelated to any in-flight query — a Postgres restart/failover, pg_terminate_backend, a network blip, the primary going through a fast/immediate shutdown. None of that is exceptional for a long-lived service; it is Tuesday. Without a listener, the next such event (not the first query failure, which already rejects its own promise and is handled by ordinary try/catch) takes the whole process down with it — including every OTHER in-flight request the process was serving, none of which had anything to do with the pool that lost a connection.

Found independently, fixed independently, 11 times, 2026-09-13/15: st0re, act0, h0st, cl0ud (×2), aud0, n0d, api0, b00k, SUP0, HARD0, NET0. Every fix was the same three lines. b00k's fix was the one shaped as a real reusable helper (createPgPool(config, label)) rather than an inline patch, and it is what this package generalizes — see docs/architecture/decisions/ADR-019-pg-pool-factory-shared-package.md for why that matters: without a shared package, any NEW repo — or any of those 11, if someone constructs a second pg.Pool by hand later — can reintroduce this exact bug with zero warning. siax-doctor pg-pool-factory (see below) is the automated half of closing that gap; this package is the standardized-fix half.

Usage

import { createPgPool } from '@siax/pg-pool-factory';

const pool = createPgPool(
  { connectionString: process.env.DATABASE_URL, max: 10 },
  { label: 'account-store', onError: (err) => metrics.increment('pg_pool_error', { pool: 'account-store' }) },
);

options (the second argument) may also be a plain string — equivalent to { label: options }. This matches b00k's original createPgPool(config, label) call shape exactly, so migrating a consumer that already has that pattern is a one-line import swap:

- import { createPgPool } from './pg-pool.js';
+ import { createPgPool } from '@siax/pg-pool-factory';

  const pool = createPgPool({ connectionString }, 'account-store');

(This is exactly what b00k's own migration in this PR series does — see git.siax.io/sax3l/b00k's branch adding this dependency.)

What it does

  • Constructs a real pg.Pool via new Pool({ ...POOL_DEFAULTS, ...config }) (config always wins on conflict).
  • Attaches a mandatory .on('error', ...) listener synchronously, in the same tick as construction, before the pool is returned — there is no window in which the returned pool could emit 'error' unheard.
  • Degrade-and-continue by default (the doctrine already established across all 11 individual fixes this session): logs (via console or a caller-supplied logger), and optionally calls a caller-supplied onError(err, { label }). Never calls process.exit, never rethrows. A consumer that genuinely wants a crash-on-pool-error policy can do that from inside its own onError — it is never this package's default.
  • Both the logger and onError are wrapped in try/catch: a bug in either one is logged and swallowed, never left to propagate back into Node's EventEmitter machinery and resurrect the exact crash this package exists to prevent.
  • One sensible default: connectionTimeoutMillis: 10_000. pg's own built-in default is 0 (wait forever for a connection from the pool) — a footgun for a long-lived service, since a saturated pool then hangs every caller instead of failing fast. Every other option is left at pg's own default.

Why this package has a real external dependency (pg)

Every other packages/* in this workspace is either genuinely zero-dependency or links only to another @siax/* workspace package (see CONTRIBUTING.md's "Kodstandard (doctor)" — that rule is scoped to packages/doctor specifically, and packages/doctor/src/commands/ pg-pool-factory.mjs, this package's own doctor command, stays true to it: pure regex/text source scanning, no pg import at all).

@siax/idempotency and @siax/outbox achieve zero-dependency by being dependency-injected — they accept a pool.query-shaped function and never import pg themselves. That pattern is not available here: this package's entire reason to exist is constructing the pg.Pool object correctly in the first place. There is no way to wrap "safely construct a pg.Pool" without depending on pg. See ADR-019 for the full reasoning.

Tests

node --test test/*.test.mjs
  • pg-pool.test.mjs — unit-level behavior: the listener is registered synchronously (before this function returns, i.e. before any query could possibly run — no .query()/.connect() call happens anywhere in these tests before the assertion), config-merging defaults, the string-label shorthand, onError/logger wiring, and that a throwing onError or logger can never resurrect the crash this package prevents.
  • crash-prevention.test.mjs — the core claim, proven as a real process-level fact, not a mock: two fixtures (test/fixtures/unsafe-pool-crashes.mjs, .../safe-pool-survives.mjs) each construct a REAL pg.Pool (the genuine pg package) and trigger the identical fault pg.Pool itself emits in production (pool.emit('error', ...)) — one via new Pool() with no listener, one via createPgPool(). Each fixture runs as its OWN child process (so a real crash in one can't take the test runner down with it), and the test asserts on that child's REAL exit code: the unsafe fixture crashes (non-zero exit, Node's real Unhandled 'error' event in stderr); the createPgPool() fixture exits 0.
  • live-postgres.test.mjs — the real end-to-end reproduction against an ACTUAL Postgres server: an idle client's backend connection is killed for real (SELECT pg_terminate_backend($1) from a second connection — one of the exact real-world triggers this package's module doc names), and the createPgPool() pool is proven to both survive it AND keep serving new queries afterward. Skip-gated on PG_POOL_FACTORY_LIVE_DATABASE_URL, the same established pattern as packages/outbox/test/r004-real-postgres-evidence.test.mjs — no CI job in this repo currently wires a live Postgres service in, so it is skipped there, but it genuinely runs end-to-end the moment the env var points at a real server (verified manually this session against a throwaway postgres:16-alpine container — see the PR description for the real transcript).

Licence / ägare

SIAX Technology AB (@sax3l). Paketnamn @siax/pg-pool-factory (publicering till Giteas npm-registry: samma blockerare som resten av arbetsytan, se prompts/003-publish-doctor-to-registry.md — token med write:package).

Dependencies

Dependencies

ID Version
pg ^8.13.1
Details
npm
2026-09-16 22:50:28 +00:00
10876
MIT
latest
7.1 KiB
Assets (1)
Versions (2) View all
0.1.1 2026-09-16
0.1.0 2026-09-16