Sign In

@ultimat3/db

Package Overview
Dependencies
Maintainers
1
Versions
17
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
6.0.0
to
7.0.0
+21
-4
CLAUDE.md

@@ -17,2 +17,3 @@ # @ultimat3/db — agent notes

| New code | add to `DB_ERROR_CODES` **and** `DB_ERROR_TITLES` in `errors.ts` |
| A value ambient across an `await` | `asyncContext<T>(subject)` from `@ultimat3/core` — never `new AsyncLocalStorage`. Three scopes here use it: `transaction.ts`, `attribution.ts`, `expected-loop.ts` |
| Exports | explicit in `src/index.ts`; no `export *` |

@@ -32,2 +33,18 @@ | Files | < 200 LOC, one responsibility, `kebab-case.ts`, test beside source |

**The three ambient scopes open through core's one lazy seam, and that is a build error rather than
a convention, `As of 2026-08`.** `transaction.ts` (`TxState`), `attribution.ts` (the entity/op
pair) and `expected-loop.ts` (the reason) each constructed a module-scope `AsyncLocalStorage` until
issue #255 closed it. A bundler stubs `node:async_hooks` to `{}` — Bun's `target: 'browser'` emits
`var { AsyncLocalStorage } = (() => ({}))` — so the `new` threw
`TypeError: undefined is not a constructor` at module **evaluation**, before any app code ran, and
took every importer of that file down with it. Through `asyncContext<T>(subject)` the module
evaluates, `get()` answers `undefined` (in a browser nothing IS in flight, so that is the true
answer) and `run()` throws `X_ASYNC_CONTEXT_UNAVAILABLE` naming the scope. Deferring the
construction changes nothing a server can observe: the storage is built on the first `get()` or
`run()` rather than at module load, and `getStore()` outside a scope answers `undefined` either
way — one object per scope, on first use, in place of one at module evaluation.
`scripts/async-context-guard.ts` refuses a `new AsyncLocalStorage` — and the import that binds the
class, aliased or namespaced — anywhere but `packages/core/src/async-context.ts`, and
`scripts/async-context-guard.test.ts` runs it over the tree in the gate's `unit` step.
`pglite.ts` is a pool of exactly one: PGlite is a single session, so `reserve()` (backed by

@@ -50,3 +67,3 @@ `pglite-turns.ts`) is what stops two concurrent `BEGIN`s becoming one transaction. Three rules

different questions and reading the second as the first was a cross-transaction write. The
`AsyncLocalStorage` store rides into every promise chain started inside `withTransaction`, so a
async-context store rides into every promise chain started inside `withTransaction`, so a
statement the app forgot to `await` still found a store after COMMIT, skipped the turn queue, and

@@ -219,3 +236,3 @@ landed inside whichever unit of work held the single session next: measured `BEGIN`, `select 'inside

fn)` runs `fn` with every statement it issues — at any depth, across every `await` — attributed to
that pair, on an `AsyncLocalStorage` the same shape `expected-loop.ts` already uses. Four rules,
that pair, on an async context the same shape `expected-loop.ts` already uses. Four rules,
none optional. **Guard first** — it reads `statementObserver()` before touching the scope at all

@@ -270,3 +287,3 @@ and, with nothing installed, hands straight to `fn`: one property read, one branch, no object

a pragma or a list is the same reason `observe.ts` is one observer: a second path is the tax
(axiom 1). `expectedQueryLoop(reason, fn)` rides an `AsyncLocalStorage`, so it survives every
(axiom 1). `expectedQueryLoop(reason, fn)` rides an async context, so it survives every
`await` at any depth and two loops running concurrently never read each other; nesting keeps the

@@ -278,3 +295,3 @@ innermost reason, because the closest scope is the one describing this loop. A blank reason is

put the answer on the event as `expected`; a detector that judges a whole request runs long after
every scope in it closed, so reading the ALS later would find nothing. **It suppresses a verdict,
every scope in it closed, so reading the scope later would find nothing. **It suppresses a verdict,
not a statement** — the SQL is still sent, still observed, and the span still opens, so anything

