New:Socket for Asana Is Now Available.Learn more
Get Started

@ultimat3/db

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/db - npm Package Compare versions

Comparing version
18.0.0
to
19.0.0
+2
-2
package.json
{
"name": "@ultimat3/db",
"version": "18.0.0",
"version": "19.0.0",
"description": "Postgres access, transactions, migrations and drift detection",

@@ -34,3 +34,3 @@ "license": "MIT",

"dependencies": {
"@ultimat3/core": "18.0.0"
"@ultimat3/core": "19.0.0"
},

@@ -37,0 +37,0 @@ "peerDependencies": {

@@ -1,5 +0,7 @@

// Single responsibility: the slice of `Bun.SQL` this package uses, declared structurally, and the
// lazy lookup of the global that provides it. Reached through a function so importing the client
// never touches `Bun` at module evaluation — the CLI imports it to print help.
// Single responsibility: the slice of `Bun.SQL` this package uses, declared structurally, the lazy
// lookup of the global that provides it, and the one safe way to hand a pinned connection back.
// Reached through a function so importing the client never touches `Bun` at module evaluation —
// the CLI imports it to print help.
import { logger, renderThrowable } from '@ultimat3/core';
import { dbUnavailable } from './errors';

@@ -10,5 +12,45 @@

unsafe(text: string, values?: readonly unknown[]): Promise<unknown>;
release(): void;
/**
* **Answers a PROMISE, and typing it `void` is what made both callers float it.** Measured on
* Bun 1.3.14 and 1.4.0 against a real server: `release()` returns a promise on both, and on
* 1.3.14 that promise REJECTS with `ERR_POSTGRES_CONNECTION_CLOSED` when the pool has already
* been closed. Nothing was attached to it, so it surfaced as an UNHANDLED REJECTION — which Bun
* takes the process down for. `unknown` rather than `Promise<void>` because a fake reserved
* connection returns nothing at all, and the caller has to handle both anyway.
*/
release(): unknown;
}
/**
* Hand a pin back, totally. The one place that knows `release()` answers a promise, so neither
* caller can forget it (axiom 1) — `client.ts`'s `DbConnection.release` and `pool-reserve.ts`'s
* late arrival both route here.
*
* A failed release is **best-effort, exactly where a throw would mask the error that caused it** —
* the rule this package already applies to `ROLLBACK`. `[Symbol.dispose]` is `DbConnection.release`
* itself, so a throw there replaces whatever error reached the `using` block, or invents one where
* the body succeeded. And the news is unactionable: the connection this would hand back is gone
* either way.
*
* Reachable, and reachable BECAUSE `close()` is bounded: an abandoned drain leaves every
* still-pinned connection to be released against a pool that no longer exists.
*/
export function releaseReserved(reserved: BunSqlReserved): void {
const report = (error: unknown): void => {
logger.debug('db.release_failed', { error: renderThrowable(error) });
};
let settled: unknown;
try {
settled = reserved.release();
} catch (error) {
report(error);
return;
}
// `then` and not `instanceof Promise`: the value comes from the driver, and a thenable is the
// contract every await in this package already relies on.
if (typeof (settled as PromiseLike<unknown> | undefined)?.then === 'function') {
void (settled as PromiseLike<unknown>).then(undefined, report);
}
}
/** The slice of `Bun.SQL` we use. Declared structurally so this package has no dependency. */

@@ -15,0 +57,0 @@ export interface BunSqlDriver {

@@ -8,3 +8,3 @@ // Single responsibility: the pooled Postgres client and the ambient `db()` handle — the lazy

import { type Role, resolveRole } from '@ultimat3/core';
import { type BunSqlDriver, type BunSqlReserved, bunSqlFactory } from './bun-sql';
import { type BunSqlDriver, type BunSqlReserved, bunSqlFactory, releaseReserved } from './bun-sql';
import { connectionUrl } from './connection-url';

@@ -14,3 +14,3 @@ // Deliberate cycle, the same shape as `client.ts ⇄ transaction.ts`: nothing here is referenced at

import { defaultClient } from './default-client';
import { DbError, driverError } from './errors';
import { DbError, drainTimeout, driverError } from './errors';
import { assertPoolProfile, type PoolProfile, poolProfileFor } from './pool-profile';

@@ -125,3 +125,4 @@ import { reserveWithin } from './pool-reserve';

held = false;
reserved.release();
// Total by construction — `releaseReserved` owns the reason (`bun-sql.ts`).
releaseReserved(reserved);
};

@@ -147,3 +148,36 @@ return {

driver = undefined;
await pool?.close();
if (pool === undefined) return;
// BOUNDED, `As of 2026-08-27`, and through the driver's OWN option rather than a race here.
// This was a bare `await pool.close()`, and `Bun.SQL`'s `end()` waits on an outstanding
// reserved connection without ever giving up — measured three runs per case on Bun 1.3.14
// AND 1.4.0, no database outage involved (#394). So a role whose database went away
// mid-shutdown never finished shutting down, and the operator's only signal was a container
// that burned its whole termination grace period before SIGKILL.
//
// `BunSqlDriver.close` has declared `{ timeout }` since this port was written and NOTHING
// ever passed it — the capability was in the seam, unused, exactly like `setOfflineMode` on
// the CDP port. Measured with a reserve outstanding: `close({ timeout: 1 })` returns in
// ~1002ms on 1.3.14, 1.4.0 and 1.4.1-canary alike, where a bare `close()` never returns.
//
// **The unit is SECONDS**, not milliseconds. `timeout: 5000` would be an eighty-three minute
// shutdown budget, which is the same hang with extra steps.
if (profile.drainTimeoutMs === 0) {
// `migrate` and `replicator`, for `acquireTimeoutMs`' reason: a run-once role cutting off
// its own session mid-statement is worse than a slow exit.
await pool.close();
return;
}
// `performance.now()`, never `Date.now()`, and the reason is this repo rather than NTP: the
// framework preload freezes `Date` for every test in the tree (`installDeterminism`), so a
// duration subtracted from `Date.now()` is 0 in all of them — the branch below could not
// fire, and the test asserting it would have been one that cannot fail.
const started = performance.now();
await pool.close({ timeout: profile.drainTimeoutMs / 1000 });
// The driver RESOLVES when it gives up — it does not reject — so the elapsed time is the only
// thing that separates "drained" from "abandoned". Reporting it is the point: a drain that
// silently gave up looks exactly like a clean one, and the work still in flight is lost with
// no line anywhere saying so. The pool is gone either way, which is why this is terminal.
if (performance.now() - started >= profile.drainTimeoutMs) {
throw drainTimeout(profile.drainTimeoutMs, role);
}
},

@@ -150,0 +184,0 @@ };

@@ -28,2 +28,3 @@ // The database layer's stable error codes. Every factory produces the exact command that

'X_DB_POOL_EXHAUSTED',
'X_DB_DRAIN_TIMEOUT',
'X_DB_DRIFT',

@@ -70,2 +71,3 @@ 'X_MIGRATION_CONFLICT',

X_DB_POOL_EXHAUSTED: 'no connection was available',
X_DB_DRAIN_TIMEOUT: 'the pool did not drain inside its shutdown budget',
X_DB_DRIFT: 'schema differs from migrations',

@@ -244,2 +246,20 @@ X_MIGRATION_CONFLICT: 'the migration ledger disagrees with this build',

/**
* `close()` gave up waiting for the pool. TERMINAL, and deliberately not retryable: the pool is
* gone either way — `close()` clears the handle before it awaits — so a caller that retried would
* be closing a pool that no longer exists. What this reports is that connections were still held
* when the process stopped waiting, which is a fact about the shutdown an operator has to see.
*
* The alternative was to resolve quietly on the deadline, and that is the version that hides the
* bug: a drain that silently gave up looks exactly like a clean one, and the rows still in flight
* are lost with no line anywhere saying so.
*/
export const drainTimeout = (ms: number, role: string): DbError =>
new DbError({
code: 'X_DB_DRAIN_TIMEOUT',
cause: `the ${role} pool still held connections after ${String(ms)}ms, so close() stopped waiting`,
fix: `find the statement that will not finish — psql "$DATABASE_URL" -c "select pid, state, query from pg_stat_activity where state <> 'idle'" — or raise drainTimeoutMs in createPostgresClient({ profile }) for the ${role} role`,
meta: { drainTimeoutMs: ms, role },
});
/**
* The pool answered nothing inside `acquireTimeoutMs`. Distinct from the server's own `53300` and

@@ -246,0 +266,0 @@ * deliberately the same code: to a caller both mean "there was no connection for this unit of

@@ -1,2 +0,2 @@

// Single responsibility: the five numbers a Postgres pool runs on — the per-role defaults, the one
// Single responsibility: the six numbers a Postgres pool runs on — the per-role defaults, the one
// environment override an operator may layer over them, and the screen every resolved profile

@@ -28,2 +28,20 @@ // passes. Split from `client.ts`, which now owns connecting and nothing about sizing.

readonly acquireTimeoutMs: number;
/**
* How long `close()` may wait for the pool to drain before `X_DB_DRAIN_TIMEOUT`. 0 waits forever.
*
* **A drain that cannot finish is the failure this bounds, and it is not hypothetical.** Measured
* against a real Postgres, three runs per case: `Bun.SQL`'s `end()` waits on an outstanding
* RESERVED connection and never stops waiting — 3 of 3 on Bun 1.3.14 *and* 3 of 3 on 1.4.0, with
* no database outage involved at all. Once that connection's backend has been terminated it
* becomes a race, which 1.3.14 loses 3 of 3 and 1.4.0 loses 1 of 3. So the runtime is not the
* variable; an unbounded await is (#394).
*
* What that cost, before this: `releaseQueue` awaits `db.close()`, so a role whose database went
* away mid-shutdown never finished shutting down. A container that will not drain is drained by
* SIGKILL, and the operator's only signal is a pod that took its full termination grace period.
*
* `migrate` and `replicator` wait forever, deliberately, for `acquireTimeoutMs`' reason: a
* run-once role cutting off its own session mid-statement is worse than a slow exit.
*/
readonly drainTimeoutMs: number;
}

@@ -39,2 +57,3 @@

acquireTimeoutMs: 5_000,
drainTimeoutMs: 5_000,
},

@@ -47,2 +66,3 @@ sync: {

acquireTimeoutMs: 5_000,
drainTimeoutMs: 5_000,
},

@@ -55,2 +75,3 @@ worker: {

acquireTimeoutMs: 10_000,
drainTimeoutMs: 15_000,
},

@@ -63,2 +84,3 @@ scheduler: {

acquireTimeoutMs: 10_000,
drainTimeoutMs: 5_000,
},

@@ -74,2 +96,3 @@ // `migrate` waits: its pool is `max: 1` and the advisory-lock pin holds it for the whole run, so

acquireTimeoutMs: 0,
drainTimeoutMs: 0,
},

@@ -82,2 +105,3 @@ replicator: {

acquireTimeoutMs: 0,
drainTimeoutMs: 0,
},

@@ -109,9 +133,9 @@ });

/**
* The five numbers a pool runs on, screened on the MERGED profile — an override is spread over a
* The six numbers a pool runs on, screened on the MERGED profile — an override is spread over a
* role default the caller never restated, so the resolved object is the only one that can be
* judged. Every one of them is a plausible `Number(process.env.…)`, which is `NaN` for an unset
* variable and not nullish, so `??` and the spread both keep it. None of the five then fails
* variable and not nullish, so `??` and the spread both keep it. None of the six then fails
* loudly: `idleTimeout: NaN` goes to `Bun.SQL`, `statement_timeout=NaN` goes into the libpq
* options string for the SERVER to reject on connect, and a timer given `NaN` fires at 1ms in this
* Bun — so a pool with free connections reports itself exhausted. `0` stays legal for the three
* Bun — so a pool with free connections reports itself exhausted. `0` stays legal for the five
* budgets that document it as "no bound"; `max` is at least one connection, or nothing can run.

@@ -132,3 +156,4 @@ */

whole('acquireTimeoutMs', profile.acquireTimeoutMs, 0);
whole('drainTimeoutMs', profile.drainTimeoutMs, 0);
return profile;
}

@@ -5,3 +5,3 @@ // Single responsibility: pinning a connection out of the pool under the profile's acquire deadline,

import type { BunSqlDriver, BunSqlReserved } from './bun-sql';
import { type BunSqlDriver, type BunSqlReserved, releaseReserved } from './bun-sql';
import { poolAcquireTimeout } from './errors';

@@ -46,3 +46,3 @@ import type { PoolProfile } from './pool-profile';

(late) => {
if (expired) late.release();
if (expired) releaseReserved(late);
},

@@ -49,0 +49,0 @@ () => undefined,

Sorry, the diff of this file is too big to display