better-ship 0.30.2 → 0.31.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.
@@ -285,9 +285,11 @@ export declare class PostgresTransaction<Schema extends DrizzleSchema> implement
285
285
  * `connectionString` is the Hyperdrive binding's `connectionString`.
286
286
  * Create the session inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
287
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.
288
290
  * Requires the `pg` package.
289
291
  */
290
- export declare function connectPostgres<Schema extends DrizzleSchema>(connectionString: string, schema: Schema): Promise<{
292
+ export declare function connectPostgres<Schema extends DrizzleSchema>(connectionString: string, schema: Schema, tracing?: Tracing): Promise<{
291
293
  readonly db: PostgresDb<Schema> & {
292
294
  readonly $client: PostgresConnection;
293
295
  };
@@ -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;;;;;;;;;;;wBCFlD,gBAAgB,eAAe,eACnD,0BACA,QAAQ,SACP;WAAmB,IAAI,WAAW;aAAqB,SAAS;;;;;;;;;;;wBCJ7C,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/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"}
package/dist/postgres.js CHANGED
@@ -219,18 +219,66 @@ var PostgresTransaction = class {
219
219
  * `connectionString` is the Hyperdrive binding's `connectionString`.
220
220
  * Create the session inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
221
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.
222
224
  * Requires the `pg` package.
223
225
  */
224
- async function connectPostgres(connectionString, schema) {
226
+ async function connectPostgres(connectionString, schema, tracing) {
225
227
  const client = new Client({ connectionString });
228
+ if (tracing !== void 0) traceQueries(client, tracing);
226
229
  client.on("error", () => {});
227
230
  try {
228
- await client.connect();
231
+ await connect(client, tracing);
229
232
  } catch (cause) {
230
233
  throw classifyPostgresError(cause, "connect") ?? connectFailure(cause);
231
234
  }
232
235
  return { db: drizzle(client, { schema }) };
233
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);
245
+ });
246
+ }
247
+ function traceQueries(client, tracing) {
248
+ const query = client.query.bind(client);
249
+ const traced = (...args) => {
250
+ const text = statementText(args);
251
+ if (text === void 0) return callQuery(query, args);
252
+ const operation = operationOf(text);
253
+ return tracing.enterSpan(operation, async (span) => {
254
+ span.setAttributes({
255
+ "db.system.name": "postgresql",
256
+ "db.query.summary": operation,
257
+ "db.operation.name": operation,
258
+ "db.query.text": text
259
+ });
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 });
264
+ return result;
265
+ });
266
+ };
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
+ }
275
+ function statementText(args) {
276
+ if (typeof args[args.length - 1] === "function") return void 0;
277
+ const first = args[0];
278
+ if (typeof first === "object" && first !== null && "submit" in first && typeof first.submit === "function") return;
279
+ if (typeof first === "string") return first;
280
+ if (typeof first === "object" && first !== null && "text" in first && typeof first.text === "string") return first.text;
281
+ }
234
282
  function connectFailure(cause) {
235
283
  return new DatabaseError("connect", "transient", {
236
284
  database: "postgres",
@@ -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 * Requires the `pg` package.\n */\nexport async function connectPostgres<Schema extends DrizzleSchema>(\n connectionString: string,\n schema: Schema,\n): Promise<{ readonly db: PostgresDb<Schema> & { readonly $client: PostgresConnection } }> {\n const client = new Client({ connectionString })\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 client.connect()\n } catch (cause) {\n throw classifyPostgresError(cause, 'connect') ?? connectFailure(cause)\n }\n return { db: drizzle(client, { schema }) }\n}\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/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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"}
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.30.2",
4
+ "version": "0.31.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": {
@@ -12,23 +12,103 @@ import type { PostgresConnection, PostgresDb } from './postgres-database-types.t
12
12
  * `connectionString` is the Hyperdrive binding's `connectionString`.
13
13
  * Create the session inside the handler, the queue consumer, the schedule, the Workflow step, or the Durable Object method.
14
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.
15
17
  * Requires the `pg` package.
16
18
  */
17
19
  export async function connectPostgres<Schema extends DrizzleSchema>(
18
20
  connectionString: string,
19
21
  schema: Schema,
22
+ tracing?: Tracing,
20
23
  ): Promise<{ readonly db: PostgresDb<Schema> & { readonly $client: PostgresConnection } }> {
21
24
  const client = new Client({ connectionString })
25
+ if (tracing !== undefined) traceQueries(client, tracing)
22
26
  // An idle socket can emit `error` after connect with no query waiting. The next query reports it.
23
27
  client.on('error', () => {})
24
28
  try {
25
- await client.connect()
29
+ await connect(client, tracing)
26
30
  } catch (cause) {
27
31
  throw classifyPostgresError(cause, 'connect') ?? connectFailure(cause)
28
32
  }
29
33
  return { db: drizzle(client, { schema }) }
30
34
  }
31
35
 
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
+ }
43
+
44
+ function traceQueries(client: Client, tracing: Tracing): void {
45
+ const query = client.query.bind(client)
46
+ const traced = (...args: readonly unknown[]) => {
47
+ const text = statementText(args)
48
+ if (text === undefined) return callQuery(query, args)
49
+ const operation = operationOf(text)
50
+ return tracing.enterSpan(operation, async (span) => {
51
+ span.setAttributes({
52
+ 'db.system.name': 'postgresql',
53
+ 'db.query.summary': operation,
54
+ 'db.operation.name': operation,
55
+ 'db.query.text': text,
56
+ })
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
64
+ })
65
+ }
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
+ }
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
+ function statementText(args: readonly unknown[]): string | undefined {
89
+ const last = args[args.length - 1]
90
+ if (typeof last === 'function') return undefined
91
+ 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
+ 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
109
+ }
110
+ /* oxlint-enable anti-slop/no-runtime-typeof */
111
+
32
112
  function connectFailure(cause: unknown): DatabaseError {
33
113
  return new DatabaseError(
34
114
  'connect',