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.
@@ -1,21 +1,3 @@
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
- }
1
+ import { n as withSpan, t as annotateSpan } from "../tracing-C_7B_odV.js";
18
2
 
19
- //#endregion
20
- export { annotateSpan, withSpan };
21
- //# sourceMappingURL=tracing.js.map
3
+ export { annotateSpan, withSpan };
@@ -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 { Client } from "pg";
6
+ import { Pool } from "pg";
7
7
  //#region src/infrastructure/postgres/postgres-database-types.d.ts
8
- /** The pg client passed to Drizzle. One client belongs to one invocation. */
9
- type PostgresConnection = Client;
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 one pg client: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.
273
- * A second call while this one is open throws. Overlap needs a second `connectPostgres`.
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 Hyperdrive session for this invocation.
285
- * `connectionString` is the Hyperdrive binding's `connectionString`.
286
- * Create the session inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
287
- * The caller does not call `client.end()` and does not reuse the client.
288
- * Pass `tracing` to record one span per connect and per statement. The statement text keeps placeholders.
289
- * A value that was written into the SQL text, rather than passed as a parameter, is part of that text.
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): Promise<{
293
- readonly db: PostgresDb<Schema> & {
294
- readonly $client: PostgresConnection;
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
  /**
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCDb,oBAAoB,eAAe,0BAA0B,aACxE,WAAW;mBAIkB;UAFrB;EAER,YAA6B,IAAI,WAAW;EAE5C,YAAY,GAAG,OAAO,IAAI,WAAW,YAAY,QAAQ,KAAK,QAAQ;;;;;;;;;;;;;wBCAlD,gBAAgB,eAAe,eACnD,0BACA,QAAQ,QACR,UAAU,UACT;WAAmB,IAAI,WAAW;aAAqB,SAAS;;;;;;;;;;;wBCP7C,gBAAgB,GACpC,mBACA,YAAY,YAAY,IACxB;WAAoB;IACnB,QAAQ;;;;wBCmBK,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"}
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, t as UnitOfWorkAlreadyActiveError } from "./application-B5hnNUdI.js";
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 { Client } from "pg";
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 one pg client: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.
194
- * A second call while this one is open throws. Overlap needs a second `connectPostgres`.
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", async () => {
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/postgres-client.ts
223
+ //#region src/infrastructure/postgres/statement-outside-transaction.error.ts
217
224
  /**
218
- * Open one Hyperdrive session for this invocation.
219
- * `connectionString` is the Hyperdrive binding's `connectionString`.
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
- async function connectPostgres(connectionString, schema, tracing) {
227
- const client = new Client({ connectionString });
228
- if (tracing !== void 0) traceQueries(client, tracing);
229
- client.on("error", () => {});
230
- try {
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
- return { db: drizzle(client, { schema }) };
236
- }
237
- function connect(client, tracing) {
238
- if (tracing === void 0) return client.connect().then(() => void 0);
239
- return tracing.enterSpan("CONNECT", (span) => {
240
- span.setAttributes({
241
- "db.system.name": "postgresql",
242
- "db.operation.name": "CONNECT"
243
- });
244
- return client.connect().then(() => void 0);
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
- function traceQueries(client, tracing) {
248
- const query = client.query.bind(client);
249
- const traced = (...args) => {
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 callQuery(query, args);
252
- const operation = operationOf(text);
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 pending = callQuery(query, args);
261
- if (!(pending instanceof Promise)) return pending;
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 === "object" && first !== null && "text" in first && typeof first.text === "string") return first.text;
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
@@ -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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"}
@@ -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
@@ -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
- export { createAuthMiddleware, createErrorMiddleware };
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
@@ -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.31.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 { Client } from 'pg'
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 { classifyPostgresError } from './classify-postgres-error.ts'
8
- import type { PostgresConnection, PostgresDb } from './postgres-database-types.ts'
5
+ import { holdsConnection } from './connection-scope.ts'
6
+ import { StatementOutsideTransactionError } from './statement-outside-transaction.error.ts'
9
7
 
10
8
  /**
11
- * Open one Hyperdrive session for this invocation.
12
- * `connectionString` is the Hyperdrive binding's `connectionString`.
13
- * Create the session inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
14
- * The caller does not call `client.end()` and does not reuse the client.
15
- * Pass `tracing` to record one span per connect and per statement. The statement text keeps placeholders.
16
- * A value that was written into the SQL text, rather than passed as a parameter, is part of that text.
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 async function connectPostgres<Schema extends DrizzleSchema>(
16
+ export function connectPostgres<Schema extends DrizzleSchema>(
20
17
  connectionString: string,
21
18
  schema: Schema,
22
19
  tracing?: Tracing,
23
- ): Promise<{ readonly db: PostgresDb<Schema> & { readonly $client: PostgresConnection } }> {
24
- const client = new Client({ connectionString })
25
- if (tracing !== undefined) traceQueries(client, tracing)
26
- // An idle socket can emit `error` after connect with no query waiting. The next query reports it.
27
- client.on('error', () => {})
28
- try {
29
- await connect(client, tracing)
30
- } catch (cause) {
31
- throw classifyPostgresError(cause, 'connect') ?? connectFailure(cause)
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
- function connect(client: Client, tracing: Tracing | undefined): Promise<void> {
37
- if (tracing === undefined) return client.connect().then(() => undefined)
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
- function traceQueries(client: Client, tracing: Tracing): void {
45
- const query = client.query.bind(client)
46
- const traced = (...args: readonly unknown[]) => {
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 callQuery(query, args)
49
- const operation = operationOf(text)
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 pending = callQuery(query, args)
58
- if (!(pending instanceof Promise)) return pending
59
- const result = await pending
60
- if ('rows' in result && Array.isArray(result.rows)) {
61
- span.setAttributes({ 'db.response.returned_rows': result.rows.length })
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
- typeof first === 'object' &&
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) => classifyPostgresError(cause, operation) ?? 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 { Client } from 'pg'
3
+ import type { Pool } from 'pg'
4
4
 
5
5
  import type { DrizzleSchema } from '../drizzle-schema.ts'
6
6
 
7
- /** The pg client passed to Drizzle. One client belongs to one invocation. */
8
- export type PostgresConnection = Client
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 { UnitOfWorkAlreadyActiveError, type ITransaction } from '@/application'
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 one pg client: `BEGIN`, the work, `COMMIT`, or `ROLLBACK` on a throw.
10
- * A second call while this one is open throws. Overlap needs a second `connectPostgres`.
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', async () => {
21
- if (this.active) throw new UnitOfWorkAlreadyActiveError()
22
- this.active = true
23
- try {
24
- return await executeDatabase('unit of work transaction', () =>
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
+ }
@@ -11,3 +11,4 @@ export {
11
11
  type AuthCookieSource,
12
12
  type ErrorMiddlewareOptions,
13
13
  } from './error.middleware.ts'
14
+ export { createFunctionTracingMiddleware } from './function-tracing.middleware.ts'
@@ -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"}