@siax/pg-pool-factory (0.1.0)
Installation
@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/npm install @siax/pg-pool-factory@0.1.0"@siax/pg-pool-factory": "0.1.0"About this package
@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.Poolvianew Pool({ ...POOL_DEFAULTS, ...config })(configalways 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
consoleor a caller-suppliedlogger), and optionally calls a caller-suppliedonError(err, { label }). Never callsprocess.exit, never rethrows. A consumer that genuinely wants a crash-on-pool-error policy can do that from inside its ownonError— it is never this package's default. - Both the logger and
onErrorare 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 is0(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 atpg'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/loggerwiring, and that a throwingonErrororloggercan 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 REALpg.Pool(the genuinepgpackage) and trigger the identical faultpg.Poolitself emits in production (pool.emit('error', ...)) — one vianew Pool()with no listener, one viacreatePgPool(). 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 realUnhandled 'error' eventin stderr); thecreatePgPool()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 thecreatePgPool()pool is proven to both survive it AND keep serving new queries afterward. Skip-gated onPG_POOL_FACTORY_LIVE_DATABASE_URL, the same established pattern aspackages/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 throwawaypostgres:16-alpinecontainer — 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 |