@@ -281,0 +298,0 @@ that measures still sees the loop and only the thing that warns is told the author already

+2
-2
{
"name": "@ultimat3/db",
"version": "6.0.0",
"version": "7.0.0",
"description": "Postgres access, transactions, migrations and drift detection",

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

"dependencies": {
"@ultimat3/core": "6.0.0"
"@ultimat3/core": "7.0.0"
},

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

@@ -264,3 +264,3 @@ # @ultimat3/db 🐘

|---|---|
| Scope | an `AsyncLocalStorage`: it survives every `await` at any depth, and two loops running at once never read each other. Nesting keeps the innermost reason |
| Scope | core's `asyncContext<string>('the expected-loop reason')`, never a `new AsyncLocalStorage` here: it survives every `await` at any depth, and two loops running at once never read each other. Nesting keeps the innermost reason |
| What it carries | `StatementEvent.expected`, stamped by both funnels at settle time — a diagnostic judging a whole request runs after every scope in it closed |

@@ -274,2 +274,20 @@ | What it suppresses | a **verdict**, never a statement. The SQL is still sent, still observed, still a span: only the thing that warns is told the author already answered |

**Every ambient scope in this package opens through `asyncContext<T>(subject)` from
`@ultimat3/core`** — the transaction store, the attribution pair and this reason — and none of the
three constructs an `AsyncLocalStorage`, `As of 2026-08`. What changed is what a browser bundle
does with these three modules: a bundler stubs `node:async_hooks` to `{}`, so the module-scope `new`
threw `TypeError: undefined is not a constructor` at module **evaluation** — before a line of app
code ran, and taking every importer of the file with it. Now the module evaluates, a read answers
`undefined` (nothing is in flight in a browser, so that is the true answer), and a write throws
`X_ASYNC_CONTEXT_UNAVAILABLE` naming the scope it could not open. A server saves no allocation —
the store is built on the first `get()` **or** `run()`, so a read constructs it too. What the
laziness costs is nothing observable: `getStore()` outside a scope answers `undefined` whether the
storage was ever constructed or not. Not a claim that the whole package
bundles: `pglite-branch.ts` imports `node:fs/promises`, which is a separate question.
The rule is a **build error**, not a convention: `scripts/async-context-guard.ts` refuses a
`new AsyncLocalStorage` — and the import that binds the class, aliased or namespaced — anywhere but
`packages/core/src/async-context.ts`, and `scripts/async-context-guard.test.ts` runs it over the
tree in the gate's `unit` step.
## A statement knows who compiled it

@@ -287,3 +305,3 @@

|---|---|
| Scope | an `AsyncLocalStorage`, `expectedQueryLoop()`'s own shape: it survives every `await` at any depth, and nesting keeps the innermost pair |
| Scope | core's `asyncContext<StatementAttribution>()`, `expectedQueryLoop()`'s own shape: it survives every `await` at any depth, and nesting keeps the innermost pair |
| What it carries | `StatementEvent.attribution`, stamped by both funnels at settle time, next to `expected` |

@@ -290,0 +308,0 @@ | Producer | `@ultimat3/entity`'s `postgresRepo` — the last caller that still knows the entity and the operation once the SQL exists |

@@ -6,9 +6,11 @@ // Single responsibility: carry "which entity, which operation" from the layer that compiled a

// `node:` for the same reason `expected-loop.ts` needs it — Bun exposes no native async-context
// primitive, and the pair has to survive every `await` between the repository call and the
// statement it causes. A module-scope variable would be shared by two concurrent requests.
import { AsyncLocalStorage } from 'node:async_hooks';
// The pair has to survive every `await` between the repository call and the statement it causes,
// and a module-scope variable would be shared by two concurrent requests — so it needs an async
// context. It opens through core's seam for the same reason `expected-loop.ts` does: constructing
// an `AsyncLocalStorage` here threw at module EVALUATION in a browser bundle, where
// `node:async_hooks` is stubbed to `{}`, taking every importer of `@ultimat3/db` down with it.
import { asyncContext } from '@ultimat3/core';
import { type StatementAttribution, statementObserver } from './observe';
const storage = new AsyncLocalStorage<StatementAttribution>();
const storage = asyncContext<StatementAttribution>('the statement attribution');

@@ -45,3 +47,3 @@ /**

export function statementAttribution(): StatementAttribution | undefined {
return storage.getStore();
return storage.get();
}

@@ -64,3 +64,3 @@ // Single responsibility: the Postgres connection and the ambient `db()` handle. Pool size and

/** Sized per role because the failure modes differ: RPS bursts vs. queue depth vs. run-once. */
export const POOL_PROFILES: Readonly<Record<Role, PoolProfile>> = Object.freeze({
export const POOL_PROFILES = Object.freeze<Record<Role, PoolProfile>>({
web: {

@@ -67,0 +67,0 @@ max: 20,

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

*/
const SQLSTATE_FIXES: Readonly<Record<DbSqlStateCode, string>> = Object.freeze({
const SQLSTATE_FIXES = Object.freeze<Record<DbSqlStateCode, string>>({
X_DB_UNIQUE_VIOLATION:

@@ -127,0 +127,0 @@ 'upsertAll(rows, { onConflict: [...] }) over the columns {constraint} covers — ' +

@@ -6,9 +6,10 @@ // Single responsibility: the one way to declare that a loop of statements is deliberate, so a

// `node:` because Bun exposes no native async-context primitive: the reason has to outlive every
// `await` inside the scope, and `AsyncLocalStorage` is the only thing that carries a value across
// them. A module-scope variable would be shared by two concurrent loops.
import { AsyncLocalStorage } from 'node:async_hooks';
import { assert } from '@ultimat3/core';
// The reason has to outlive every `await` inside the scope and a module-scope variable would be
// shared by two concurrent loops, so it needs an async context — opened through core's one lazy
// seam rather than a `node:async_hooks` construction here, which threw at module EVALUATION in a
// browser bundle (the bundler stubs the module to `{}`) and took every importer of `@ultimat3/db`
// with it.
import { assert, asyncContext } from '@ultimat3/core';
const storage = new AsyncLocalStorage<string>();
const storage = asyncContext<string>('the expected-loop reason');

@@ -53,3 +54,3 @@ /**

export function expectedQueryLoopReason(): string | undefined {
return storage.getStore();
return storage.get();
}

@@ -6,4 +6,3 @@ // Single responsibility: transaction scope. The open `DbTx` rides an AsyncLocalStorage rather

import { AsyncLocalStorage } from 'node:async_hooks';
import { assert, nanoid } from '@ultimat3/core';
import { assert, asyncContext, nanoid } from '@ultimat3/core';
import { baseClient, type DbClient, type DbConnection, isReservable } from './client';

@@ -83,7 +82,11 @@ import { serializationExhausted } from './errors';

const storage = new AsyncLocalStorage<TxState>();
// Core's one lazy seam, never a construction here: a module-scope `new` threw at EVALUATION in a
// browser bundle, where the bundler stubs `node:async_hooks` to `{}`, and took every importer of
// `@ultimat3/db` with it. `get()` still answers `undefined` outside a scope, so the server pays
// nothing for the deferral.
const storage = asyncContext<TxState>('a database transaction');
/** The open transaction, or `undefined` outside one. `@ultimat3/jobs` calls this per enqueue. */
export function currentTx(): DbTx | undefined {
return storage.getStore()?.tx;
return storage.get()?.tx;
}

@@ -101,3 +104,3 @@

export function inLiveTx(): boolean {
return storage.getStore()?.live.value === true;
return storage.get()?.live.value === true;
}

@@ -223,3 +226,3 @@

);
const outer = storage.getStore();
const outer = storage.get();
if (outer !== undefined) {

@@ -226,0 +229,0 @@ // A nested scope is a SAVEPOINT, and a savepoint cannot survive the thing `retry` exists for: