better-ship 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/postgres.d.ts +29 -18
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +75 -70
- package/dist/postgres.js.map +1 -1
- package/package.json +1 -1
- package/src/infrastructure/postgres/connection-scope.ts +14 -0
- package/src/infrastructure/postgres/index.ts +1 -0
- package/src/infrastructure/postgres/postgres-client.ts +51 -95
- package/src/infrastructure/postgres/postgres-database-execution.ts +10 -2
- package/src/infrastructure/postgres/postgres-database-types.ts +3 -3
- package/src/infrastructure/postgres/postgres-transaction.ts +9 -16
- package/src/infrastructure/postgres/statement-outside-transaction.error.ts +14 -0
package/dist/postgres.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { W as DomainEvent, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-DUFS0SJ_.js";
|
|
1
|
+
import { W as DomainEvent, Xt as AppError, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-DUFS0SJ_.js";
|
|
2
2
|
import { A as IOutboxRepository, M as OutboxClaim, N as OutboxMessage, P as IMessageStore, k as DeadLetterReason, w as ITransaction } from "./index-CbvNd665.js";
|
|
3
3
|
import { n as eventCollector, r as DrizzleSchema, t as OutboxRepositoryOptions } from "./outbox-repository-options-gk3JOjLB.js";
|
|
4
4
|
import { ExtractTablesWithRelations } from "drizzle-orm";
|
|
5
5
|
import { NodePgDatabase, NodePgTransaction } from "drizzle-orm/node-postgres";
|
|
6
|
-
import {
|
|
6
|
+
import { Pool } from "pg";
|
|
7
7
|
//#region src/infrastructure/postgres/postgres-database-types.d.ts
|
|
8
|
-
/** The pg
|
|
9
|
-
type PostgresConnection =
|
|
8
|
+
/** The pg pool passed to Drizzle. One pool, with `max: 1`, belongs to one invocation. */
|
|
9
|
+
type PostgresConnection = Pool;
|
|
10
10
|
/** The application's Drizzle Postgres database, preserving its schema and relational query types. */
|
|
11
11
|
type PostgresDb<Schema extends DrizzleSchema> = NodePgDatabase<Schema>;
|
|
12
12
|
/** Exposes Drizzle query methods without insert, update, delete, transaction, or raw execution methods. */
|
|
@@ -269,31 +269,42 @@ export declare const messageOutbox: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
269
269
|
//#endregion
|
|
270
270
|
//#region src/infrastructure/postgres/postgres-transaction.d.ts
|
|
271
271
|
/**
|
|
272
|
-
* One Drizzle transaction on
|
|
273
|
-
*
|
|
272
|
+
* One Drizzle transaction on the invocation's pool: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.
|
|
273
|
+
* Drizzle checks out the pool's one connection for the call; a second call waits until it is released.
|
|
274
274
|
*/
|
|
275
275
|
export declare class PostgresTransaction<Schema extends DrizzleSchema> implements ITransaction<PostgresTx<Schema>> {
|
|
276
276
|
private readonly db;
|
|
277
|
-
private active;
|
|
278
277
|
constructor(db: PostgresDb<Schema>);
|
|
279
278
|
transaction<T>(work: (tx: PostgresTx<Schema>) => Promise<T>): Promise<T>;
|
|
280
279
|
}
|
|
281
280
|
//#endregion
|
|
282
281
|
//#region src/infrastructure/postgres/postgres-client.d.ts
|
|
283
282
|
/**
|
|
284
|
-
* Open one
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* Requires the `pg` package.
|
|
283
|
+
* Open the invocation's database: one `pg` Pool with `max: 1` through Hyperdrive.
|
|
284
|
+
* Call it inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
|
|
285
|
+
* `db` is ready at once; the first query connects. A transaction checks out the connection, and other statements wait.
|
|
286
|
+
* A fetch, queue, cron, or Workflow step does not call `close()`: Hyperdrive drops the socket when the invocation ends.
|
|
287
|
+
* A Durable Object calls `close()` at the end of each method or alarm, after that call's background work.
|
|
288
|
+
* Pass `tracing` to record one span per statement. The statement text keeps placeholders; parameter values stay off.
|
|
291
289
|
*/
|
|
292
|
-
export declare function connectPostgres<Schema extends DrizzleSchema>(connectionString: string, schema: Schema, tracing?: Tracing):
|
|
293
|
-
|
|
294
|
-
|
|
290
|
+
export declare function connectPostgres<Schema extends DrizzleSchema>(connectionString: string, schema: Schema, tracing?: Tracing): {
|
|
291
|
+
db: import("drizzle-orm/node-postgres").NodePgDatabase<Schema> & {
|
|
292
|
+
$client: Pool;
|
|
295
293
|
};
|
|
296
|
-
|
|
294
|
+
/** Ends the pool after the checked-out client is released. A second call returns the same promise. */
|
|
295
|
+
close: () => Promise<void>;
|
|
296
|
+
};
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/infrastructure/postgres/statement-outside-transaction.error.d.ts
|
|
299
|
+
/**
|
|
300
|
+
* A pool statement started while this async path is inside a unit of work.
|
|
301
|
+
* The pool's one connection is already checked out, so the statement would wait forever.
|
|
302
|
+
*/
|
|
303
|
+
export declare class StatementOutsideTransactionError extends AppError {
|
|
304
|
+
readonly _tag = "StatementOutsideTransactionError";
|
|
305
|
+
readonly classification = "terminal";
|
|
306
|
+
constructor();
|
|
307
|
+
}
|
|
297
308
|
//#endregion
|
|
298
309
|
//#region src/infrastructure/postgres/postgres-database-execution.d.ts
|
|
299
310
|
/**
|
package/dist/postgres.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.d.ts","names":[],"sources":["../src/infrastructure/postgres/postgres-database-types.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-transaction.ts","../src/infrastructure/postgres/postgres-client.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/classify-postgres-error.ts","../src/infrastructure/postgres/postgres-outbox-repository.ts"],"mappings":";;;;;;;;KAOY,qBAAqB;;KAGrB,WAAW,eAAe,iBAAiB,eAAe;;KAG1D,eAAe,eAAe,iBAAiB,KACzD,WAAW;;KAKD,sBAAsB,eAAe,iBAAiB,kBAChE,QACA,2BAA2B;;KAIjB,WAAW,eAAe;WAC3B,IAAI,sBAAsB;;;;;UClBpB;EACf,iBAAiB,cAAc;EAC/B;;;8BAIoB,uBAAuB,eAAe;qBAC3B,IAAI,sBAAsB;EAAzD,YAA+B,IAAI,sBAAsB;;YAG/C,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;qBCfzB,qBAAqB,eAAe,0BAA0B;mBAC5C;EAA7B,YAA6B,IAAI,sBAAsB;EAEjD,MAAM,WAAW,YAAY;EAU7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCf9C,iDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAMA,6CAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","names":[],"sources":["../src/infrastructure/postgres/postgres-database-types.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-transaction.ts","../src/infrastructure/postgres/postgres-client.ts","../src/infrastructure/postgres/statement-outside-transaction.error.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/classify-postgres-error.ts","../src/infrastructure/postgres/postgres-outbox-repository.ts"],"mappings":";;;;;;;;KAOY,qBAAqB;;KAGrB,WAAW,eAAe,iBAAiB,eAAe;;KAG1D,eAAe,eAAe,iBAAiB,KACzD,WAAW;;KAKD,sBAAsB,eAAe,iBAAiB,kBAChE,QACA,2BAA2B;;KAIjB,WAAW,eAAe;WAC3B,IAAI,sBAAsB;;;;;UClBpB;EACf,iBAAiB,cAAc;EAC/B;;;8BAIoB,uBAAuB,eAAe;qBAC3B,IAAI,sBAAsB;EAAzD,YAA+B,IAAI,sBAAsB;;YAG/C,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;qBCfzB,qBAAqB,eAAe,0BAA0B;mBAC5C;EAA7B,YAA6B,IAAI,sBAAsB;EAEjD,MAAM,WAAW,YAAY;EAU7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCf9C,iDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAMA,6CAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCAb,oBAAoB,eAAe,0BAA0B,aACxE,WAAW;mBAEkB;EAA7B,YAA6B,IAAI,WAAW;EAE5C,YAAY,GAAG,OAAO,IAAI,WAAW,YAAY,QAAQ,KAAK,QAAQ;;;;;;;;;;;;wBCFxD,gBAAgB,eAAe,eAC7C,0BACA,QAAQ,QACR,UAAU;EAqBR,wCAAE,eAAA;;;;EAEF,aAAK;;;;;;;;qBCnCI,yCAAyC;WAC3C;WACA;EAET;;;;;;;;;;wBCMoB,gBAAgB,GACpC,mBACA,YAAY,YAAY,IACxB;WAAoB;IACnB,QAAQ;;;;wBCkBK,sBACd,gBACA,oBACC;;;;;;;qBCvBU,oCAAoC;mBAI5B;mBAEA;mBALF;EAEjB,YACmB,IAAI,WAAW,gBAChC,UAAS,yBACQ,YAAW;;EAMxB,aAAa,oBAAoB,QAAQ;;EAoDzC,cAAc,OAAO,aAAa,UAAU,cAAc,iBAAiB;;EAmB3E,aAAa,OAAO,aAAa,UAAU,cAAc,iBAAiB;;EAwB1E,WACJ,OAAO,aACP,UAAU,cAAc,gBACxB,QAAQ,kBACR,uBACC;;EAwBG,sBAAsB,QAAQ,MAAM,oBAAoB;;EAqBxD,mBAAmB,QAAQ,MAAM,oBAAoB;;EAqBrD,qBAAqB,QAAQ,MAAM,oBAAoB"}
|
package/dist/postgres.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { O as DatabaseError, nt as AppError, s as exponentialDelayCapMs } from "./core-BiN6S_cm.js";
|
|
2
|
-
import { o as MessageAlreadyProcessedError
|
|
2
|
+
import { o as MessageAlreadyProcessedError } from "./application-B5hnNUdI.js";
|
|
3
3
|
import { i as runWithUnitOfWorkContext, r as eventCollector, t as OUTBOX_REPOSITORY_DEFAULTS } from "./outbox-repository-options-BNpCWTht.js";
|
|
4
4
|
import { Result } from "better-result";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
6
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
6
7
|
import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
7
8
|
import { index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
8
9
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
9
|
-
import {
|
|
10
|
+
import { Pool } from "pg";
|
|
10
11
|
|
|
11
12
|
//#region src/infrastructure/postgres/classify-postgres-error.ts
|
|
12
13
|
const TRANSIENT_CODES = /* @__PURE__ */ new Set([
|
|
@@ -113,7 +114,7 @@ const RETRYABLE_ON_SESSION = /* @__PURE__ */ new Set([
|
|
|
113
114
|
async function executeDatabase(operation, work, options = {}) {
|
|
114
115
|
const result = await Result.tryPromise({
|
|
115
116
|
try: async () => await work(),
|
|
116
|
-
catch: (cause) => classifyPostgresError(cause, operation) ?? cause
|
|
117
|
+
catch: (cause) => classifyPostgresError(cause, operation) ?? appErrorUnderDrizzle(cause) ?? cause
|
|
117
118
|
}, { retry: {
|
|
118
119
|
times: options.retry === "transient" ? 2 : 0,
|
|
119
120
|
delayMs: 250,
|
|
@@ -124,6 +125,9 @@ async function executeDatabase(operation, work, options = {}) {
|
|
|
124
125
|
if (result.isErr()) throw result.error;
|
|
125
126
|
return result.value;
|
|
126
127
|
}
|
|
128
|
+
function appErrorUnderDrizzle(cause) {
|
|
129
|
+
if (cause instanceof DrizzleQueryError && cause.cause instanceof AppError) return cause.cause;
|
|
130
|
+
}
|
|
127
131
|
|
|
128
132
|
//#endregion
|
|
129
133
|
//#region src/infrastructure/postgres/base-postgres-repository.ts
|
|
@@ -187,69 +191,89 @@ var PostgresMessageStore = class {
|
|
|
187
191
|
}
|
|
188
192
|
};
|
|
189
193
|
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/infrastructure/postgres/connection-scope.ts
|
|
196
|
+
const scope = new AsyncLocalStorage();
|
|
197
|
+
/** Run a transaction on the path that holds the pool's one connection. */
|
|
198
|
+
function holdConnection(work) {
|
|
199
|
+
return scope.run(true, work);
|
|
200
|
+
}
|
|
201
|
+
/** True inside a transaction: a pool statement here would wait for its own connection forever. */
|
|
202
|
+
function holdsConnection() {
|
|
203
|
+
return scope.getStore() === true;
|
|
204
|
+
}
|
|
205
|
+
|
|
190
206
|
//#endregion
|
|
191
207
|
//#region src/infrastructure/postgres/postgres-transaction.ts
|
|
192
208
|
/**
|
|
193
|
-
* One Drizzle transaction on
|
|
194
|
-
*
|
|
209
|
+
* One Drizzle transaction on the invocation's pool: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.
|
|
210
|
+
* Drizzle checks out the pool's one connection for the call; a second call waits until it is released.
|
|
195
211
|
*/
|
|
196
212
|
var PostgresTransaction = class {
|
|
197
213
|
db;
|
|
198
|
-
active = false;
|
|
199
214
|
constructor(db) {
|
|
200
215
|
this.db = db;
|
|
201
216
|
}
|
|
202
217
|
transaction(work) {
|
|
203
|
-
return runWithUnitOfWorkContext("postgres",
|
|
204
|
-
if (this.active) throw new UnitOfWorkAlreadyActiveError();
|
|
205
|
-
this.active = true;
|
|
206
|
-
try {
|
|
207
|
-
return await executeDatabase("unit of work transaction", () => this.db.transaction((db) => work({ db })));
|
|
208
|
-
} finally {
|
|
209
|
-
this.active = false;
|
|
210
|
-
}
|
|
211
|
-
});
|
|
218
|
+
return runWithUnitOfWorkContext("postgres", () => executeDatabase("unit of work transaction", () => holdConnection(() => this.db.transaction((db) => work({ db })))));
|
|
212
219
|
}
|
|
213
220
|
};
|
|
214
221
|
|
|
215
222
|
//#endregion
|
|
216
|
-
//#region src/infrastructure/postgres/
|
|
223
|
+
//#region src/infrastructure/postgres/statement-outside-transaction.error.ts
|
|
217
224
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* Create the session inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
|
|
221
|
-
* The caller does not call `client.end()` and does not reuse the client.
|
|
222
|
-
* Pass `tracing` to record one span per connect and per statement. The statement text keeps placeholders.
|
|
223
|
-
* A value that was written into the SQL text, rather than passed as a parameter, is part of that text.
|
|
224
|
-
* Requires the `pg` package.
|
|
225
|
+
* A pool statement started while this async path is inside a unit of work.
|
|
226
|
+
* The pool's one connection is already checked out, so the statement would wait forever.
|
|
225
227
|
*/
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await connect(client, tracing);
|
|
232
|
-
} catch (cause) {
|
|
233
|
-
throw classifyPostgresError(cause, "connect") ?? connectFailure(cause);
|
|
228
|
+
var StatementOutsideTransactionError = class extends AppError {
|
|
229
|
+
_tag = "StatementOutsideTransactionError";
|
|
230
|
+
classification = "terminal";
|
|
231
|
+
constructor() {
|
|
232
|
+
super("a statement outside the transaction cannot run while a unit of work is open");
|
|
234
233
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/infrastructure/postgres/postgres-client.ts
|
|
238
|
+
/**
|
|
239
|
+
* Open the invocation's database: one `pg` Pool with `max: 1` through Hyperdrive.
|
|
240
|
+
* Call it inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
|
|
241
|
+
* `db` is ready at once; the first query connects. A transaction checks out the connection, and other statements wait.
|
|
242
|
+
* A fetch, queue, cron, or Workflow step does not call `close()`: Hyperdrive drops the socket when the invocation ends.
|
|
243
|
+
* A Durable Object calls `close()` at the end of each method or alarm, after that call's background work.
|
|
244
|
+
* Pass `tracing` to record one span per statement. The statement text keeps placeholders; parameter values stay off.
|
|
245
|
+
*/
|
|
246
|
+
function connectPostgres(connectionString, schema, tracing) {
|
|
247
|
+
const pool = new Pool({
|
|
248
|
+
connectionString,
|
|
249
|
+
max: 1
|
|
245
250
|
});
|
|
251
|
+
pool.on("error", () => {});
|
|
252
|
+
pool.on("connect", (client) => {
|
|
253
|
+
client.on("error", () => {});
|
|
254
|
+
if (tracing === void 0) return;
|
|
255
|
+
client.query = traced(client.query.bind(client), tracing);
|
|
256
|
+
});
|
|
257
|
+
const poolQuery = pool.query.bind(pool);
|
|
258
|
+
const query = tracing ? traced(poolQuery, tracing) : poolQuery;
|
|
259
|
+
const guarded = (...args) => holdsConnection() ? Promise.reject(new StatementOutsideTransactionError()) : query(...args);
|
|
260
|
+
pool.query = guarded;
|
|
261
|
+
let ending;
|
|
262
|
+
return {
|
|
263
|
+
db: drizzle(pool, { schema }),
|
|
264
|
+
/** Ends the pool after the checked-out client is released. A second call returns the same promise. */
|
|
265
|
+
close: () => {
|
|
266
|
+
if (holdsConnection()) return Promise.reject(new StatementOutsideTransactionError());
|
|
267
|
+
return ending ??= pool.end();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
246
270
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
/** Spans promise-form calls. A callback-form call is the pool's own checkout, already spanned by `pool.query`. */
|
|
272
|
+
function traced(query, tracing) {
|
|
273
|
+
return (...args) => {
|
|
250
274
|
const text = statementText(args);
|
|
251
|
-
if (text === void 0) return
|
|
252
|
-
const operation =
|
|
275
|
+
if (text === void 0 || typeof args.at(-1) === "function") return query(...args);
|
|
276
|
+
const operation = /^\s*([A-Za-z]+)/.exec(text)?.[1]?.toUpperCase() ?? "QUERY";
|
|
253
277
|
return tracing.enterSpan(operation, async (span) => {
|
|
254
278
|
span.setAttributes({
|
|
255
279
|
"db.system.name": "postgresql",
|
|
@@ -257,36 +281,17 @@ function traceQueries(client, tracing) {
|
|
|
257
281
|
"db.operation.name": operation,
|
|
258
282
|
"db.query.text": text
|
|
259
283
|
});
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
262
|
-
const result = await pending;
|
|
263
|
-
if ("rows" in result && Array.isArray(result.rows)) span.setAttributes({ "db.response.returned_rows": result.rows.length });
|
|
284
|
+
const result = await query(...args);
|
|
285
|
+
if (result?.rows) span.setAttributes({ "db.response.returned_rows": result.rows.length });
|
|
264
286
|
return result;
|
|
265
287
|
});
|
|
266
288
|
};
|
|
267
|
-
client.query = traced;
|
|
268
|
-
}
|
|
269
|
-
function operationOf(text) {
|
|
270
|
-
return /^\s*([A-Za-z]+)/.exec(text)?.[1]?.toUpperCase() ?? "QUERY";
|
|
271
|
-
}
|
|
272
|
-
function callQuery(query, args) {
|
|
273
|
-
return query(...args);
|
|
274
289
|
}
|
|
275
290
|
function statementText(args) {
|
|
276
|
-
if (typeof args[args.length - 1] === "function") return void 0;
|
|
277
291
|
const first = args[0];
|
|
278
|
-
if (typeof first === "object" && first !== null && "submit" in first && typeof first.submit === "function") return;
|
|
279
292
|
if (typeof first === "string") return first;
|
|
280
|
-
if (typeof first
|
|
281
|
-
|
|
282
|
-
function connectFailure(cause) {
|
|
283
|
-
return new DatabaseError("connect", "transient", {
|
|
284
|
-
database: "postgres",
|
|
285
|
-
code: void 0,
|
|
286
|
-
table_name: void 0,
|
|
287
|
-
column_name: void 0,
|
|
288
|
-
constraint_name: void 0
|
|
289
|
-
}, { cause });
|
|
293
|
+
if (typeof first !== "object" || first === null || "submit" in first) return void 0;
|
|
294
|
+
return "text" in first && typeof first.text === "string" ? first.text : void 0;
|
|
290
295
|
}
|
|
291
296
|
|
|
292
297
|
//#endregion
|
|
@@ -377,5 +382,5 @@ var PostgresOutboxRepository = class {
|
|
|
377
382
|
};
|
|
378
383
|
|
|
379
384
|
//#endregion
|
|
380
|
-
export { BasePostgresRepository, DatabaseError, PostgresMessageStore, PostgresOutboxRepository, PostgresTransaction, classifyPostgresError, connectPostgres, eventCollector, executeDatabase, messageOutbox, processedMessages };
|
|
385
|
+
export { BasePostgresRepository, DatabaseError, PostgresMessageStore, PostgresOutboxRepository, PostgresTransaction, StatementOutsideTransactionError, classifyPostgresError, connectPostgres, eventCollector, executeDatabase, messageOutbox, processedMessages };
|
|
381
386
|
//# sourceMappingURL=postgres.js.map
|
package/dist/postgres.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.js","names":["text"],"sources":["../src/infrastructure/postgres/classify-postgres-error.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/postgres-transaction.ts","../src/infrastructure/postgres/postgres-client.ts","../src/infrastructure/postgres/postgres-outbox-repository.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type FailureClassification } from '@/core'\n\nconst TRANSIENT_CODES = new Set([\n '40001',\n '40P01',\n '58000',\n '08000',\n '08001',\n '08003',\n '08006',\n '57P01',\n '57P02',\n '57P03',\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'EAI_AGAIN',\n])\nconst TERMINAL_CODES = new Set(['57014', 'CONNECTION_ENDED', 'CONNECTION_DESTROYED', 'ECANCELED'])\nconst DRIVER_CODES = new Set([\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'CONNECTION_ENDED',\n 'CONNECTION_DESTROYED',\n])\nconst TERMINAL_CLASSES = new Set(['0A', '22', '23', '25', '28', '3D', '3F', '42', '54'])\nconst DEAD_SOCKET_MESSAGES = new Set([\n 'Connection terminated unexpectedly',\n 'Client has encountered a connection error and is not queryable',\n])\n\n/* oxlint-disable anti-slop/no-runtime-typeof -- Raw driver exceptions have no shared schema; this boundary validates the fields it exposes. */\n/** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */\nexport function classifyPostgresError(\n cause: unknown,\n operation: string,\n): DatabaseError | undefined {\n let error = cause\n const seen = new Set<unknown>()\n while (error instanceof DrizzleQueryError && !seen.has(error)) {\n seen.add(error)\n error = error.cause\n }\n if (error instanceof AppError || (error instanceof Error && error.name === 'AbortError'))\n return undefined\n const code =\n error instanceof Error && 'code' in error && typeof error.code === 'string'\n ? error.code\n : undefined\n const serverError =\n error instanceof Error &&\n (error.name === 'PostgresError' || error.name === 'error') &&\n code !== undefined &&\n /^[0-9A-Z]{5}$/.test(code)\n const driverError = code !== undefined && DRIVER_CODES.has(code)\n const deadSocket = error instanceof Error && DEAD_SOCKET_MESSAGES.has(error.message)\n if (!(cause instanceof DrizzleQueryError) && !serverError && !driverError) return undefined\n\n return new DatabaseError(\n operation,\n classificationOf(code, deadSocket),\n {\n database: 'postgres',\n code,\n table_name: error instanceof Error ? text(error, 'table', 'table_name') : undefined,\n column_name: error instanceof Error ? text(error, 'column', 'column_name') : undefined,\n constraint_name:\n error instanceof Error ? text(error, 'constraint', 'constraint_name') : undefined,\n },\n { cause },\n )\n}\n\nfunction text(error: Error, pgName: string, jsName: string): string | undefined {\n return read(error, pgName) ?? read(error, jsName)\n}\n\nfunction read(error: Error, name: string): string | undefined {\n switch (name) {\n case 'table':\n return 'table' in error && typeof error.table === 'string' ? error.table : undefined\n case 'column':\n return 'column' in error && typeof error.column === 'string' ? error.column : undefined\n case 'constraint':\n return 'constraint' in error && typeof error.constraint === 'string'\n ? error.constraint\n : undefined\n case 'table_name':\n return 'table_name' in error && typeof error.table_name === 'string'\n ? error.table_name\n : undefined\n case 'column_name':\n return 'column_name' in error && typeof error.column_name === 'string'\n ? error.column_name\n : undefined\n case 'constraint_name':\n return 'constraint_name' in error && typeof error.constraint_name === 'string'\n ? error.constraint_name\n : undefined\n default:\n return undefined\n }\n}\n\n/* oxlint-enable anti-slop/no-runtime-typeof */\n\nfunction classificationOf(code: string | undefined, deadSocket: boolean): FailureClassification {\n if (deadSocket) return 'transient'\n if (code === undefined) return 'unknown'\n if (TRANSIENT_CODES.has(code)) return 'transient'\n if (TERMINAL_CODES.has(code) || TERMINAL_CLASSES.has(code.slice(0, 2))) return 'terminal'\n return 'unknown'\n}\n","import { Result } from 'better-result'\n\nimport { DatabaseError } from '@/core'\n\nimport { classifyPostgresError } from './classify-postgres-error.ts'\n\n/** SQLSTATE values that leave the pg socket usable, so the same client can run the statement again. */\nconst RETRYABLE_ON_SESSION = new Set(['40001', '40P01', '58000'])\n\n/**\n * Convert database failures and preserve other exceptions. The default executes once.\n * Opt-in retries allow three attempts for serialization failure, deadlock, and a Hyperdrive pool acquire.\n * A dropped socket stays transient and is not retried here: pg cannot reconnect the client.\n * Delays use full jitter with backoff caps of 250 ms and 500 ms.\n */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n options: { readonly retry?: 'never' | 'transient' } = {},\n): Promise<T> {\n const result = await Result.tryPromise(\n {\n try: async () => await work(),\n catch: (cause) => classifyPostgresError(cause, operation) ?? cause,\n },\n {\n retry: {\n times: options.retry === 'transient' ? 2 : 0,\n delayMs: 250,\n backoff: 'exponential',\n jitter: true,\n shouldRetry: (error) =>\n error instanceof DatabaseError &&\n error.classification === 'transient' &&\n error.details.database === 'postgres' &&\n error.details.code !== undefined &&\n RETRYABLE_ON_SESSION.has(error.details.code),\n },\n },\n )\n if (result.isErr()) throw result.error\n return result.value\n}\n","import type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/** Shared plumbing for a repository that writes inside a Postgres unit of work. */\nexport abstract class BasePostgresRepository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: PostgresDbTransaction<Schema>) {}\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { isNull } from 'drizzle-orm'\nimport { index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = pgTable('processed_messages', {\n messageId: text('message_id').primaryKey(),\n processedAt: timestamp('processed_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = pgTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: jsonb('payload').$type<DomainEvent>().notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n availableAt: timestamp('available_at', { withTimezone: true }).notNull().defaultNow(),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: timestamp('claim_expires_at', { withTimezone: true }),\n publishedAt: timestamp('published_at', { withTimezone: true }),\n failedAt: timestamp('failed_at', { withTimezone: true }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(isNull(table.publishedAt)),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\nimport { messageOutbox, processedMessages } from './postgres-schema.ts'\n\n/** Processed messages and outbox events share the active Postgres transaction; the insert result establishes the claim. */\nexport class PostgresMessageStore<Schema extends DrizzleSchema> implements IMessageStore {\n constructor(private readonly db: PostgresDbTransaction<Schema>) {}\n\n async claim(messageId: MessageId): Promise<void> {\n const claimed = await this.db\n .insert(processedMessages)\n .values({ messageId })\n .onConflictDoNothing()\n .returning({ messageId: processedMessages.messageId })\n // Thrown inside the transaction, so drizzle rolls back everything written before the claim.\n if (claimed.length === 0) throw new MessageAlreadyProcessedError(messageId)\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n await this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event })))\n }\n}\n","import { UnitOfWorkAlreadyActiveError, type ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDb, PostgresTx } from './postgres-database-types.ts'\n\n/**\n * One Drizzle transaction on one pg client: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.\n * A second call while this one is open throws. Overlap needs a second `connectPostgres`.\n */\nexport class PostgresTransaction<Schema extends DrizzleSchema> implements ITransaction<\n PostgresTx<Schema>\n> {\n private active = false\n\n constructor(private readonly db: PostgresDb<Schema>) {}\n\n transaction<T>(work: (tx: PostgresTx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('postgres', async () => {\n if (this.active) throw new UnitOfWorkAlreadyActiveError()\n this.active = true\n try {\n return await executeDatabase('unit of work transaction', () =>\n this.db.transaction((db) => work({ db })),\n )\n } finally {\n this.active = false\n }\n })\n }\n}\n","import { drizzle } from 'drizzle-orm/node-postgres'\nimport { Client } from 'pg'\n\nimport { DatabaseError } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { classifyPostgresError } from './classify-postgres-error.ts'\nimport type { PostgresConnection, PostgresDb } from './postgres-database-types.ts'\n\n/**\n * Open one Hyperdrive session for this invocation.\n * `connectionString` is the Hyperdrive binding's `connectionString`.\n * Create the session inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.\n * The caller does not call `client.end()` and does not reuse the client.\n * Pass `tracing` to record one span per connect and per statement. The statement text keeps placeholders.\n * A value that was written into the SQL text, rather than passed as a parameter, is part of that text.\n * Requires the `pg` package.\n */\nexport async function connectPostgres<Schema extends DrizzleSchema>(\n connectionString: string,\n schema: Schema,\n tracing?: Tracing,\n): Promise<{ readonly db: PostgresDb<Schema> & { readonly $client: PostgresConnection } }> {\n const client = new Client({ connectionString })\n if (tracing !== undefined) traceQueries(client, tracing)\n // An idle socket can emit `error` after connect with no query waiting. The next query reports it.\n client.on('error', () => {})\n try {\n await connect(client, tracing)\n } catch (cause) {\n throw classifyPostgresError(cause, 'connect') ?? connectFailure(cause)\n }\n return { db: drizzle(client, { schema }) }\n}\n\nfunction connect(client: Client, tracing: Tracing | undefined): Promise<void> {\n if (tracing === undefined) return client.connect().then(() => undefined)\n return tracing.enterSpan('CONNECT', (span) => {\n span.setAttributes({ 'db.system.name': 'postgresql', 'db.operation.name': 'CONNECT' })\n return client.connect().then(() => undefined)\n })\n}\n\nfunction traceQueries(client: Client, tracing: Tracing): void {\n const query = client.query.bind(client)\n const traced = (...args: readonly unknown[]) => {\n const text = statementText(args)\n if (text === undefined) return callQuery(query, args)\n const operation = operationOf(text)\n return tracing.enterSpan(operation, async (span) => {\n span.setAttributes({\n 'db.system.name': 'postgresql',\n 'db.query.summary': operation,\n 'db.operation.name': operation,\n 'db.query.text': text,\n })\n const pending = callQuery(query, args)\n if (!(pending instanceof Promise)) return pending\n const result = await pending\n if ('rows' in result && Array.isArray(result.rows)) {\n span.setAttributes({ 'db.response.returned_rows': result.rows.length })\n }\n return result\n })\n }\n // SAFETY: callback and Submittable calls are forwarded. Drizzle uses the promise form, which is wrapped.\n client.query = traced as Client['query']\n}\n\nfunction operationOf(text: string): string {\n const match = /^\\s*([A-Za-z]+)/.exec(text)\n return match?.[1]?.toUpperCase() ?? 'QUERY'\n}\n\ntype QueryRows = { readonly rows: readonly unknown[] }\n\nfunction callQuery(\n query: Client['query'],\n args: readonly unknown[],\n): Promise<QueryRows> | QueryRows | object {\n // SAFETY: these are the arguments passed to this client's query method.\n return (query as (...args: readonly unknown[]) => Promise<QueryRows> | QueryRows | object)(\n ...args,\n )\n}\n\n/* oxlint-disable anti-slop/no-runtime-typeof -- pg query accepts a string, a config object, a callback, or a Submittable. */\nfunction statementText(args: readonly unknown[]): string | undefined {\n const last = args[args.length - 1]\n if (typeof last === 'function') return undefined\n const first = args[0]\n if (\n typeof first === 'object' &&\n first !== null &&\n 'submit' in first &&\n typeof first.submit === 'function'\n ) {\n return undefined\n }\n if (typeof first === 'string') return first\n if (\n typeof first === 'object' &&\n first !== null &&\n 'text' in first &&\n typeof first.text === 'string'\n )\n return first.text\n return undefined\n}\n/* oxlint-enable anti-slop/no-runtime-typeof */\n\nfunction connectFailure(cause: unknown): DatabaseError {\n return new DatabaseError(\n 'connect',\n 'transient',\n {\n database: 'postgres',\n code: undefined,\n table_name: undefined,\n column_name: undefined,\n constraint_name: undefined,\n },\n { cause },\n )\n}\n","import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from 'drizzle-orm'\n\nimport type { DeadLetterReason, IOutboxRepository, OutboxClaim, OutboxMessage } from '@/application'\nimport { exponentialDelayCapMs } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport {\n OUTBOX_REPOSITORY_DEFAULTS,\n type OutboxRepositoryOptions,\n} from '../outbox-repository-options.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDb } from './postgres-database-types.ts'\nimport { messageOutbox, processedMessages } from './postgres-schema.ts'\n\n/**\n * Typist's lease protocol with riposte's SKIP LOCKED selection on a plain connection, outside a unit of work.\n * Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.\n */\nexport class PostgresOutboxRepository implements IOutboxRepository {\n private readonly options: Required<OutboxRepositoryOptions>\n\n constructor(\n private readonly db: PostgresDb<DrizzleSchema>,\n options: OutboxRepositoryOptions = {},\n private readonly now: () => Date = () => new Date(),\n ) {\n this.options = { ...OUTBOX_REPOSITORY_DEFAULTS, ...options }\n }\n\n /** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */\n async claimPending(batchSize: number): Promise<OutboxClaim> {\n const token = crypto.randomUUID()\n const now = this.now()\n const rows = await executeDatabase('claim pending outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n claimToken: token,\n claimExpiresAt: new Date(now.getTime() + this.options.leaseMs),\n attempts: sql`${messageOutbox.attempts} + 1`,\n })\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(\n and(\n isNull(messageOutbox.publishedAt),\n isNull(messageOutbox.failedAt),\n lte(messageOutbox.availableAt, now),\n or(isNull(messageOutbox.claimExpiresAt), lte(messageOutbox.claimExpiresAt, now)),\n ),\n )\n .orderBy(\n asc(messageOutbox.availableAt),\n asc(messageOutbox.createdAt),\n asc(messageOutbox.id),\n )\n .limit(batchSize)\n .for('update', { skipLocked: true }),\n ),\n )\n .returning({\n payload: messageOutbox.payload,\n attempts: messageOutbox.attempts,\n availableAt: messageOutbox.availableAt,\n createdAt: messageOutbox.createdAt,\n }),\n )\n // UPDATE RETURNING does not preserve the selection order.\n rows.sort(\n (a, b) =>\n a.availableAt.getTime() - b.availableAt.getTime() ||\n a.createdAt.getTime() - b.createdAt.getTime() ||\n a.payload.id.localeCompare(b.payload.id),\n )\n return { token, messages: rows.map(({ payload, attempts }) => ({ payload, attempts })) }\n }\n\n /** Mark only supplied rows that still hold this token as published. */\n async markPublished(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('mark outbox messages published', () =>\n this.db\n .update(messageOutbox)\n .set({ publishedAt: this.now(), claimToken: null, claimExpiresAt: null })\n .where(\n and(\n inArray(\n messageOutbox.id,\n messages.map((row) => row.payload.id),\n ),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */\n async releaseClaim(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n const now = this.now()\n for (const attempts of new Set(messages.map((row) => row.attempts))) {\n const ids = messages.flatMap((row) => (row.attempts === attempts ? [row.payload.id] : []))\n const delay = exponentialDelayCapMs(\n this.options.retryBaseDelayMs,\n this.options.retryMaxDelayMs,\n attempts,\n )\n // oxlint-disable-next-line no-await-in-loop -- Stop releasing rows on the first database failure.\n await executeDatabase('release outbox claim', () =>\n this.db\n .update(messageOutbox)\n .set({\n availableAt: new Date(now.getTime() + delay),\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(and(inArray(messageOutbox.id, ids), eq(messageOutbox.claimToken, claim.token))),\n )\n }\n }\n\n /** Retain the full payload and record a closed reason with a readable error message. */\n async deadLetter(\n claim: OutboxClaim,\n messages: ReadonlyArray<OutboxMessage>,\n reason: DeadLetterReason,\n errorMessage: string,\n ): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('dead-letter outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n failedAt: this.now(),\n lastError: `${reason}: ${errorMessage}`,\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(\n and(\n inArray(\n messageOutbox.id,\n messages.map((row) => row.payload.id),\n ),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Delete at most batchSize published rows, strictly before the cutoff. */\n async deletePublishedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete published outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(lt(messageOutbox.publishedAt, cutoff))\n .orderBy(asc(messageOutbox.publishedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize failed rows; pending and published rows remain untouched. */\n async deleteFailedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete failed outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(and(isNull(messageOutbox.publishedAt), lt(messageOutbox.failedAt, cutoff)))\n .orderBy(asc(messageOutbox.failedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize receipts, strictly before the cutoff. */\n async deleteReceiptsBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete message receipts', () =>\n this.db\n .delete(processedMessages)\n .where(\n inArray(\n processedMessages.messageId,\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(lt(processedMessages.processedAt, cutoff))\n .orderBy(asc(processedMessages.processedAt), asc(processedMessages.messageId))\n .limit(batchSize),\n ),\n )\n .returning({ messageId: processedMessages.messageId }),\n )\n return rows.length\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"postgres.js","names":["text"],"sources":["../src/infrastructure/postgres/classify-postgres-error.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/connection-scope.ts","../src/infrastructure/postgres/postgres-transaction.ts","../src/infrastructure/postgres/statement-outside-transaction.error.ts","../src/infrastructure/postgres/postgres-client.ts","../src/infrastructure/postgres/postgres-outbox-repository.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type FailureClassification } from '@/core'\n\nconst TRANSIENT_CODES = new Set([\n '40001',\n '40P01',\n '58000',\n '08000',\n '08001',\n '08003',\n '08006',\n '57P01',\n '57P02',\n '57P03',\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'EAI_AGAIN',\n])\nconst TERMINAL_CODES = new Set(['57014', 'CONNECTION_ENDED', 'CONNECTION_DESTROYED', 'ECANCELED'])\nconst DRIVER_CODES = new Set([\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'CONNECTION_ENDED',\n 'CONNECTION_DESTROYED',\n])\nconst TERMINAL_CLASSES = new Set(['0A', '22', '23', '25', '28', '3D', '3F', '42', '54'])\nconst DEAD_SOCKET_MESSAGES = new Set([\n 'Connection terminated unexpectedly',\n 'Client has encountered a connection error and is not queryable',\n])\n\n/* oxlint-disable anti-slop/no-runtime-typeof -- Raw driver exceptions have no shared schema; this boundary validates the fields it exposes. */\n/** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */\nexport function classifyPostgresError(\n cause: unknown,\n operation: string,\n): DatabaseError | undefined {\n let error = cause\n const seen = new Set<unknown>()\n while (error instanceof DrizzleQueryError && !seen.has(error)) {\n seen.add(error)\n error = error.cause\n }\n if (error instanceof AppError || (error instanceof Error && error.name === 'AbortError'))\n return undefined\n const code =\n error instanceof Error && 'code' in error && typeof error.code === 'string'\n ? error.code\n : undefined\n const serverError =\n error instanceof Error &&\n (error.name === 'PostgresError' || error.name === 'error') &&\n code !== undefined &&\n /^[0-9A-Z]{5}$/.test(code)\n const driverError = code !== undefined && DRIVER_CODES.has(code)\n const deadSocket = error instanceof Error && DEAD_SOCKET_MESSAGES.has(error.message)\n if (!(cause instanceof DrizzleQueryError) && !serverError && !driverError) return undefined\n\n return new DatabaseError(\n operation,\n classificationOf(code, deadSocket),\n {\n database: 'postgres',\n code,\n table_name: error instanceof Error ? text(error, 'table', 'table_name') : undefined,\n column_name: error instanceof Error ? text(error, 'column', 'column_name') : undefined,\n constraint_name:\n error instanceof Error ? text(error, 'constraint', 'constraint_name') : undefined,\n },\n { cause },\n )\n}\n\nfunction text(error: Error, pgName: string, jsName: string): string | undefined {\n return read(error, pgName) ?? read(error, jsName)\n}\n\nfunction read(error: Error, name: string): string | undefined {\n switch (name) {\n case 'table':\n return 'table' in error && typeof error.table === 'string' ? error.table : undefined\n case 'column':\n return 'column' in error && typeof error.column === 'string' ? error.column : undefined\n case 'constraint':\n return 'constraint' in error && typeof error.constraint === 'string'\n ? error.constraint\n : undefined\n case 'table_name':\n return 'table_name' in error && typeof error.table_name === 'string'\n ? error.table_name\n : undefined\n case 'column_name':\n return 'column_name' in error && typeof error.column_name === 'string'\n ? error.column_name\n : undefined\n case 'constraint_name':\n return 'constraint_name' in error && typeof error.constraint_name === 'string'\n ? error.constraint_name\n : undefined\n default:\n return undefined\n }\n}\n\n/* oxlint-enable anti-slop/no-runtime-typeof */\n\nfunction classificationOf(code: string | undefined, deadSocket: boolean): FailureClassification {\n if (deadSocket) return 'transient'\n if (code === undefined) return 'unknown'\n if (TRANSIENT_CODES.has(code)) return 'transient'\n if (TERMINAL_CODES.has(code) || TERMINAL_CLASSES.has(code.slice(0, 2))) return 'terminal'\n return 'unknown'\n}\n","import { Result } from 'better-result'\nimport { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError } from '@/core'\n\nimport { classifyPostgresError } from './classify-postgres-error.ts'\n\n/** SQLSTATE values that leave the pg socket usable, so the same client can run the statement again. */\nconst RETRYABLE_ON_SESSION = new Set(['40001', '40P01', '58000'])\n\n/**\n * Convert database failures and preserve other exceptions. The default executes once.\n * Opt-in retries allow three attempts for serialization failure, deadlock, and a Hyperdrive pool acquire.\n * A dropped socket stays transient and is not retried here: pg cannot reconnect the client.\n * Delays use full jitter with backoff caps of 250 ms and 500 ms.\n */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n options: { readonly retry?: 'never' | 'transient' } = {},\n): Promise<T> {\n const result = await Result.tryPromise(\n {\n try: async () => await work(),\n catch: (cause) =>\n classifyPostgresError(cause, operation) ?? appErrorUnderDrizzle(cause) ?? cause,\n },\n {\n retry: {\n times: options.retry === 'transient' ? 2 : 0,\n delayMs: 250,\n backoff: 'exponential',\n jitter: true,\n shouldRetry: (error) =>\n error instanceof DatabaseError &&\n error.classification === 'transient' &&\n error.details.database === 'postgres' &&\n error.details.code !== undefined &&\n RETRYABLE_ON_SESSION.has(error.details.code),\n },\n },\n )\n if (result.isErr()) throw result.error\n return result.value\n}\n\n// Drizzle wraps every `query` failure once; the typed error inside is the one the caller handles.\nfunction appErrorUnderDrizzle(cause: unknown): AppError | undefined {\n if (cause instanceof DrizzleQueryError && cause.cause instanceof AppError) return cause.cause\n return undefined\n}\n","import type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/** Shared plumbing for a repository that writes inside a Postgres unit of work. */\nexport abstract class BasePostgresRepository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: PostgresDbTransaction<Schema>) {}\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { isNull } from 'drizzle-orm'\nimport { index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = pgTable('processed_messages', {\n messageId: text('message_id').primaryKey(),\n processedAt: timestamp('processed_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = pgTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: jsonb('payload').$type<DomainEvent>().notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n availableAt: timestamp('available_at', { withTimezone: true }).notNull().defaultNow(),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: timestamp('claim_expires_at', { withTimezone: true }),\n publishedAt: timestamp('published_at', { withTimezone: true }),\n failedAt: timestamp('failed_at', { withTimezone: true }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(isNull(table.publishedAt)),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\nimport { messageOutbox, processedMessages } from './postgres-schema.ts'\n\n/** Processed messages and outbox events share the active Postgres transaction; the insert result establishes the claim. */\nexport class PostgresMessageStore<Schema extends DrizzleSchema> implements IMessageStore {\n constructor(private readonly db: PostgresDbTransaction<Schema>) {}\n\n async claim(messageId: MessageId): Promise<void> {\n const claimed = await this.db\n .insert(processedMessages)\n .values({ messageId })\n .onConflictDoNothing()\n .returning({ messageId: processedMessages.messageId })\n // Thrown inside the transaction, so drizzle rolls back everything written before the claim.\n if (claimed.length === 0) throw new MessageAlreadyProcessedError(messageId)\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n await this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event })))\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\n// Marks the async path that holds the invocation pool's one connection for a transaction.\nconst scope = new AsyncLocalStorage<true>()\n\n/** Run a transaction on the path that holds the pool's one connection. */\nexport function holdConnection<T>(work: () => Promise<T>): Promise<T> {\n return scope.run(true, work)\n}\n\n/** True inside a transaction: a pool statement here would wait for its own connection forever. */\nexport function holdsConnection(): boolean {\n return scope.getStore() === true\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { holdConnection } from './connection-scope.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDb, PostgresTx } from './postgres-database-types.ts'\n\n/**\n * One Drizzle transaction on the invocation's pool: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.\n * Drizzle checks out the pool's one connection for the call; a second call waits until it is released.\n */\nexport class PostgresTransaction<Schema extends DrizzleSchema> implements ITransaction<\n PostgresTx<Schema>\n> {\n constructor(private readonly db: PostgresDb<Schema>) {}\n\n transaction<T>(work: (tx: PostgresTx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('postgres', () =>\n executeDatabase('unit of work transaction', () =>\n holdConnection(() => this.db.transaction((db) => work({ db }))),\n ),\n )\n }\n}\n","import { AppError } from '@/core'\n\n/**\n * A pool statement started while this async path is inside a unit of work.\n * The pool's one connection is already checked out, so the statement would wait forever.\n */\nexport class StatementOutsideTransactionError extends AppError {\n readonly _tag = 'StatementOutsideTransactionError'\n readonly classification = 'terminal'\n\n constructor() {\n super('a statement outside the transaction cannot run while a unit of work is open')\n }\n}\n","import { drizzle } from 'drizzle-orm/node-postgres'\nimport { Pool, type QueryResult } from 'pg'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { holdsConnection } from './connection-scope.ts'\nimport { StatementOutsideTransactionError } from './statement-outside-transaction.error.ts'\n\n/**\n * Open the invocation's database: one `pg` Pool with `max: 1` through Hyperdrive.\n * Call it inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.\n * `db` is ready at once; the first query connects. A transaction checks out the connection, and other statements wait.\n * A fetch, queue, cron, or Workflow step does not call `close()`: Hyperdrive drops the socket when the invocation ends.\n * A Durable Object calls `close()` at the end of each method or alarm, after that call's background work.\n * Pass `tracing` to record one span per statement. The statement text keeps placeholders; parameter values stay off.\n */\nexport function connectPostgres<Schema extends DrizzleSchema>(\n connectionString: string,\n schema: Schema,\n tracing?: Tracing,\n) {\n const pool = new Pool({ connectionString, max: 1 })\n // The pool emits `error` when it drops a client; the waiting statement rejects with that error.\n pool.on('error', () => {})\n pool.on('connect', (client) => {\n // pg-cloudflare reports the socket that `pool.end()` closed as a client error.\n client.on('error', () => {})\n if (tracing === undefined) return\n // SAFETY: a transaction's statements run here in promise form, and `traced` forwards every overload.\n client.query = traced(client.query.bind(client) as Query, tracing) as typeof client.query\n })\n // SAFETY: `pool.query` returns a promise in promise form and nothing in callback form.\n const poolQuery = pool.query.bind(pool) as Query\n const query = tracing ? traced(poolQuery, tracing) : poolQuery\n const guarded: Query = (...args) =>\n holdsConnection() ? Promise.reject(new StatementOutsideTransactionError()) : query(...args)\n // SAFETY: `guarded` forwards every `pool.query` overload unchanged.\n pool.query = guarded as Pool['query']\n let ending: Promise<void> | undefined\n return {\n db: drizzle(pool, { schema }),\n /** Ends the pool after the checked-out client is released. A second call returns the same promise. */\n close: () => {\n if (holdsConnection()) return Promise.reject(new StatementOutsideTransactionError())\n return (ending ??= pool.end())\n },\n }\n}\n\n/** `pool.query` and `client.query`: a promise in promise form, nothing in callback form. */\ntype Query = (...args: readonly unknown[]) => Promise<QueryResult> | void\n\n/* oxlint-disable anti-slop/no-runtime-typeof -- pg query takes a string, a config object, a callback, or a Submittable. */\n/** Spans promise-form calls. A callback-form call is the pool's own checkout, already spanned by `pool.query`. */\nfunction traced(query: Query, tracing: Tracing): Query {\n return (...args) => {\n const text = statementText(args)\n if (text === undefined || typeof args.at(-1) === 'function') return query(...args)\n const operation = /^\\s*([A-Za-z]+)/.exec(text)?.[1]?.toUpperCase() ?? 'QUERY'\n return tracing.enterSpan(operation, async (span) => {\n span.setAttributes({\n 'db.system.name': 'postgresql',\n 'db.query.summary': operation,\n 'db.operation.name': operation,\n 'db.query.text': text,\n })\n const result = await query(...args)\n // A multi-statement text resolves to an array of results, which has no `rows`.\n if (result?.rows) span.setAttributes({ 'db.response.returned_rows': result.rows.length })\n // SAFETY: the promise form always resolves to a result.\n return result as QueryResult\n })\n }\n}\n\nfunction statementText(args: readonly unknown[]): string | undefined {\n const first = args[0]\n if (typeof first === 'string') return first\n if (typeof first !== 'object' || first === null || 'submit' in first) return undefined\n return 'text' in first && typeof first.text === 'string' ? first.text : undefined\n}\n/* oxlint-enable anti-slop/no-runtime-typeof */\n","import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from 'drizzle-orm'\n\nimport type { DeadLetterReason, IOutboxRepository, OutboxClaim, OutboxMessage } from '@/application'\nimport { exponentialDelayCapMs } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport {\n OUTBOX_REPOSITORY_DEFAULTS,\n type OutboxRepositoryOptions,\n} from '../outbox-repository-options.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDb } from './postgres-database-types.ts'\nimport { messageOutbox, processedMessages } from './postgres-schema.ts'\n\n/**\n * Typist's lease protocol with riposte's SKIP LOCKED selection on a plain connection, outside a unit of work.\n * Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.\n */\nexport class PostgresOutboxRepository implements IOutboxRepository {\n private readonly options: Required<OutboxRepositoryOptions>\n\n constructor(\n private readonly db: PostgresDb<DrizzleSchema>,\n options: OutboxRepositoryOptions = {},\n private readonly now: () => Date = () => new Date(),\n ) {\n this.options = { ...OUTBOX_REPOSITORY_DEFAULTS, ...options }\n }\n\n /** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */\n async claimPending(batchSize: number): Promise<OutboxClaim> {\n const token = crypto.randomUUID()\n const now = this.now()\n const rows = await executeDatabase('claim pending outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n claimToken: token,\n claimExpiresAt: new Date(now.getTime() + this.options.leaseMs),\n attempts: sql`${messageOutbox.attempts} + 1`,\n })\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(\n and(\n isNull(messageOutbox.publishedAt),\n isNull(messageOutbox.failedAt),\n lte(messageOutbox.availableAt, now),\n or(isNull(messageOutbox.claimExpiresAt), lte(messageOutbox.claimExpiresAt, now)),\n ),\n )\n .orderBy(\n asc(messageOutbox.availableAt),\n asc(messageOutbox.createdAt),\n asc(messageOutbox.id),\n )\n .limit(batchSize)\n .for('update', { skipLocked: true }),\n ),\n )\n .returning({\n payload: messageOutbox.payload,\n attempts: messageOutbox.attempts,\n availableAt: messageOutbox.availableAt,\n createdAt: messageOutbox.createdAt,\n }),\n )\n // UPDATE RETURNING does not preserve the selection order.\n rows.sort(\n (a, b) =>\n a.availableAt.getTime() - b.availableAt.getTime() ||\n a.createdAt.getTime() - b.createdAt.getTime() ||\n a.payload.id.localeCompare(b.payload.id),\n )\n return { token, messages: rows.map(({ payload, attempts }) => ({ payload, attempts })) }\n }\n\n /** Mark only supplied rows that still hold this token as published. */\n async markPublished(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('mark outbox messages published', () =>\n this.db\n .update(messageOutbox)\n .set({ publishedAt: this.now(), claimToken: null, claimExpiresAt: null })\n .where(\n and(\n inArray(\n messageOutbox.id,\n messages.map((row) => row.payload.id),\n ),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */\n async releaseClaim(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n const now = this.now()\n for (const attempts of new Set(messages.map((row) => row.attempts))) {\n const ids = messages.flatMap((row) => (row.attempts === attempts ? [row.payload.id] : []))\n const delay = exponentialDelayCapMs(\n this.options.retryBaseDelayMs,\n this.options.retryMaxDelayMs,\n attempts,\n )\n // oxlint-disable-next-line no-await-in-loop -- Stop releasing rows on the first database failure.\n await executeDatabase('release outbox claim', () =>\n this.db\n .update(messageOutbox)\n .set({\n availableAt: new Date(now.getTime() + delay),\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(and(inArray(messageOutbox.id, ids), eq(messageOutbox.claimToken, claim.token))),\n )\n }\n }\n\n /** Retain the full payload and record a closed reason with a readable error message. */\n async deadLetter(\n claim: OutboxClaim,\n messages: ReadonlyArray<OutboxMessage>,\n reason: DeadLetterReason,\n errorMessage: string,\n ): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('dead-letter outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n failedAt: this.now(),\n lastError: `${reason}: ${errorMessage}`,\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(\n and(\n inArray(\n messageOutbox.id,\n messages.map((row) => row.payload.id),\n ),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Delete at most batchSize published rows, strictly before the cutoff. */\n async deletePublishedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete published outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(lt(messageOutbox.publishedAt, cutoff))\n .orderBy(asc(messageOutbox.publishedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize failed rows; pending and published rows remain untouched. */\n async deleteFailedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete failed outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(and(isNull(messageOutbox.publishedAt), lt(messageOutbox.failedAt, cutoff)))\n .orderBy(asc(messageOutbox.failedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize receipts, strictly before the cutoff. */\n async deleteReceiptsBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete message receipts', () =>\n this.db\n .delete(processedMessages)\n .where(\n inArray(\n processedMessages.messageId,\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(lt(processedMessages.processedAt, cutoff))\n .orderBy(asc(processedMessages.processedAt), asc(processedMessages.messageId))\n .limit(batchSize),\n ),\n )\n .returning({ messageId: processedMessages.messageId }),\n )\n return rows.length\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "better-ship",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.32.0",
|
|
5
5
|
"description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
// Marks the async path that holds the invocation pool's one connection for a transaction.
|
|
4
|
+
const scope = new AsyncLocalStorage<true>()
|
|
5
|
+
|
|
6
|
+
/** Run a transaction on the path that holds the pool's one connection. */
|
|
7
|
+
export function holdConnection<T>(work: () => Promise<T>): Promise<T> {
|
|
8
|
+
return scope.run(true, work)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** True inside a transaction: a pool statement here would wait for its own connection forever. */
|
|
12
|
+
export function holdsConnection(): boolean {
|
|
13
|
+
return scope.getStore() === true
|
|
14
|
+
}
|
|
@@ -11,6 +11,7 @@ export type {
|
|
|
11
11
|
} from './postgres-database-types.ts'
|
|
12
12
|
export { DatabaseError, type DatabaseFailure } from '@/core'
|
|
13
13
|
export { connectPostgres } from './postgres-client.ts'
|
|
14
|
+
export { StatementOutsideTransactionError } from './statement-outside-transaction.error.ts'
|
|
14
15
|
export { executeDatabase } from './postgres-database-execution.ts'
|
|
15
16
|
export { classifyPostgresError } from './classify-postgres-error.ts'
|
|
16
17
|
export { eventCollector } from '../unit-of-work-context.ts'
|
|
@@ -1,52 +1,61 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/node-postgres'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { DatabaseError } from '@/core'
|
|
2
|
+
import { Pool, type QueryResult } from 'pg'
|
|
5
3
|
|
|
6
4
|
import type { DrizzleSchema } from '../drizzle-schema.ts'
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
5
|
+
import { holdsConnection } from './connection-scope.ts'
|
|
6
|
+
import { StatementOutsideTransactionError } from './statement-outside-transaction.error.ts'
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
|
-
* Open one
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Requires the `pg` package.
|
|
9
|
+
* Open the invocation's database: one `pg` Pool with `max: 1` through Hyperdrive.
|
|
10
|
+
* Call it inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
|
|
11
|
+
* `db` is ready at once; the first query connects. A transaction checks out the connection, and other statements wait.
|
|
12
|
+
* A fetch, queue, cron, or Workflow step does not call `close()`: Hyperdrive drops the socket when the invocation ends.
|
|
13
|
+
* A Durable Object calls `close()` at the end of each method or alarm, after that call's background work.
|
|
14
|
+
* Pass `tracing` to record one span per statement. The statement text keeps placeholders; parameter values stay off.
|
|
18
15
|
*/
|
|
19
|
-
export
|
|
16
|
+
export function connectPostgres<Schema extends DrizzleSchema>(
|
|
20
17
|
connectionString: string,
|
|
21
18
|
schema: Schema,
|
|
22
19
|
tracing?: Tracing,
|
|
23
|
-
)
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
) {
|
|
21
|
+
const pool = new Pool({ connectionString, max: 1 })
|
|
22
|
+
// The pool emits `error` when it drops a client; the waiting statement rejects with that error.
|
|
23
|
+
pool.on('error', () => {})
|
|
24
|
+
pool.on('connect', (client) => {
|
|
25
|
+
// pg-cloudflare reports the socket that `pool.end()` closed as a client error.
|
|
26
|
+
client.on('error', () => {})
|
|
27
|
+
if (tracing === undefined) return
|
|
28
|
+
// SAFETY: a transaction's statements run here in promise form, and `traced` forwards every overload.
|
|
29
|
+
client.query = traced(client.query.bind(client) as Query, tracing) as typeof client.query
|
|
30
|
+
})
|
|
31
|
+
// SAFETY: `pool.query` returns a promise in promise form and nothing in callback form.
|
|
32
|
+
const poolQuery = pool.query.bind(pool) as Query
|
|
33
|
+
const query = tracing ? traced(poolQuery, tracing) : poolQuery
|
|
34
|
+
const guarded: Query = (...args) =>
|
|
35
|
+
holdsConnection() ? Promise.reject(new StatementOutsideTransactionError()) : query(...args)
|
|
36
|
+
// SAFETY: `guarded` forwards every `pool.query` overload unchanged.
|
|
37
|
+
pool.query = guarded as Pool['query']
|
|
38
|
+
let ending: Promise<void> | undefined
|
|
39
|
+
return {
|
|
40
|
+
db: drizzle(pool, { schema }),
|
|
41
|
+
/** Ends the pool after the checked-out client is released. A second call returns the same promise. */
|
|
42
|
+
close: () => {
|
|
43
|
+
if (holdsConnection()) return Promise.reject(new StatementOutsideTransactionError())
|
|
44
|
+
return (ending ??= pool.end())
|
|
45
|
+
},
|
|
32
46
|
}
|
|
33
|
-
return { db: drizzle(client, { schema }) }
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return tracing.enterSpan('CONNECT', (span) => {
|
|
39
|
-
span.setAttributes({ 'db.system.name': 'postgresql', 'db.operation.name': 'CONNECT' })
|
|
40
|
-
return client.connect().then(() => undefined)
|
|
41
|
-
})
|
|
42
|
-
}
|
|
49
|
+
/** `pool.query` and `client.query`: a promise in promise form, nothing in callback form. */
|
|
50
|
+
type Query = (...args: readonly unknown[]) => Promise<QueryResult> | void
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
/* oxlint-disable anti-slop/no-runtime-typeof -- pg query takes a string, a config object, a callback, or a Submittable. */
|
|
53
|
+
/** Spans promise-form calls. A callback-form call is the pool's own checkout, already spanned by `pool.query`. */
|
|
54
|
+
function traced(query: Query, tracing: Tracing): Query {
|
|
55
|
+
return (...args) => {
|
|
47
56
|
const text = statementText(args)
|
|
48
|
-
if (text === undefined) return
|
|
49
|
-
const operation =
|
|
57
|
+
if (text === undefined || typeof args.at(-1) === 'function') return query(...args)
|
|
58
|
+
const operation = /^\s*([A-Za-z]+)/.exec(text)?.[1]?.toUpperCase() ?? 'QUERY'
|
|
50
59
|
return tracing.enterSpan(operation, async (span) => {
|
|
51
60
|
span.setAttributes({
|
|
52
61
|
'db.system.name': 'postgresql',
|
|
@@ -54,72 +63,19 @@ function traceQueries(client: Client, tracing: Tracing): void {
|
|
|
54
63
|
'db.operation.name': operation,
|
|
55
64
|
'db.query.text': text,
|
|
56
65
|
})
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
return result
|
|
66
|
+
const result = await query(...args)
|
|
67
|
+
// A multi-statement text resolves to an array of results, which has no `rows`.
|
|
68
|
+
if (result?.rows) span.setAttributes({ 'db.response.returned_rows': result.rows.length })
|
|
69
|
+
// SAFETY: the promise form always resolves to a result.
|
|
70
|
+
return result as QueryResult
|
|
64
71
|
})
|
|
65
72
|
}
|
|
66
|
-
// SAFETY: callback and Submittable calls are forwarded. Drizzle uses the promise form, which is wrapped.
|
|
67
|
-
client.query = traced as Client['query']
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function operationOf(text: string): string {
|
|
71
|
-
const match = /^\s*([A-Za-z]+)/.exec(text)
|
|
72
|
-
return match?.[1]?.toUpperCase() ?? 'QUERY'
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
type QueryRows = { readonly rows: readonly unknown[] }
|
|
76
|
-
|
|
77
|
-
function callQuery(
|
|
78
|
-
query: Client['query'],
|
|
79
|
-
args: readonly unknown[],
|
|
80
|
-
): Promise<QueryRows> | QueryRows | object {
|
|
81
|
-
// SAFETY: these are the arguments passed to this client's query method.
|
|
82
|
-
return (query as (...args: readonly unknown[]) => Promise<QueryRows> | QueryRows | object)(
|
|
83
|
-
...args,
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* oxlint-disable anti-slop/no-runtime-typeof -- pg query accepts a string, a config object, a callback, or a Submittable. */
|
|
88
75
|
function statementText(args: readonly unknown[]): string | undefined {
|
|
89
|
-
const last = args[args.length - 1]
|
|
90
|
-
if (typeof last === 'function') return undefined
|
|
91
76
|
const first = args[0]
|
|
92
|
-
if (
|
|
93
|
-
typeof first === 'object' &&
|
|
94
|
-
first !== null &&
|
|
95
|
-
'submit' in first &&
|
|
96
|
-
typeof first.submit === 'function'
|
|
97
|
-
) {
|
|
98
|
-
return undefined
|
|
99
|
-
}
|
|
100
77
|
if (typeof first === 'string') return first
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
first !== null &&
|
|
104
|
-
'text' in first &&
|
|
105
|
-
typeof first.text === 'string'
|
|
106
|
-
)
|
|
107
|
-
return first.text
|
|
108
|
-
return undefined
|
|
78
|
+
if (typeof first !== 'object' || first === null || 'submit' in first) return undefined
|
|
79
|
+
return 'text' in first && typeof first.text === 'string' ? first.text : undefined
|
|
109
80
|
}
|
|
110
81
|
/* oxlint-enable anti-slop/no-runtime-typeof */
|
|
111
|
-
|
|
112
|
-
function connectFailure(cause: unknown): DatabaseError {
|
|
113
|
-
return new DatabaseError(
|
|
114
|
-
'connect',
|
|
115
|
-
'transient',
|
|
116
|
-
{
|
|
117
|
-
database: 'postgres',
|
|
118
|
-
code: undefined,
|
|
119
|
-
table_name: undefined,
|
|
120
|
-
column_name: undefined,
|
|
121
|
-
constraint_name: undefined,
|
|
122
|
-
},
|
|
123
|
-
{ cause },
|
|
124
|
-
)
|
|
125
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Result } from 'better-result'
|
|
2
|
+
import { DrizzleQueryError } from 'drizzle-orm/errors'
|
|
2
3
|
|
|
3
|
-
import { DatabaseError } from '@/core'
|
|
4
|
+
import { AppError, DatabaseError } from '@/core'
|
|
4
5
|
|
|
5
6
|
import { classifyPostgresError } from './classify-postgres-error.ts'
|
|
6
7
|
|
|
@@ -21,7 +22,8 @@ export async function executeDatabase<T>(
|
|
|
21
22
|
const result = await Result.tryPromise(
|
|
22
23
|
{
|
|
23
24
|
try: async () => await work(),
|
|
24
|
-
catch: (cause) =>
|
|
25
|
+
catch: (cause) =>
|
|
26
|
+
classifyPostgresError(cause, operation) ?? appErrorUnderDrizzle(cause) ?? cause,
|
|
25
27
|
},
|
|
26
28
|
{
|
|
27
29
|
retry: {
|
|
@@ -41,3 +43,9 @@ export async function executeDatabase<T>(
|
|
|
41
43
|
if (result.isErr()) throw result.error
|
|
42
44
|
return result.value
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
// Drizzle wraps every `query` failure once; the typed error inside is the one the caller handles.
|
|
48
|
+
function appErrorUnderDrizzle(cause: unknown): AppError | undefined {
|
|
49
|
+
if (cause instanceof DrizzleQueryError && cause.cause instanceof AppError) return cause.cause
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ExtractTablesWithRelations } from 'drizzle-orm'
|
|
2
2
|
import type { NodePgDatabase, NodePgTransaction } from 'drizzle-orm/node-postgres'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Pool } from 'pg'
|
|
4
4
|
|
|
5
5
|
import type { DrizzleSchema } from '../drizzle-schema.ts'
|
|
6
6
|
|
|
7
|
-
/** The pg
|
|
8
|
-
export type PostgresConnection =
|
|
7
|
+
/** The pg pool passed to Drizzle. One pool, with `max: 1`, belongs to one invocation. */
|
|
8
|
+
export type PostgresConnection = Pool
|
|
9
9
|
|
|
10
10
|
/** The application's Drizzle Postgres database, preserving its schema and relational query types. */
|
|
11
11
|
export type PostgresDb<Schema extends DrizzleSchema> = NodePgDatabase<Schema>
|
|
@@ -1,32 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ITransaction } from '@/application'
|
|
2
2
|
|
|
3
3
|
import type { DrizzleSchema } from '../drizzle-schema.ts'
|
|
4
4
|
import { runWithUnitOfWorkContext } from '../unit-of-work-context.ts'
|
|
5
|
+
import { holdConnection } from './connection-scope.ts'
|
|
5
6
|
import { executeDatabase } from './postgres-database-execution.ts'
|
|
6
7
|
import type { PostgresDb, PostgresTx } from './postgres-database-types.ts'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* One Drizzle transaction on
|
|
10
|
-
*
|
|
10
|
+
* One Drizzle transaction on the invocation's pool: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.
|
|
11
|
+
* Drizzle checks out the pool's one connection for the call; a second call waits until it is released.
|
|
11
12
|
*/
|
|
12
13
|
export class PostgresTransaction<Schema extends DrizzleSchema> implements ITransaction<
|
|
13
14
|
PostgresTx<Schema>
|
|
14
15
|
> {
|
|
15
|
-
private active = false
|
|
16
|
-
|
|
17
16
|
constructor(private readonly db: PostgresDb<Schema>) {}
|
|
18
17
|
|
|
19
18
|
transaction<T>(work: (tx: PostgresTx<Schema>) => Promise<T>): Promise<T> {
|
|
20
|
-
return runWithUnitOfWorkContext('postgres',
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.db.transaction((db) => work({ db })),
|
|
26
|
-
)
|
|
27
|
-
} finally {
|
|
28
|
-
this.active = false
|
|
29
|
-
}
|
|
30
|
-
})
|
|
19
|
+
return runWithUnitOfWorkContext('postgres', () =>
|
|
20
|
+
executeDatabase('unit of work transaction', () =>
|
|
21
|
+
holdConnection(() => this.db.transaction((db) => work({ db }))),
|
|
22
|
+
),
|
|
23
|
+
)
|
|
31
24
|
}
|
|
32
25
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AppError } from '@/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A pool statement started while this async path is inside a unit of work.
|
|
5
|
+
* The pool's one connection is already checked out, so the statement would wait forever.
|
|
6
|
+
*/
|
|
7
|
+
export class StatementOutsideTransactionError extends AppError {
|
|
8
|
+
readonly _tag = 'StatementOutsideTransactionError'
|
|
9
|
+
readonly classification = 'terminal'
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super('a statement outside the transaction cannot run while a unit of work is open')
|
|
13
|
+
}
|
|
14
|
+
}
|