better-ship 0.31.0 → 0.33.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/cloudflare/tracing.js +2 -20
- 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/dist/tanstack.d.ts +8 -0
- package/dist/tanstack.d.ts.map +1 -1
- package/dist/tanstack.js +16 -1
- package/dist/tanstack.js.map +1 -1
- package/dist/tracing-C_7B_odV.js +21 -0
- package/dist/tracing-C_7B_odV.js.map +1 -0
- 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/src/infrastructure/tanstack/function-tracing.middleware.ts +23 -0
- package/src/infrastructure/tanstack/index.ts +1 -0
- package/dist/cloudflare/tracing.js.map +0 -1
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
/** Add attributes to the active span. That span is the invocation root unless `withSpan` encloses the call. */
|
|
3
|
-
function annotateSpan(tracing, attributes) {
|
|
4
|
-
tracing.getActiveSpan()?.setAttributes(attributes);
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Run `operation` in a child span.
|
|
8
|
-
* Use it when one invocation runs several units of work, such as each message in a queue batch.
|
|
9
|
-
* This helper does not record an exception. A failure reaches Issues as the uncaught error or `logger.error`.
|
|
10
|
-
* An expected throw, such as a duplicate message, is not an exception record.
|
|
11
|
-
*/
|
|
12
|
-
function withSpan(tracing, name, attributes, operation) {
|
|
13
|
-
return tracing.enterSpan(name, (span) => {
|
|
14
|
-
span.setAttributes(attributes);
|
|
15
|
-
return operation();
|
|
16
|
-
});
|
|
17
|
-
}
|
|
1
|
+
import { n as withSpan, t as annotateSpan } from "../tracing-C_7B_odV.js";
|
|
18
2
|
|
|
19
|
-
|
|
20
|
-
export { annotateSpan, withSpan };
|
|
21
|
-
//# sourceMappingURL=tracing.js.map
|
|
3
|
+
export { annotateSpan, withSpan };
|
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/dist/tanstack.d.ts
CHANGED
|
@@ -97,5 +97,13 @@ export declare function createErrorMiddleware<Entries>(options: ErrorMiddlewareO
|
|
|
97
97
|
toHttpErrorResponse: (cause: unknown) => Response;
|
|
98
98
|
};
|
|
99
99
|
//#endregion
|
|
100
|
+
//#region src/infrastructure/tanstack/function-tracing.middleware.d.ts
|
|
101
|
+
/**
|
|
102
|
+
* Run one server function call in a child span named for that function.
|
|
103
|
+
* Ported from Suparelay `tracing.middleware.ts`. `getTracing` runs on each request.
|
|
104
|
+
* A handler rejection is not a span exception, and this middleware does not log.
|
|
105
|
+
*/
|
|
106
|
+
export declare function createFunctionTracingMiddleware(getTracing: (context: RequestContext) => Tracing): import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, undefined, undefined, undefined>;
|
|
107
|
+
//#endregion
|
|
100
108
|
export type { AuthContext, AuthCookieSource, AuthMiddlewareOptions, AuthSession, AuthenticatedContext, ErrorMiddlewareOptions, RequiredAuthContext };
|
|
101
109
|
//# sourceMappingURL=tanstack.d.ts.map
|
package/dist/tanstack.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tanstack.d.ts","names":[],"sources":["../src/infrastructure/tanstack/request-context.ts","../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/error.middleware.ts"],"mappings":";;;;;;;;;;KAQY,iBAAiB;EAAmB;IAAU,sBAAsB;;IAC5E;;;;KCCQ;WACD;aAAiB;;WACjB;aAAoB;;;;KAInB,oBAAoB,gBAAgB;WACrC,MAAM;WACN,SAAS;;;KAIR,YAAY,gBAAgB,eACpC,oBAAoB;WAET;WACA;;;KAIH,qBAAqB,gBAAgB;WACtC,MAAM;WACN,SAAS;WACT,gBAAgB;;;KAIf,sBAAsB,gBAAgB;WACvC,UAAU,SAAS;aACjB;eACE,aAAa;QAAW,SAAS;YAAc,QAAQ;;;;WAI3D,mBAAmB,SAAS,qBAAqB;;;;;;;wBAc5C,qBAAqB,gBAAgB,aACnD,SAAS,sBAAsB;;;;;;;;KC/CrB;WACD,UAAU;aAAmB,aAAa;;;;KAIzC,uBAAuB;WACxB,QAAQ,WAAW;;WAEnB;;WAEA,UAAU,SAAS,mBAAmB;;;;;;wBAOjC,sBAAsB,SAAS,SAAS,uBAAuB"}
|
|
1
|
+
{"version":3,"file":"tanstack.d.ts","names":[],"sources":["../src/infrastructure/tanstack/request-context.ts","../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/error.middleware.ts","../src/infrastructure/tanstack/function-tracing.middleware.ts"],"mappings":";;;;;;;;;;KAQY,iBAAiB;EAAmB;IAAU,sBAAsB;;IAC5E;;;;KCCQ;WACD;aAAiB;;WACjB;aAAoB;;;;KAInB,oBAAoB,gBAAgB;WACrC,MAAM;WACN,SAAS;;;KAIR,YAAY,gBAAgB,eACpC,oBAAoB;WAET;WACA;;;KAIH,qBAAqB,gBAAgB;WACtC,MAAM;WACN,SAAS;WACT,gBAAgB;;;KAIf,sBAAsB,gBAAgB;WACvC,UAAU,SAAS;aACjB;eACE,aAAa;QAAW,SAAS;YAAc,QAAQ;;;;WAI3D,mBAAmB,SAAS,qBAAqB;;;;;;;wBAc5C,qBAAqB,gBAAgB,aACnD,SAAS,sBAAsB;;;;;;;;KC/CrB;WACD,UAAU;aAAmB,aAAa;;;;KAIzC,uBAAuB;WACxB,QAAQ,WAAW;;WAEnB;;WAEA,UAAU,SAAS,mBAAmB;;;;;;wBAOjC,sBAAsB,SAAS,SAAS,uBAAuB;;;;;;;;;;;;;;;;;;;;;wBCnB/D,gCAAgC,aAAa,SAAS,mBAAmB,0CAAO"}
|
package/dist/tanstack.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { H as StaleSessionError, R as ValidationError, W as AuthenticationError, nt as AppError, r as createLogger } from "./core-BiN6S_cm.js";
|
|
2
|
+
import { n as withSpan } from "./tracing-C_7B_odV.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getReasonPhrase } from "http-status-codes";
|
|
4
5
|
import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
|
|
@@ -229,5 +230,19 @@ function createErrorMiddleware(options) {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
//#endregion
|
|
232
|
-
|
|
233
|
+
//#region src/infrastructure/tanstack/function-tracing.middleware.ts
|
|
234
|
+
/**
|
|
235
|
+
* Run one server function call in a child span named for that function.
|
|
236
|
+
* Ported from Suparelay `tracing.middleware.ts`. `getTracing` runs on each request.
|
|
237
|
+
* A handler rejection is not a span exception, and this middleware does not log.
|
|
238
|
+
*/
|
|
239
|
+
function createFunctionTracingMiddleware(getTracing) {
|
|
240
|
+
return createMiddleware({ type: "function" }).server(({ context, serverFnMeta, next }) => withSpan(getTracing(context), `server_fn ${serverFnMeta.name}`, {
|
|
241
|
+
"code.function.name": serverFnMeta.name,
|
|
242
|
+
"code.file.path": serverFnMeta.filename
|
|
243
|
+
}, () => next()));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
export { createAuthMiddleware, createErrorMiddleware, createFunctionTracingMiddleware };
|
|
233
248
|
//# sourceMappingURL=tanstack.js.map
|
package/dist/tanstack.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tanstack.js","names":["logger"],"sources":["../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/resolve-failure.ts","../src/infrastructure/tanstack/http-error-response.ts","../src/infrastructure/tanstack/session-cookies.ts","../src/infrastructure/tanstack/to-error-payload.ts","../src/infrastructure/tanstack/error.middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\nimport { getSessionCookie } from 'better-auth/cookies'\n\nimport { AuthenticationError, StaleSessionError, createLogger } from '@/core'\n\nimport type { RequestContext } from './request-context.ts'\n\nconst logger = createLogger('auth-middleware')\n\n/** The fields middleware reads; generic callers retain every additional session and user field. */\nexport type AuthSession = {\n readonly user: { readonly id: string }\n readonly session: { readonly id: string }\n}\n\n/** Authenticated context retains the application's complete user and session types. */\nexport type RequiredAuthContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n}\n\n/** Optional authentication supplies both fields together or neither field. */\nexport type AuthContext<Session extends AuthSession> =\n | RequiredAuthContext<Session>\n | {\n readonly user: undefined\n readonly session: undefined\n }\n\n/** A resolved session and the request context that produced it. */\nexport type AuthenticatedContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n readonly requestContext: RequestContext\n}\n\n/** Select the current request's Better Auth instance from TanStack's registered context. */\nexport type AuthMiddlewareOptions<Session extends AuthSession> = {\n readonly getAuth: (context: RequestContext) => {\n readonly api: {\n readonly getSession: (options: { headers: Headers }) => Promise<Session | null>\n }\n }\n /** Runs after a session resolves and before the handler. A guest or a failed lookup does not call it. */\n readonly onAuthenticated?: (context: AuthenticatedContext<Session>) => void\n}\n\n// The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily inside `.server()`.\nconst requestHeaders = async () => {\n const { getRequestHeaders } = await import('@tanstack/react-start/server')\n return getRequestHeaders()\n}\n\n/**\n * Preserve Porte's function and request middleware contracts with application-owned services.\n * Optional lookup failures log once and continue as guests. Required lookup failures propagate.\n * `onAuthenticated` runs after a session resolves and before the rest of the chain.\n */\nexport function createAuthMiddleware<Session extends AuthSession>(\n options: AuthMiddlewareOptions<Session>,\n) {\n function notifyAuthenticated(requestContext: RequestContext, resolved: Session): void {\n options.onAuthenticated?.({\n user: resolved.user,\n session: resolved.session,\n requestContext,\n })\n }\n const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(\n async ({ next, context }) => {\n let resolved: Session | null\n try {\n const headers = await requestHeaders()\n resolved = await options.getAuth(context).api.getSession({ headers })\n } catch (error) {\n logger.error('get_session_failed', { error })\n resolved = null\n }\n if (resolved === null)\n return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })\n notifyAuthenticated(context, resolved)\n return next<AuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n\n async function resolveRequiredSession(\n context: RequestContext,\n headers: Headers,\n ): Promise<Session> {\n const resolved = await options.getAuth(context).api.getSession({ headers })\n if (resolved === null) {\n if (getSessionCookie(headers)) throw new StaleSessionError()\n throw new AuthenticationError()\n }\n return resolved\n }\n\n const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {\n const resolved = await resolveRequiredSession(context, await requestHeaders())\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n })\n const requireAuthRequest = createMiddleware({ type: 'request' }).server(\n async ({ request, next, context }) => {\n const resolved = await resolveRequiredSession(context, request.headers)\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n return { extractAuth, requireAuth, requireAuthRequest }\n}\n","import { z } from 'zod'\n\nimport {\n AppError,\n ValidationError,\n createLogger,\n type ErrorTable,\n type ResolvedError,\n} from '@/core'\nimport type { ErrorPayloadOf } from '@/core'\n\nconst logger = createLogger('error-boundary')\n\n// TanStack serializes a validator's issues into `Error.message` as JSON.\nconst SerializedIssuesSchema = z\n .array(z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])) }))\n .min(1)\n\n// A live ZodError keeps `.issues`. TanStack leaves only JSON in `Error.message`.\nfunction reconstructValidationError(cause: unknown): ValidationError | null {\n if (cause instanceof z.ZodError) return ValidationError.fromZod(cause)\n if (!(cause instanceof Error)) return null\n let message: unknown\n try {\n message = JSON.parse(cause.message)\n } catch {\n return null\n }\n const parsed = SerializedIssuesSchema.safeParse(message)\n return parsed.success ? ValidationError.fromIssues(parsed.data) : null\n}\n\n/**\n * Resolve one failure through the table or fall back to the generic replacement, and log it once.\n * A thrown ZodError, or issues serialized into an error message, becomes a ValidationError first.\n * Both transport converters call this, so one rule decides the log level: mapped is a warning, unmapped is an error with the cause.\n */\nexport function resolveFailure<Entries>(\n table: ErrorTable<Entries>,\n cause: unknown,\n): ResolvedError<ErrorPayloadOf<ErrorTable<Entries>>> {\n const failure = reconstructValidationError(cause) ?? cause\n const resolved = table.resolve(failure)\n if (resolved !== null) {\n logger.warn('handled_error', {\n tag: resolved.payload._tag,\n status: resolved.status,\n })\n return resolved\n }\n const classification = failure instanceof AppError ? failure.classification : 'unknown'\n logger.error('unexpected_error', { error: failure, classification })\n return table.fallback(failure)\n}\n","import { getReasonPhrase } from 'http-status-codes'\nimport { z } from 'zod'\n\nimport type { ErrorTable, ProblemDetails } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// A known delay becomes the header; a null delay sends no header.\nconst RetryAfterSchema = z.object({ retryAfter: z.number() })\n\n/**\n * Build the API route converter for one error table.\n * The body is RFC 9457 problem details; public fields become extension members.\n */\nexport function createHttpErrorResponseConverter<Entries>(table: ErrorTable<Entries>) {\n return function toHttpErrorResponse(cause: unknown): Response {\n const { status, payload } = resolveFailure(table, cause)\n const { _tag, message, ...fields } = payload\n const body: ProblemDetails = {\n type: 'about:blank',\n title: getReasonPhrase(status),\n detail: message,\n ...fields,\n }\n const headers = new Headers({ 'Content-Type': 'application/problem+json' })\n const retry = RetryAfterSchema.safeParse(payload)\n if (retry.success) headers.set('Retry-After', String(retry.data.retryAfter))\n return Response.json(body, { status, headers })\n }\n}\n","import type { deleteCookie } from '@tanstack/react-start/server'\nimport type { BetterAuthCookies } from 'better-auth'\nimport { parseCookies } from 'better-auth/cookies'\n\ntype AuthCookie = BetterAuthCookies['sessionToken']\ntype DeleteCookieOptions = NonNullable<Parameters<typeof deleteCookie>[1]>\n\n/**\n * Delete the session, session cache, account cache, and remember-me cookies of one instance.\n * Chunks present on the request go too. Names and attributes come from the instance, so custom prefixes and domain cookies clear.\n */\nexport async function clearStaleSessionCookies(cookies: BetterAuthCookies): Promise<void> {\n // The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily here.\n const server = await import('@tanstack/react-start/server')\n const present = [...parseCookies(server.getRequestHeaders().get('cookie') ?? '').keys()]\n const definitions = [\n cookies.sessionToken,\n cookies.sessionData,\n cookies.accountData,\n cookies.dontRememberToken,\n ]\n for (const cookie of definitions) {\n // Better Auth names a chunk `${name}.${index}`, so a missed chunk would resurrect the value.\n const chunks = present.filter((name) => name.startsWith(`${cookie.name}.`))\n for (const name of new Set([cookie.name, ...chunks]))\n server.deleteCookie(name, deletionOf(cookie.attributes))\n }\n}\n\nfunction deletionOf(attributes: AuthCookie['attributes']): DeleteCookieOptions {\n // Better Auth sets `/` and the secure flag at runtime; the types leave both optional.\n const options: DeleteCookieOptions = {\n path: attributes.path ?? '/',\n secure: attributes.secure ?? false,\n }\n if (attributes.domain !== undefined) options.domain = attributes.domain\n return options\n}\n","import type { ErrorPayloadOf, ErrorTable } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n/**\n * Build the server-function converter for one error table.\n * The result is a plain object, because only own data survives serialization.\n */\nexport function createErrorPayloadConverter<Entries>(table: ErrorTable<Entries>) {\n return function toErrorPayload(cause: unknown): ErrorPayloadOf<ErrorTable<Entries>> {\n return resolveFailure(table, cause).payload\n }\n}\n","import { isNotFound, isRedirect, redirect } from '@tanstack/react-router'\nimport { createMiddleware } from '@tanstack/react-start'\nimport type { BetterAuthCookies } from 'better-auth'\n\nimport { StaleSessionError, type ErrorTable } from '@/core'\n\nimport { createHttpErrorResponseConverter } from './http-error-response.ts'\nimport type { RequestContext } from './request-context.ts'\nimport { clearStaleSessionCookies } from './session-cookies.ts'\nimport { createErrorPayloadConverter } from './to-error-payload.ts'\n\n/** The part of a Better Auth instance the boundary reads: its computed cookie definitions. */\nexport type AuthCookieSource = {\n readonly $context: Promise<{ readonly authCookies: BetterAuthCookies }>\n}\n\n/** The application's error table, its stale-session redirect, and its Better Auth instance. */\nexport type ErrorMiddlewareOptions<Entries> = {\n readonly errors: ErrorTable<Entries>\n /** The path a server function redirects to for a stale session, with any search string. */\n readonly signIn: string\n /** Select the request's Better Auth instance; its cookie definitions say what to clear. */\n readonly getAuth: (context: RequestContext) => AuthCookieSource\n}\n\n/**\n * Build both transport boundaries from one error table.\n * Register `functionErrorMiddleware` globally. Put `routeErrorMiddleware` first in an API route's `server.middleware`.\n */\nexport function createErrorMiddleware<Entries>(options: ErrorMiddlewareOptions<Entries>) {\n const toErrorPayload = createErrorPayloadConverter(options.errors)\n const toHttpErrorResponse = createHttpErrorResponseConverter(options.errors)\n\n async function clearSession(context: RequestContext): Promise<void> {\n const { authCookies } = await options.getAuth(context).$context\n await clearStaleSessionCookies(authCookies)\n }\n\n async function handleFunctionError(cause: unknown, context: RequestContext): Promise<never> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) {\n await clearSession(context)\n // oxlint-disable-next-line typescript/only-throw-error -- Router redirects by throwing this.\n throw redirect({ href: options.signIn })\n }\n // oxlint-disable-next-line typescript/only-throw-error -- The client rejects with this and reads its tag.\n throw toErrorPayload(cause)\n }\n\n // A route answers 401 instead of a redirect because fetch would follow it into the HTML shell.\n async function handleRouteError(cause: unknown, context: RequestContext): Promise<Response> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) await clearSession(context)\n return toHttpErrorResponse(cause)\n }\n\n const functionErrorMiddleware = createMiddleware({ type: 'function' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleFunctionError(cause, context)\n }\n },\n )\n\n const routeErrorMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleRouteError(cause, context)\n }\n },\n )\n\n return { functionErrorMiddleware, routeErrorMiddleware, toErrorPayload, toHttpErrorResponse }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tanstack.js","names":["logger"],"sources":["../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/resolve-failure.ts","../src/infrastructure/tanstack/http-error-response.ts","../src/infrastructure/tanstack/session-cookies.ts","../src/infrastructure/tanstack/to-error-payload.ts","../src/infrastructure/tanstack/error.middleware.ts","../src/infrastructure/tanstack/function-tracing.middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\nimport { getSessionCookie } from 'better-auth/cookies'\n\nimport { AuthenticationError, StaleSessionError, createLogger } from '@/core'\n\nimport type { RequestContext } from './request-context.ts'\n\nconst logger = createLogger('auth-middleware')\n\n/** The fields middleware reads; generic callers retain every additional session and user field. */\nexport type AuthSession = {\n readonly user: { readonly id: string }\n readonly session: { readonly id: string }\n}\n\n/** Authenticated context retains the application's complete user and session types. */\nexport type RequiredAuthContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n}\n\n/** Optional authentication supplies both fields together or neither field. */\nexport type AuthContext<Session extends AuthSession> =\n | RequiredAuthContext<Session>\n | {\n readonly user: undefined\n readonly session: undefined\n }\n\n/** A resolved session and the request context that produced it. */\nexport type AuthenticatedContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n readonly requestContext: RequestContext\n}\n\n/** Select the current request's Better Auth instance from TanStack's registered context. */\nexport type AuthMiddlewareOptions<Session extends AuthSession> = {\n readonly getAuth: (context: RequestContext) => {\n readonly api: {\n readonly getSession: (options: { headers: Headers }) => Promise<Session | null>\n }\n }\n /** Runs after a session resolves and before the handler. A guest or a failed lookup does not call it. */\n readonly onAuthenticated?: (context: AuthenticatedContext<Session>) => void\n}\n\n// The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily inside `.server()`.\nconst requestHeaders = async () => {\n const { getRequestHeaders } = await import('@tanstack/react-start/server')\n return getRequestHeaders()\n}\n\n/**\n * Preserve Porte's function and request middleware contracts with application-owned services.\n * Optional lookup failures log once and continue as guests. Required lookup failures propagate.\n * `onAuthenticated` runs after a session resolves and before the rest of the chain.\n */\nexport function createAuthMiddleware<Session extends AuthSession>(\n options: AuthMiddlewareOptions<Session>,\n) {\n function notifyAuthenticated(requestContext: RequestContext, resolved: Session): void {\n options.onAuthenticated?.({\n user: resolved.user,\n session: resolved.session,\n requestContext,\n })\n }\n const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(\n async ({ next, context }) => {\n let resolved: Session | null\n try {\n const headers = await requestHeaders()\n resolved = await options.getAuth(context).api.getSession({ headers })\n } catch (error) {\n logger.error('get_session_failed', { error })\n resolved = null\n }\n if (resolved === null)\n return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })\n notifyAuthenticated(context, resolved)\n return next<AuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n\n async function resolveRequiredSession(\n context: RequestContext,\n headers: Headers,\n ): Promise<Session> {\n const resolved = await options.getAuth(context).api.getSession({ headers })\n if (resolved === null) {\n if (getSessionCookie(headers)) throw new StaleSessionError()\n throw new AuthenticationError()\n }\n return resolved\n }\n\n const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {\n const resolved = await resolveRequiredSession(context, await requestHeaders())\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n })\n const requireAuthRequest = createMiddleware({ type: 'request' }).server(\n async ({ request, next, context }) => {\n const resolved = await resolveRequiredSession(context, request.headers)\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n return { extractAuth, requireAuth, requireAuthRequest }\n}\n","import { z } from 'zod'\n\nimport {\n AppError,\n ValidationError,\n createLogger,\n type ErrorTable,\n type ResolvedError,\n} from '@/core'\nimport type { ErrorPayloadOf } from '@/core'\n\nconst logger = createLogger('error-boundary')\n\n// TanStack serializes a validator's issues into `Error.message` as JSON.\nconst SerializedIssuesSchema = z\n .array(z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])) }))\n .min(1)\n\n// A live ZodError keeps `.issues`. TanStack leaves only JSON in `Error.message`.\nfunction reconstructValidationError(cause: unknown): ValidationError | null {\n if (cause instanceof z.ZodError) return ValidationError.fromZod(cause)\n if (!(cause instanceof Error)) return null\n let message: unknown\n try {\n message = JSON.parse(cause.message)\n } catch {\n return null\n }\n const parsed = SerializedIssuesSchema.safeParse(message)\n return parsed.success ? ValidationError.fromIssues(parsed.data) : null\n}\n\n/**\n * Resolve one failure through the table or fall back to the generic replacement, and log it once.\n * A thrown ZodError, or issues serialized into an error message, becomes a ValidationError first.\n * Both transport converters call this, so one rule decides the log level: mapped is a warning, unmapped is an error with the cause.\n */\nexport function resolveFailure<Entries>(\n table: ErrorTable<Entries>,\n cause: unknown,\n): ResolvedError<ErrorPayloadOf<ErrorTable<Entries>>> {\n const failure = reconstructValidationError(cause) ?? cause\n const resolved = table.resolve(failure)\n if (resolved !== null) {\n logger.warn('handled_error', {\n tag: resolved.payload._tag,\n status: resolved.status,\n })\n return resolved\n }\n const classification = failure instanceof AppError ? failure.classification : 'unknown'\n logger.error('unexpected_error', { error: failure, classification })\n return table.fallback(failure)\n}\n","import { getReasonPhrase } from 'http-status-codes'\nimport { z } from 'zod'\n\nimport type { ErrorTable, ProblemDetails } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// A known delay becomes the header; a null delay sends no header.\nconst RetryAfterSchema = z.object({ retryAfter: z.number() })\n\n/**\n * Build the API route converter for one error table.\n * The body is RFC 9457 problem details; public fields become extension members.\n */\nexport function createHttpErrorResponseConverter<Entries>(table: ErrorTable<Entries>) {\n return function toHttpErrorResponse(cause: unknown): Response {\n const { status, payload } = resolveFailure(table, cause)\n const { _tag, message, ...fields } = payload\n const body: ProblemDetails = {\n type: 'about:blank',\n title: getReasonPhrase(status),\n detail: message,\n ...fields,\n }\n const headers = new Headers({ 'Content-Type': 'application/problem+json' })\n const retry = RetryAfterSchema.safeParse(payload)\n if (retry.success) headers.set('Retry-After', String(retry.data.retryAfter))\n return Response.json(body, { status, headers })\n }\n}\n","import type { deleteCookie } from '@tanstack/react-start/server'\nimport type { BetterAuthCookies } from 'better-auth'\nimport { parseCookies } from 'better-auth/cookies'\n\ntype AuthCookie = BetterAuthCookies['sessionToken']\ntype DeleteCookieOptions = NonNullable<Parameters<typeof deleteCookie>[1]>\n\n/**\n * Delete the session, session cache, account cache, and remember-me cookies of one instance.\n * Chunks present on the request go too. Names and attributes come from the instance, so custom prefixes and domain cookies clear.\n */\nexport async function clearStaleSessionCookies(cookies: BetterAuthCookies): Promise<void> {\n // The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily here.\n const server = await import('@tanstack/react-start/server')\n const present = [...parseCookies(server.getRequestHeaders().get('cookie') ?? '').keys()]\n const definitions = [\n cookies.sessionToken,\n cookies.sessionData,\n cookies.accountData,\n cookies.dontRememberToken,\n ]\n for (const cookie of definitions) {\n // Better Auth names a chunk `${name}.${index}`, so a missed chunk would resurrect the value.\n const chunks = present.filter((name) => name.startsWith(`${cookie.name}.`))\n for (const name of new Set([cookie.name, ...chunks]))\n server.deleteCookie(name, deletionOf(cookie.attributes))\n }\n}\n\nfunction deletionOf(attributes: AuthCookie['attributes']): DeleteCookieOptions {\n // Better Auth sets `/` and the secure flag at runtime; the types leave both optional.\n const options: DeleteCookieOptions = {\n path: attributes.path ?? '/',\n secure: attributes.secure ?? false,\n }\n if (attributes.domain !== undefined) options.domain = attributes.domain\n return options\n}\n","import type { ErrorPayloadOf, ErrorTable } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n/**\n * Build the server-function converter for one error table.\n * The result is a plain object, because only own data survives serialization.\n */\nexport function createErrorPayloadConverter<Entries>(table: ErrorTable<Entries>) {\n return function toErrorPayload(cause: unknown): ErrorPayloadOf<ErrorTable<Entries>> {\n return resolveFailure(table, cause).payload\n }\n}\n","import { isNotFound, isRedirect, redirect } from '@tanstack/react-router'\nimport { createMiddleware } from '@tanstack/react-start'\nimport type { BetterAuthCookies } from 'better-auth'\n\nimport { StaleSessionError, type ErrorTable } from '@/core'\n\nimport { createHttpErrorResponseConverter } from './http-error-response.ts'\nimport type { RequestContext } from './request-context.ts'\nimport { clearStaleSessionCookies } from './session-cookies.ts'\nimport { createErrorPayloadConverter } from './to-error-payload.ts'\n\n/** The part of a Better Auth instance the boundary reads: its computed cookie definitions. */\nexport type AuthCookieSource = {\n readonly $context: Promise<{ readonly authCookies: BetterAuthCookies }>\n}\n\n/** The application's error table, its stale-session redirect, and its Better Auth instance. */\nexport type ErrorMiddlewareOptions<Entries> = {\n readonly errors: ErrorTable<Entries>\n /** The path a server function redirects to for a stale session, with any search string. */\n readonly signIn: string\n /** Select the request's Better Auth instance; its cookie definitions say what to clear. */\n readonly getAuth: (context: RequestContext) => AuthCookieSource\n}\n\n/**\n * Build both transport boundaries from one error table.\n * Register `functionErrorMiddleware` globally. Put `routeErrorMiddleware` first in an API route's `server.middleware`.\n */\nexport function createErrorMiddleware<Entries>(options: ErrorMiddlewareOptions<Entries>) {\n const toErrorPayload = createErrorPayloadConverter(options.errors)\n const toHttpErrorResponse = createHttpErrorResponseConverter(options.errors)\n\n async function clearSession(context: RequestContext): Promise<void> {\n const { authCookies } = await options.getAuth(context).$context\n await clearStaleSessionCookies(authCookies)\n }\n\n async function handleFunctionError(cause: unknown, context: RequestContext): Promise<never> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) {\n await clearSession(context)\n // oxlint-disable-next-line typescript/only-throw-error -- Router redirects by throwing this.\n throw redirect({ href: options.signIn })\n }\n // oxlint-disable-next-line typescript/only-throw-error -- The client rejects with this and reads its tag.\n throw toErrorPayload(cause)\n }\n\n // A route answers 401 instead of a redirect because fetch would follow it into the HTML shell.\n async function handleRouteError(cause: unknown, context: RequestContext): Promise<Response> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) await clearSession(context)\n return toHttpErrorResponse(cause)\n }\n\n const functionErrorMiddleware = createMiddleware({ type: 'function' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleFunctionError(cause, context)\n }\n },\n )\n\n const routeErrorMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleRouteError(cause, context)\n }\n },\n )\n\n return { functionErrorMiddleware, routeErrorMiddleware, toErrorPayload, toHttpErrorResponse }\n}\n","import { createMiddleware } from '@tanstack/react-start'\n\nimport { withSpan } from '../cloudflare/tracing.ts'\nimport type { RequestContext } from './request-context.ts'\n\n/**\n * Run one server function call in a child span named for that function.\n * Ported from Suparelay `tracing.middleware.ts`. `getTracing` runs on each request.\n * A handler rejection is not a span exception, and this middleware does not log.\n */\nexport function createFunctionTracingMiddleware(getTracing: (context: RequestContext) => Tracing) {\n return createMiddleware({ type: 'function' }).server(({ context, serverFnMeta, next }) =>\n withSpan(\n getTracing(context),\n `server_fn ${serverFnMeta.name}`,\n {\n 'code.function.name': serverFnMeta.name,\n 'code.file.path': serverFnMeta.filename,\n },\n () => next(),\n ),\n )\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/infrastructure/cloudflare/tracing.ts
|
|
2
|
+
/** Add attributes to the active span. That span is the invocation root unless `withSpan` encloses the call. */
|
|
3
|
+
function annotateSpan(tracing, attributes) {
|
|
4
|
+
tracing.getActiveSpan()?.setAttributes(attributes);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Run `operation` in a child span.
|
|
8
|
+
* Use it when one invocation runs several units of work, such as each message in a queue batch.
|
|
9
|
+
* This helper does not record an exception. A failure reaches Issues as the uncaught error or `logger.error`.
|
|
10
|
+
* An expected throw, such as a duplicate message, is not an exception record.
|
|
11
|
+
*/
|
|
12
|
+
function withSpan(tracing, name, attributes, operation) {
|
|
13
|
+
return tracing.enterSpan(name, (span) => {
|
|
14
|
+
span.setAttributes(attributes);
|
|
15
|
+
return operation();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { withSpan as n, annotateSpan as t };
|
|
21
|
+
//# sourceMappingURL=tracing-C_7B_odV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing-C_7B_odV.js","names":[],"sources":["../src/infrastructure/cloudflare/tracing.ts"],"sourcesContent":["/**\n * Workers tracing for Workers Issues.\n *\n * Ported from Suparelay `apps/web/src/server/infrastructure/tracing/span.ts`.\n * Pass `tracing` in. This file does not import `cloudflare:workers`.\n * A client bundle can import a server module that uses these helpers.\n *\n * Issues shows the active span's attributes beside a failure. Put identifiers on the span.\n * Outside an invocation there is no active span, and a missing span records nothing.\n */\n\n/** Attribute values a Workers span accepts. `undefined` leaves that attribute unset. */\nexport type SpanAttributes = Record<string, string | number | boolean | undefined>\n\n/** Add attributes to the active span. That span is the invocation root unless `withSpan` encloses the call. */\nexport function annotateSpan(tracing: Tracing, attributes: SpanAttributes): void {\n tracing.getActiveSpan()?.setAttributes(attributes)\n}\n\n/**\n * Run `operation` in a child span.\n * Use it when one invocation runs several units of work, such as each message in a queue batch.\n * This helper does not record an exception. A failure reaches Issues as the uncaught error or `logger.error`.\n * An expected throw, such as a duplicate message, is not an exception record.\n */\nexport function withSpan<T>(\n tracing: Tracing,\n name: string,\n attributes: SpanAttributes,\n operation: () => Promise<T>,\n): Promise<T> {\n return tracing.enterSpan(name, (span) => {\n span.setAttributes(attributes)\n return operation()\n })\n}\n"],"mappings":";;AAeA,SAAgB,aAAa,SAAkB,YAAkC;CAC/E,QAAQ,cAAc,CAAC,EAAE,cAAc,UAAU;AACnD;;;;;;;AAQA,SAAgB,SACd,SACA,MACA,YACA,WACY;CACZ,OAAO,QAAQ,UAAU,OAAO,SAAS;EACvC,KAAK,cAAc,UAAU;EAC7B,OAAO,UAAU;CACnB,CAAC;AACH"}
|
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.33.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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
2
|
+
|
|
3
|
+
import { withSpan } from '../cloudflare/tracing.ts'
|
|
4
|
+
import type { RequestContext } from './request-context.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run one server function call in a child span named for that function.
|
|
8
|
+
* Ported from Suparelay `tracing.middleware.ts`. `getTracing` runs on each request.
|
|
9
|
+
* A handler rejection is not a span exception, and this middleware does not log.
|
|
10
|
+
*/
|
|
11
|
+
export function createFunctionTracingMiddleware(getTracing: (context: RequestContext) => Tracing) {
|
|
12
|
+
return createMiddleware({ type: 'function' }).server(({ context, serverFnMeta, next }) =>
|
|
13
|
+
withSpan(
|
|
14
|
+
getTracing(context),
|
|
15
|
+
`server_fn ${serverFnMeta.name}`,
|
|
16
|
+
{
|
|
17
|
+
'code.function.name': serverFnMeta.name,
|
|
18
|
+
'code.file.path': serverFnMeta.filename,
|
|
19
|
+
},
|
|
20
|
+
() => next(),
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing.js","names":[],"sources":["../../src/infrastructure/cloudflare/tracing.ts"],"sourcesContent":["/**\n * Workers tracing for Workers Issues.\n *\n * Ported from Suparelay `apps/web/src/server/infrastructure/tracing/span.ts`.\n * Pass `tracing` in. This file does not import `cloudflare:workers`.\n * A client bundle can import a server module that uses these helpers.\n *\n * Issues shows the active span's attributes beside a failure. Put identifiers on the span.\n * Outside an invocation there is no active span, and a missing span records nothing.\n */\n\n/** Attribute values a Workers span accepts. `undefined` leaves that attribute unset. */\nexport type SpanAttributes = Record<string, string | number | boolean | undefined>\n\n/** Add attributes to the active span. That span is the invocation root unless `withSpan` encloses the call. */\nexport function annotateSpan(tracing: Tracing, attributes: SpanAttributes): void {\n tracing.getActiveSpan()?.setAttributes(attributes)\n}\n\n/**\n * Run `operation` in a child span.\n * Use it when one invocation runs several units of work, such as each message in a queue batch.\n * This helper does not record an exception. A failure reaches Issues as the uncaught error or `logger.error`.\n * An expected throw, such as a duplicate message, is not an exception record.\n */\nexport function withSpan<T>(\n tracing: Tracing,\n name: string,\n attributes: SpanAttributes,\n operation: () => Promise<T>,\n): Promise<T> {\n return tracing.enterSpan(name, (span) => {\n span.setAttributes(attributes)\n return operation()\n })\n}\n"],"mappings":";;AAeA,SAAgB,aAAa,SAAkB,YAAkC;CAC/E,QAAQ,cAAc,CAAC,EAAE,cAAc,UAAU;AACnD;;;;;;;AAQA,SAAgB,SACd,SACA,MACA,YACA,WACY;CACZ,OAAO,QAAQ,UAAU,OAAO,SAAS;EACvC,KAAK,cAAc,UAAU;EAC7B,OAAO,UAAU;CACnB,CAAC;AACH"}
|