better-ship 0.29.2 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{application-C_xN_1pa.js → application-B5hnNUdI.js} +9 -9
- package/dist/application-B5hnNUdI.js.map +1 -0
- package/dist/application.d.ts +1 -1
- package/dist/application.js +1 -1
- package/dist/bin/notify-ci.js +1 -1
- package/dist/cloudflare/queue.d.ts +1 -1
- package/dist/cloudflare/queue.js +2 -2
- package/dist/cloudflare/queue.js.map +1 -1
- package/dist/cloudflare.d.ts +2 -2
- package/dist/cloudflare.js +2 -2
- package/dist/{index-lS_g1olK.d.ts → index-CbvNd665.d.ts} +5 -6
- package/dist/{index-lS_g1olK.d.ts.map → index-CbvNd665.d.ts.map} +1 -1
- package/dist/{metrics-cv9P5nvk.d.ts → metrics-DXhrc6ii.d.ts} +2 -2
- package/dist/{metrics-cv9P5nvk.d.ts.map → metrics-DXhrc6ii.d.ts.map} +1 -1
- package/dist/{outbox-repository-options-Ww0kAgbq.js → outbox-repository-options-BNpCWTht.js} +2 -2
- package/dist/{outbox-repository-options-Ww0kAgbq.js.map → outbox-repository-options-BNpCWTht.js.map} +1 -1
- package/dist/{outbox-repository-options-15Sv9WnG.d.ts → outbox-repository-options-gk3JOjLB.d.ts} +2 -2
- package/dist/{outbox-repository-options-15Sv9WnG.d.ts.map → outbox-repository-options-gk3JOjLB.d.ts.map} +1 -1
- package/dist/postgres.d.ts +28 -10
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +81 -15
- package/dist/postgres.js.map +1 -1
- package/dist/posthog/metrics.d.ts +1 -1
- package/dist/posthog/server.d.ts +2 -2
- package/dist/resend.d.ts +1 -1
- package/dist/sentry/server.d.ts +1 -1
- package/dist/{slack-webhook-j1FCPeu8.js → slack-webhook-DSxFrjE9.js} +2 -2
- package/dist/slack-webhook-DSxFrjE9.js.map +1 -0
- package/dist/slack.js +1 -1
- package/dist/testing/outbox-repository.d.ts +1 -1
- package/package.json +7 -2
- package/src/application/message-bus.ts +17 -13
- package/src/application/transaction.ts +0 -1
- package/src/application/unit-of-work.ts +1 -1
- package/src/infrastructure/cloudflare/queue/queue-client.ts +1 -1
- package/src/infrastructure/postgres/classify-postgres-error.ts +44 -16
- package/src/infrastructure/postgres/index.ts +1 -0
- package/src/infrastructure/postgres/postgres-client.ts +45 -0
- package/src/infrastructure/postgres/postgres-database-execution.ts +11 -2
- package/src/infrastructure/postgres/postgres-database-types.ts +6 -5
- package/src/infrastructure/postgres/postgres-outbox-repository.ts +1 -1
- package/src/infrastructure/postgres/postgres-transaction.ts +16 -6
- package/src/infrastructure/slack/ci-message.ts +1 -4
- package/dist/application-C_xN_1pa.js.map +0 -1
- package/dist/slack-webhook-j1FCPeu8.js.map +0 -1
package/dist/postgres.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { O as DatabaseError, nt as AppError, s as exponentialDelayCapMs } from "./core-BiN6S_cm.js";
|
|
2
|
-
import { o as MessageAlreadyProcessedError } from "./application-
|
|
3
|
-
import { i as runWithUnitOfWorkContext, r as eventCollector, t as OUTBOX_REPOSITORY_DEFAULTS } from "./outbox-repository-options-
|
|
2
|
+
import { o as MessageAlreadyProcessedError, t as UnitOfWorkAlreadyActiveError } from "./application-B5hnNUdI.js";
|
|
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
5
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
6
6
|
import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
7
7
|
import { index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
8
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
9
|
+
import { Client } from "pg";
|
|
8
10
|
|
|
9
11
|
//#region src/infrastructure/postgres/classify-postgres-error.ts
|
|
10
12
|
const TRANSIENT_CODES = /* @__PURE__ */ new Set([
|
|
11
13
|
"40001",
|
|
12
14
|
"40P01",
|
|
15
|
+
"58000",
|
|
13
16
|
"08000",
|
|
14
17
|
"08001",
|
|
15
18
|
"08003",
|
|
@@ -48,6 +51,7 @@ const TERMINAL_CLASSES = /* @__PURE__ */ new Set([
|
|
|
48
51
|
"42",
|
|
49
52
|
"54"
|
|
50
53
|
]);
|
|
54
|
+
const DEAD_SOCKET_MESSAGES = /* @__PURE__ */ new Set(["Connection terminated unexpectedly", "Client has encountered a connection error and is not queryable"]);
|
|
51
55
|
/** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */
|
|
52
56
|
function classifyPostgresError(cause, operation) {
|
|
53
57
|
let error = cause;
|
|
@@ -58,18 +62,34 @@ function classifyPostgresError(cause, operation) {
|
|
|
58
62
|
}
|
|
59
63
|
if (error instanceof AppError || error instanceof Error && error.name === "AbortError") return void 0;
|
|
60
64
|
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
61
|
-
const serverError = error instanceof Error && error.name === "PostgresError" && code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
|
|
65
|
+
const serverError = error instanceof Error && (error.name === "PostgresError" || error.name === "error") && code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
|
|
62
66
|
const driverError = code !== void 0 && DRIVER_CODES.has(code);
|
|
67
|
+
const deadSocket = error instanceof Error && DEAD_SOCKET_MESSAGES.has(error.message);
|
|
63
68
|
if (!(cause instanceof DrizzleQueryError) && !serverError && !driverError) return void 0;
|
|
64
|
-
return new DatabaseError(operation, classificationOf(code), {
|
|
69
|
+
return new DatabaseError(operation, classificationOf(code, deadSocket), {
|
|
65
70
|
database: "postgres",
|
|
66
71
|
code,
|
|
67
|
-
table_name: error instanceof Error
|
|
68
|
-
column_name: error instanceof Error
|
|
69
|
-
constraint_name: error instanceof Error
|
|
72
|
+
table_name: error instanceof Error ? text$1(error, "table", "table_name") : void 0,
|
|
73
|
+
column_name: error instanceof Error ? text$1(error, "column", "column_name") : void 0,
|
|
74
|
+
constraint_name: error instanceof Error ? text$1(error, "constraint", "constraint_name") : void 0
|
|
70
75
|
}, { cause });
|
|
71
76
|
}
|
|
72
|
-
function
|
|
77
|
+
function text$1(error, pgName, jsName) {
|
|
78
|
+
return read(error, pgName) ?? read(error, jsName);
|
|
79
|
+
}
|
|
80
|
+
function read(error, name) {
|
|
81
|
+
switch (name) {
|
|
82
|
+
case "table": return "table" in error && typeof error.table === "string" ? error.table : void 0;
|
|
83
|
+
case "column": return "column" in error && typeof error.column === "string" ? error.column : void 0;
|
|
84
|
+
case "constraint": return "constraint" in error && typeof error.constraint === "string" ? error.constraint : void 0;
|
|
85
|
+
case "table_name": return "table_name" in error && typeof error.table_name === "string" ? error.table_name : void 0;
|
|
86
|
+
case "column_name": return "column_name" in error && typeof error.column_name === "string" ? error.column_name : void 0;
|
|
87
|
+
case "constraint_name": return "constraint_name" in error && typeof error.constraint_name === "string" ? error.constraint_name : void 0;
|
|
88
|
+
default: return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function classificationOf(code, deadSocket) {
|
|
92
|
+
if (deadSocket) return "transient";
|
|
73
93
|
if (code === void 0) return "unknown";
|
|
74
94
|
if (TRANSIENT_CODES.has(code)) return "transient";
|
|
75
95
|
if (TERMINAL_CODES.has(code) || TERMINAL_CLASSES.has(code.slice(0, 2))) return "terminal";
|
|
@@ -78,9 +98,17 @@ function classificationOf(code) {
|
|
|
78
98
|
|
|
79
99
|
//#endregion
|
|
80
100
|
//#region src/infrastructure/postgres/postgres-database-execution.ts
|
|
101
|
+
/** SQLSTATE values that leave the pg socket usable, so the same client can run the statement again. */
|
|
102
|
+
const RETRYABLE_ON_SESSION = /* @__PURE__ */ new Set([
|
|
103
|
+
"40001",
|
|
104
|
+
"40P01",
|
|
105
|
+
"58000"
|
|
106
|
+
]);
|
|
81
107
|
/**
|
|
82
108
|
* Convert database failures and preserve other exceptions. The default executes once.
|
|
83
|
-
* Opt-in
|
|
109
|
+
* Opt-in retries allow three attempts for serialization failure, deadlock, and a Hyperdrive pool acquire.
|
|
110
|
+
* A dropped socket stays transient and is not retried here: pg cannot reconnect the client.
|
|
111
|
+
* Delays use full jitter with backoff caps of 250 ms and 500 ms.
|
|
84
112
|
*/
|
|
85
113
|
async function executeDatabase(operation, work, options = {}) {
|
|
86
114
|
const result = await Result.tryPromise({
|
|
@@ -91,7 +119,7 @@ async function executeDatabase(operation, work, options = {}) {
|
|
|
91
119
|
delayMs: 250,
|
|
92
120
|
backoff: "exponential",
|
|
93
121
|
jitter: true,
|
|
94
|
-
shouldRetry: (error) => error instanceof DatabaseError && error.classification === "transient"
|
|
122
|
+
shouldRetry: (error) => error instanceof DatabaseError && error.classification === "transient" && error.details.database === "postgres" && error.details.code !== void 0 && RETRYABLE_ON_SESSION.has(error.details.code)
|
|
95
123
|
} });
|
|
96
124
|
if (result.isErr()) throw result.error;
|
|
97
125
|
return result.value;
|
|
@@ -162,19 +190,57 @@ var PostgresMessageStore = class {
|
|
|
162
190
|
//#endregion
|
|
163
191
|
//#region src/infrastructure/postgres/postgres-transaction.ts
|
|
164
192
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
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`.
|
|
167
195
|
*/
|
|
168
196
|
var PostgresTransaction = class {
|
|
169
197
|
db;
|
|
198
|
+
active = false;
|
|
170
199
|
constructor(db) {
|
|
171
200
|
this.db = db;
|
|
172
201
|
}
|
|
173
202
|
transaction(work) {
|
|
174
|
-
return runWithUnitOfWorkContext("postgres",
|
|
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
|
+
});
|
|
175
212
|
}
|
|
176
213
|
};
|
|
177
214
|
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/infrastructure/postgres/postgres-client.ts
|
|
217
|
+
/**
|
|
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
|
+
* Requires the `pg` package.
|
|
223
|
+
*/
|
|
224
|
+
async function connectPostgres(connectionString, schema) {
|
|
225
|
+
const client = new Client({ connectionString });
|
|
226
|
+
client.on("error", () => {});
|
|
227
|
+
try {
|
|
228
|
+
await client.connect();
|
|
229
|
+
} catch (cause) {
|
|
230
|
+
throw classifyPostgresError(cause, "connect") ?? connectFailure(cause);
|
|
231
|
+
}
|
|
232
|
+
return { db: drizzle(client, { schema }) };
|
|
233
|
+
}
|
|
234
|
+
function connectFailure(cause) {
|
|
235
|
+
return new DatabaseError("connect", "transient", {
|
|
236
|
+
database: "postgres",
|
|
237
|
+
code: void 0,
|
|
238
|
+
table_name: void 0,
|
|
239
|
+
column_name: void 0,
|
|
240
|
+
constraint_name: void 0
|
|
241
|
+
}, { cause });
|
|
242
|
+
}
|
|
243
|
+
|
|
178
244
|
//#endregion
|
|
179
245
|
//#region src/infrastructure/postgres/postgres-outbox-repository.ts
|
|
180
246
|
/**
|
|
@@ -229,7 +295,7 @@ var PostgresOutboxRepository = class {
|
|
|
229
295
|
async releaseClaim(claim, messages) {
|
|
230
296
|
const now = this.now();
|
|
231
297
|
for (const attempts of new Set(messages.map((row) => row.attempts))) {
|
|
232
|
-
const ids = messages.
|
|
298
|
+
const ids = messages.flatMap((row) => row.attempts === attempts ? [row.payload.id] : []);
|
|
233
299
|
const delay = exponentialDelayCapMs(this.options.retryBaseDelayMs, this.options.retryMaxDelayMs, attempts);
|
|
234
300
|
await executeDatabase("release outbox claim", () => this.db.update(messageOutbox).set({
|
|
235
301
|
availableAt: new Date(now.getTime() + delay),
|
|
@@ -263,5 +329,5 @@ var PostgresOutboxRepository = class {
|
|
|
263
329
|
};
|
|
264
330
|
|
|
265
331
|
//#endregion
|
|
266
|
-
export { BasePostgresRepository, DatabaseError, PostgresMessageStore, PostgresOutboxRepository, PostgresTransaction, classifyPostgresError, eventCollector, executeDatabase, messageOutbox, processedMessages };
|
|
332
|
+
export { BasePostgresRepository, DatabaseError, PostgresMessageStore, PostgresOutboxRepository, PostgresTransaction, classifyPostgresError, connectPostgres, eventCollector, executeDatabase, messageOutbox, processedMessages };
|
|
267
333
|
//# sourceMappingURL=postgres.js.map
|
package/dist/postgres.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.js","names":[],"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-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 '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'])\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' &&\n code !== undefined &&\n /^[0-9A-Z]{5}$/.test(code)\n const driverError = code !== undefined && DRIVER_CODES.has(code)\n if (!(cause instanceof DrizzleQueryError) && !serverError && !driverError) return undefined\n\n return new DatabaseError(\n operation,\n classificationOf(code),\n {\n database: 'postgres',\n code,\n table_name:\n error instanceof Error && 'table_name' in error && typeof error.table_name === 'string'\n ? error.table_name\n : undefined,\n column_name:\n error instanceof Error && 'column_name' in error && typeof error.column_name === 'string'\n ? error.column_name\n : undefined,\n constraint_name:\n error instanceof Error &&\n 'constraint_name' in error &&\n typeof error.constraint_name === 'string'\n ? error.constraint_name\n : undefined,\n },\n { cause },\n )\n}\n\n/* oxlint-enable anti-slop/no-runtime-typeof */\n\nfunction classificationOf(code: string | undefined): FailureClassification {\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/**\n * Convert database failures and preserve other exceptions. The default executes once.\n * Opt-in transient retries allow three attempts with full jitter and 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 && error.classification === 'transient',\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 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 * The Postgres transaction boundary uses native Drizzle transactions through postgres.js.\n * Drizzle commits when `work` returns and rolls back when it throws.\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', () => this.db.transaction((db) => work({ db }))),\n )\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.filter((row) => row.attempts === attempts).map((row) => 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 * 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":";;;;;;;;;;;AAIA,MAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,iCAAiB,IAAI,IAAI;CAAC;CAAS;CAAoB;CAAwB;AAAW,CAAC;AACjG,MAAM,+BAAe,IAAI,IAAI;CAC3B;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,mCAAmB,IAAI,IAAI;CAAC;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;CAAM;AAAI,CAAC;AACvF,MAAM,uCAAuB,IAAI,IAAI,CACnC,sCACA,gEACF,CAAC;;AAID,SAAgB,sBACd,OACA,WAC2B;CAC3B,IAAI,QAAQ;CACZ,MAAM,uBAAO,IAAI,IAAa;CAC9B,OAAO,iBAAiB,qBAAqB,CAAC,KAAK,IAAI,KAAK,GAAG;EAC7D,KAAK,IAAI,KAAK;EACd,QAAQ,MAAM;CAChB;CACA,IAAI,iBAAiB,YAAa,iBAAiB,SAAS,MAAM,SAAS,cACzE,OAAO;CACT,MAAM,OACJ,iBAAiB,SAAS,UAAU,SAAS,OAAO,MAAM,SAAS,WAC/D,MAAM,OACN;CACN,MAAM,cACJ,iBAAiB,UAChB,MAAM,SAAS,mBAAmB,MAAM,SAAS,YAClD,SAAS,UACT,gBAAgB,KAAK,IAAI;CAC3B,MAAM,cAAc,SAAS,UAAa,aAAa,IAAI,IAAI;CAC/D,MAAM,aAAa,iBAAiB,SAAS,qBAAqB,IAAI,MAAM,OAAO;CACnF,IAAI,EAAE,iBAAiB,sBAAsB,CAAC,eAAe,CAAC,aAAa,OAAO;CAElF,OAAO,IAAI,cACT,WACA,iBAAiB,MAAM,UAAU,GACjC;EACE,UAAU;EACV;EACA,YAAY,iBAAiB,QAAQA,OAAK,OAAO,SAAS,YAAY,IAAI;EAC1E,aAAa,iBAAiB,QAAQA,OAAK,OAAO,UAAU,aAAa,IAAI;EAC7E,iBACE,iBAAiB,QAAQA,OAAK,OAAO,cAAc,iBAAiB,IAAI;CAC5E,GACA,EAAE,MAAM,CACV;AACF;AAEA,SAASA,OAAK,OAAc,QAAgB,QAAoC;CAC9E,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,OAAO,MAAM;AAClD;AAEA,SAAS,KAAK,OAAc,MAAkC;CAC5D,QAAQ,MAAR;EACE,KAAK,SACH,OAAO,WAAW,SAAS,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;EAC7E,KAAK,UACH,OAAO,YAAY,SAAS,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS;EAChF,KAAK,cACH,OAAO,gBAAgB,SAAS,OAAO,MAAM,eAAe,WACxD,MAAM,aACN;EACN,KAAK,cACH,OAAO,gBAAgB,SAAS,OAAO,MAAM,eAAe,WACxD,MAAM,aACN;EACN,KAAK,eACH,OAAO,iBAAiB,SAAS,OAAO,MAAM,gBAAgB,WAC1D,MAAM,cACN;EACN,KAAK,mBACH,OAAO,qBAAqB,SAAS,OAAO,MAAM,oBAAoB,WAClE,MAAM,kBACN;EACN,SACE;CACJ;AACF;AAIA,SAAS,iBAAiB,MAA0B,YAA4C;CAC9F,IAAI,YAAY,OAAO;CACvB,IAAI,SAAS,QAAW,OAAO;CAC/B,IAAI,gBAAgB,IAAI,IAAI,GAAG,OAAO;CACtC,IAAI,eAAe,IAAI,IAAI,KAAK,iBAAiB,IAAI,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,OAAO;CAC/E,OAAO;AACT;;;;;AC9GA,MAAM,uCAAuB,IAAI,IAAI;CAAC;CAAS;CAAS;AAAO,CAAC;;;;;;;AAQhE,eAAsB,gBACpB,WACA,MACA,UAAsD,CAAC,GAC3C;CACZ,MAAM,SAAS,MAAM,OAAO,WAC1B;EACE,KAAK,YAAY,MAAM,KAAK;EAC5B,QAAQ,UAAU,sBAAsB,OAAO,SAAS,KAAK;CAC/D,GACA,EACE,OAAO;EACL,OAAO,QAAQ,UAAU,cAAc,IAAI;EAC3C,SAAS;EACT,SAAS;EACT,QAAQ;EACR,cAAc,UACZ,iBAAiB,iBACjB,MAAM,mBAAmB,eACzB,MAAM,QAAQ,aAAa,cAC3B,MAAM,QAAQ,SAAS,UACvB,qBAAqB,IAAI,MAAM,QAAQ,IAAI;CAC/C,EACF,CACF;CACA,IAAI,OAAO,MAAM,GAAG,MAAM,OAAO;CACjC,OAAO,OAAO;AAChB;;;;;AC5BA,IAAsB,yBAAtB,MAA2E;CAC1C;CAA/B,YAAY,AAAmB,IAAmC;EAAnC;CAAoC;;CAGnE,AAAU,KAAQ,WAAmB,OAAyC;EAC5E,OAAO,gBAAgB,WAAW,KAAK;CACzC;;CAGA,AAAU,eAAe,WAAkC;EACzD,MAAM,SAAS,UAAU,cAAc;EACvC,IAAI,OAAO,WAAW,GAAG;EACzB,KAAK,MAAM,SAAS,QAAQ,eAAe,KAAK,KAAK;EACrD,UAAU,YAAY;CACxB;AACF;;;;;ACvBA,MAAa,oBAAoB,QAAQ,sBAAsB;CAC7D,WAAW,KAAK,YAAY,CAAC,CAAC,WAAW;CACzC,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AACtF,CAAC;;AAGD,MAAa,gBAAgB,QAC3B,kBACA;CACE,IAAI,KAAK,IAAI,CAAC,CAAC,WAAW;CAC1B,SAAS,MAAM,SAAS,CAAC,CAAC,MAAmB,CAAC,CAAC,QAAQ;CACvD,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CACpF,UAAU,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CACjD,YAAY,KAAK,aAAa;CAC9B,gBAAgB,UAAU,oBAAoB,EAAE,cAAc,KAAK,CAAC;CACpE,aAAa,UAAU,gBAAgB,EAAE,cAAc,KAAK,CAAC;CAC7D,UAAU,UAAU,aAAa,EAAE,cAAc,KAAK,CAAC;CACvD,WAAW,KAAK,YAAY;AAC9B,IACC,UAAU,CACT,MAAM,wBAAwB,CAAC,CAC5B,GAAG,MAAM,aAAa,MAAM,SAAS,CAAC,CACtC,MAAM,OAAO,MAAM,WAAW,CAAC,GAClC,MAAM,gCAAgC,CAAC,CAAC,GAAG,MAAM,UAAU,CAC7D,CACF;;;;;ACxBA,IAAa,uBAAb,MAAyF;CAC1D;CAA7B,YAAY,AAAiB,IAAmC;EAAnC;CAAoC;CAEjE,MAAM,MAAM,WAAqC;EAO/C,KAAI,MANkB,KAAK,GACxB,OAAO,iBAAiB,CAAC,CACzB,OAAO,EAAE,UAAU,CAAC,CAAC,CACrB,oBAAoB,CAAC,CACrB,UAAU,EAAE,WAAW,kBAAkB,UAAU,CAAC,EAE5C,CAAC,WAAW,GAAG,MAAM,IAAI,6BAA6B,SAAS;CAC5E;CAEA,MAAM,cAAc,QAAmD;EACrE,IAAI,OAAO,WAAW,GAAG;EACzB,MAAM,KAAK,GACR,OAAO,aAAa,CAAC,CACrB,OAAO,OAAO,KAAK,WAAW;GAAE,IAAI,MAAM;GAAI,SAAS;EAAM,EAAE,CAAC;CACrE;AACF;;;;;;;;AChBA,IAAa,sBAAb,MAEE;CAG6B;CAF7B,AAAQ,SAAS;CAEjB,YAAY,AAAiB,IAAwB;EAAxB;CAAyB;CAEtD,YAAe,MAA0D;EACvE,OAAO,yBAAyB,YAAY,YAAY;GACtD,IAAI,KAAK,QAAQ,MAAM,IAAI,6BAA6B;GACxD,KAAK,SAAS;GACd,IAAI;IACF,OAAO,MAAM,gBAAgB,kCAC3B,KAAK,GAAG,aAAa,OAAO,KAAK,EAAE,GAAG,CAAC,CAAC,CAC1C;GACF,UAAU;IACR,KAAK,SAAS;GAChB;EACF,CAAC;CACH;AACF;;;;;;;;;;;ACfA,eAAsB,gBACpB,kBACA,QACyF;CACzF,MAAM,SAAS,IAAI,OAAO,EAAE,iBAAiB,CAAC;CAE9C,OAAO,GAAG,eAAe,CAAC,CAAC;CAC3B,IAAI;EACF,MAAM,OAAO,QAAQ;CACvB,SAAS,OAAO;EACd,MAAM,sBAAsB,OAAO,SAAS,KAAK,eAAe,KAAK;CACvE;CACA,OAAO,EAAE,IAAI,QAAQ,QAAQ,EAAE,OAAO,CAAC,EAAE;AAC3C;AAEA,SAAS,eAAe,OAA+B;CACrD,OAAO,IAAI,cACT,WACA,aACA;EACE,UAAU;EACV,MAAM;EACN,YAAY;EACZ,aAAa;EACb,iBAAiB;CACnB,GACA,EAAE,MAAM,CACV;AACF;;;;;;;;AC1BA,IAAa,2BAAb,MAAmE;CAI9C;CAEA;CALnB,AAAiB;CAEjB,YACE,AAAiB,IACjB,UAAmC,CAAC,GACpC,AAAiB,4BAAwB,IAAI,KAAK,GAClD;EAHiB;EAEA;EAEjB,KAAK,UAAU;GAAE,GAAG;GAA4B,GAAG;EAAQ;CAC7D;;CAGA,MAAM,aAAa,WAAyC;EAC1D,MAAM,QAAQ,OAAO,WAAW;EAChC,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,OAAO,MAAM,gBAAgB,uCACjC,KAAK,GACF,OAAO,aAAa,CAAC,CACrB,IAAI;GACH,YAAY;GACZ,gBAAgB,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,QAAQ,OAAO;GAC7D,UAAU,GAAG,GAAG,cAAc,SAAS;EACzC,CAAC,CAAC,CACD,MACC,QACE,cAAc,IACd,KAAK,GACF,OAAO,EAAE,IAAI,cAAc,GAAG,CAAC,CAAC,CAChC,KAAK,aAAa,CAAC,CACnB,MACC,IACE,OAAO,cAAc,WAAW,GAChC,OAAO,cAAc,QAAQ,GAC7B,IAAI,cAAc,aAAa,GAAG,GAClC,GAAG,OAAO,cAAc,cAAc,GAAG,IAAI,cAAc,gBAAgB,GAAG,CAAC,CACjF,CACF,CAAC,CACA,QACC,IAAI,cAAc,WAAW,GAC7B,IAAI,cAAc,SAAS,GAC3B,IAAI,cAAc,EAAE,CACtB,CAAC,CACA,MAAM,SAAS,CAAC,CAChB,IAAI,UAAU,EAAE,YAAY,KAAK,CAAC,CACvC,CACF,CAAC,CACA,UAAU;GACT,SAAS,cAAc;GACvB,UAAU,cAAc;GACxB,aAAa,cAAc;GAC3B,WAAW,cAAc;EAC3B,CAAC,CACL;EAEA,KAAK,MACF,GAAG,MACF,EAAE,YAAY,QAAQ,IAAI,EAAE,YAAY,QAAQ,KAChD,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ,KAC5C,EAAE,QAAQ,GAAG,cAAc,EAAE,QAAQ,EAAE,CAC3C;EACA,OAAO;GAAE;GAAO,UAAU,KAAK,KAAK,EAAE,SAAS,gBAAgB;IAAE;IAAS;GAAS,EAAE;EAAE;CACzF;;CAGA,MAAM,cAAc,OAAoB,UAAuD;EAC7F,IAAI,SAAS,WAAW,GAAG;EAC3B,MAAM,gBAAgB,wCACpB,KAAK,GACF,OAAO,aAAa,CAAC,CACrB,IAAI;GAAE,aAAa,KAAK,IAAI;GAAG,YAAY;GAAM,gBAAgB;EAAK,CAAC,CAAC,CACxE,MACC,IACE,QACE,cAAc,IACd,SAAS,KAAK,QAAQ,IAAI,QAAQ,EAAE,CACtC,GACA,GAAG,cAAc,YAAY,MAAM,KAAK,CAC1C,CACF,CACJ;CACF;;CAGA,MAAM,aAAa,OAAoB,UAAuD;EAC5F,MAAM,MAAM,KAAK,IAAI;EACrB,KAAK,MAAM,YAAY,IAAI,IAAI,SAAS,KAAK,QAAQ,IAAI,QAAQ,CAAC,GAAG;GACnE,MAAM,MAAM,SAAS,SAAS,QAAS,IAAI,aAAa,WAAW,CAAC,IAAI,QAAQ,EAAE,IAAI,CAAC,CAAE;GACzF,MAAM,QAAQ,sBACZ,KAAK,QAAQ,kBACb,KAAK,QAAQ,iBACb,QACF;GAEA,MAAM,gBAAgB,8BACpB,KAAK,GACF,OAAO,aAAa,CAAC,CACrB,IAAI;IACH,aAAa,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK;IAC3C,YAAY;IACZ,gBAAgB;GAClB,CAAC,CAAC,CACD,MAAM,IAAI,QAAQ,cAAc,IAAI,GAAG,GAAG,GAAG,cAAc,YAAY,MAAM,KAAK,CAAC,CAAC,CACzF;EACF;CACF;;CAGA,MAAM,WACJ,OACA,UACA,QACA,cACe;EACf,IAAI,SAAS,WAAW,GAAG;EAC3B,MAAM,gBAAgB,qCACpB,KAAK,GACF,OAAO,aAAa,CAAC,CACrB,IAAI;GACH,UAAU,KAAK,IAAI;GACnB,WAAW,GAAG,OAAO,IAAI;GACzB,YAAY;GACZ,gBAAgB;EAClB,CAAC,CAAC,CACD,MACC,IACE,QACE,cAAc,IACd,SAAS,KAAK,QAAQ,IAAI,QAAQ,EAAE,CACtC,GACA,GAAG,cAAc,YAAY,MAAM,KAAK,CAC1C,CACF,CACJ;CACF;;CAGA,MAAM,sBAAsB,QAAc,WAAoC;EAiB5E,QAAO,MAhBY,gBAAgB,0CACjC,KAAK,GACF,OAAO,aAAa,CAAC,CACrB,MACC,QACE,cAAc,IACd,KAAK,GACF,OAAO,EAAE,IAAI,cAAc,GAAG,CAAC,CAAC,CAChC,KAAK,aAAa,CAAC,CACnB,MAAM,GAAG,cAAc,aAAa,MAAM,CAAC,CAAC,CAC5C,QAAQ,IAAI,cAAc,WAAW,GAAG,IAAI,cAAc,EAAE,CAAC,CAAC,CAC9D,MAAM,SAAS,CACpB,CACF,CAAC,CACA,UAAU,EAAE,IAAI,cAAc,GAAG,CAAC,CACvC,EACW,CAAC;CACd;;CAGA,MAAM,mBAAmB,QAAc,WAAoC;EAiBzE,QAAO,MAhBY,gBAAgB,uCACjC,KAAK,GACF,OAAO,aAAa,CAAC,CACrB,MACC,QACE,cAAc,IACd,KAAK,GACF,OAAO,EAAE,IAAI,cAAc,GAAG,CAAC,CAAC,CAChC,KAAK,aAAa,CAAC,CACnB,MAAM,IAAI,OAAO,cAAc,WAAW,GAAG,GAAG,cAAc,UAAU,MAAM,CAAC,CAAC,CAAC,CACjF,QAAQ,IAAI,cAAc,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC,CAAC,CAC3D,MAAM,SAAS,CACpB,CACF,CAAC,CACA,UAAU,EAAE,IAAI,cAAc,GAAG,CAAC,CACvC,EACW,CAAC;CACd;;CAGA,MAAM,qBAAqB,QAAc,WAAoC;EAiB3E,QAAO,MAhBY,gBAAgB,iCACjC,KAAK,GACF,OAAO,iBAAiB,CAAC,CACzB,MACC,QACE,kBAAkB,WAClB,KAAK,GACF,OAAO,EAAE,WAAW,kBAAkB,UAAU,CAAC,CAAC,CAClD,KAAK,iBAAiB,CAAC,CACvB,MAAM,GAAG,kBAAkB,aAAa,MAAM,CAAC,CAAC,CAChD,QAAQ,IAAI,kBAAkB,WAAW,GAAG,IAAI,kBAAkB,SAAS,CAAC,CAAC,CAC7E,MAAM,SAAS,CACpB,CACF,CAAC,CACA,UAAU,EAAE,WAAW,kBAAkB,UAAU,CAAC,CACzD,EACW,CAAC;CACd;AACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as PostHogMetricsService } from "../metrics-
|
|
1
|
+
import { t as PostHogMetricsService } from "../metrics-DXhrc6ii.js";
|
|
2
2
|
export { PostHogMetricsService };
|
package/dist/posthog/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Gt as AnalyticsEventIdentity, Kt as AnalyticsEventInput, qt as AnalyticsProperties } from "../index-DUFS0SJ_.js";
|
|
2
|
-
import { $ as AnalyticsService } from "../index-
|
|
3
|
-
import { t as PostHogMetricsService } from "../metrics-
|
|
2
|
+
import { $ as AnalyticsService } from "../index-CbvNd665.js";
|
|
3
|
+
import { t as PostHogMetricsService } from "../metrics-DXhrc6ii.js";
|
|
4
4
|
import { PostHog, PostHogOptions } from "posthog-node";
|
|
5
5
|
//#region src/infrastructure/posthog/server.d.ts
|
|
6
6
|
/** Deployment values and SDK options; the runtime owns delivery promises. */
|
package/dist/resend.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jt as BounceType } from "./index-DUFS0SJ_.js";
|
|
2
|
-
import { B as OutboundEmail, H as ReceivedContent, I as EmailAccepted, R as IEmailService, U as ReceivedEmail } from "./index-
|
|
2
|
+
import { B as OutboundEmail, H as ReceivedContent, I as EmailAccepted, R as IEmailService, U as ReceivedEmail } from "./index-CbvNd665.js";
|
|
3
3
|
import { n as WebhookVerifier } from "./webhook-verification-D1c7BY4q.js";
|
|
4
4
|
import "resend";
|
|
5
5
|
//#region src/infrastructure/resend/resend-email-service.d.ts
|
package/dist/sentry/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { u as LoggerErrorHook } from "../index-DUFS0SJ_.js";
|
|
2
|
-
import { G as ObservabilityScope, K as ObservabilityService, q as ObservabilityUser } from "../index-
|
|
2
|
+
import { G as ObservabilityScope, K as ObservabilityService, q as ObservabilityUser } from "../index-CbvNd665.js";
|
|
3
3
|
import * as Sentry from "@sentry/cloudflare";
|
|
4
4
|
//#region src/infrastructure/sentry/server.d.ts
|
|
5
5
|
/** Application-owned deployment settings with the full Cloudflare SDK option contract. */
|
|
@@ -174,7 +174,7 @@ function headlineFor(run) {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
function namesWith(results, outcome) {
|
|
177
|
-
return results.
|
|
177
|
+
return results.flatMap((result) => result.outcome === outcome ? [result.name] : []).join(", ");
|
|
178
178
|
}
|
|
179
179
|
function linkElements(run) {
|
|
180
180
|
const runHost = new URL(run.runUrl).hostname;
|
|
@@ -267,4 +267,4 @@ async function postSlackMessage(webhookUrl, message, fetcher = fetch) {
|
|
|
267
267
|
|
|
268
268
|
//#endregion
|
|
269
269
|
export { parseCiRun as a, buildCiMessage as i, postSlackMessage as n, CiRunParseError as r, SlackWebhookError as t };
|
|
270
|
-
//# sourceMappingURL=slack-webhook-
|
|
270
|
+
//# sourceMappingURL=slack-webhook-DSxFrjE9.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-webhook-DSxFrjE9.js","names":[],"sources":["../src/infrastructure/slack/ci-message.ts","../src/infrastructure/slack/slack-webhook.ts"],"sourcesContent":["import { z } from 'zod'\n\n/** The result of one CI job or step, in GitHub Actions terms. */\nexport type CiOutcome = 'success' | 'failure' | 'cancelled' | 'skipped'\n\n/** What the run delivers after its checks pass. */\nexport type CiTarget =\n | { readonly kind: 'check' }\n | {\n readonly kind: 'deploy'\n readonly accountId: string\n readonly worker: string\n readonly siteUrl: string\n /** Absent when the run reached no deploy, such as a pull request. */\n readonly versionId: string | undefined\n }\n | {\n readonly kind: 'publish'\n readonly packageName: string\n /** Absent when npm already has the package version. */\n readonly version: string | undefined\n }\n\n/** One CI run, parsed from the environment that the workflow sets. */\nexport interface CiRun {\n readonly repository: string\n readonly commitSha: string\n readonly ref:\n | { readonly kind: 'branch'; readonly name: string }\n | {\n readonly kind: 'pullRequest'\n readonly number: number\n readonly url: string\n readonly title: string\n }\n readonly runUrl: string\n readonly jobs: ReadonlyArray<CiStepResult>\n readonly steps: ReadonlyArray<CiStepResult>\n readonly target: CiTarget\n}\n\n/** A named job or step and its outcome. */\nexport interface CiStepResult {\n readonly name: string\n readonly outcome: CiOutcome\n}\n\n/** The commit facts that only git knows. */\nexport interface CiCommit {\n readonly subject: string\n readonly author: string\n}\n\n/** A Slack incoming-webhook payload. */\nexport interface SlackMessage {\n readonly text: string\n readonly blocks: ReadonlyArray<SlackBlock>\n}\n\ntype SlackElement =\n | { readonly type: 'mrkdwn'; readonly text: string }\n | { readonly type: 'image'; readonly image_url: string; readonly alt_text: string }\n\ntype SlackBlock =\n | { readonly type: 'section'; readonly text: { readonly type: 'mrkdwn'; readonly text: string } }\n | { readonly type: 'context'; readonly elements: ReadonlyArray<SlackElement> }\n\n/** The CI environment could not be parsed. `issues` names each invalid variable. */\nexport class CiRunParseError extends Error {\n readonly _tag = 'CiRunParseError' as const\n\n constructor(\n readonly issues: ReadonlyArray<string>,\n override readonly cause: unknown,\n ) {\n super(`Invalid CI environment: ${issues.join(', ')}`)\n }\n}\n\nconst OutcomeSchema = z.enum(['success', 'failure', 'cancelled', 'skipped'])\n\nconst jsonRecord = <T extends z.ZodType>(value: T) =>\n z\n .string()\n .transform((text, context) => {\n try {\n const value: unknown = JSON.parse(text)\n return value\n } catch {\n context.addIssue({ code: 'custom', message: 'Invalid JSON' })\n return z.NEVER\n }\n })\n .pipe(z.record(z.string(), value))\n\nconst CiEnvSchema = z.object({\n REPO: z.string().regex(/^[\\w.-]+\\/[\\w.-]+$/),\n COMMIT_SHA: z.string().regex(/^[0-9a-f]{40}$/),\n BRANCH: z.string().min(1),\n RUN_URL: z.httpUrl(),\n JOBS_JSON: jsonRecord(z.object({ result: OutcomeSchema })).optional(),\n STEPS_JSON: jsonRecord(z.object({ outcome: OutcomeSchema })).optional(),\n PR_NUMBER: z.coerce.number().int().positive().optional(),\n PR_URL: z.httpUrl().optional(),\n PR_TITLE: z.string().min(1).optional(),\n ACTION: z.enum(['check', 'deploy', 'publish']).default('check'),\n CLOUDFLARE_ACCOUNT_ID: z.string().min(1).optional(),\n CLOUDFLARE_WORKER: z.string().min(1).optional(),\n SITE_URL: z.httpUrl().optional(),\n DEPLOY_VERSION_ID: z.string().min(1).optional(),\n NPM_PACKAGE: z.string().min(1).optional(),\n NPM_VERSION: z.string().min(1).optional(),\n})\n\ntype CiEnv = z.infer<typeof CiEnvSchema>\n\n/**\n * Parse the variables that a workflow passes to the notifier. GitHub expressions render a\n * missing value as an empty string, so an empty variable counts as unset.\n * @throws {CiRunParseError} when a variable is missing or invalid.\n */\nexport function parseCiRun(env: Readonly<Record<string, string | undefined>>): CiRun {\n const present = Object.fromEntries(Object.entries(env).filter(([, value]) => value !== ''))\n const parsed = CiEnvSchema.safeParse(present)\n if (!parsed.success) {\n const issues = parsed.error.issues.map((issue) => issue.path.join('.'))\n throw new CiRunParseError(issues, parsed.error)\n }\n return toCiRun(parsed.data)\n}\n\nfunction toCiRun(env: CiEnv): CiRun {\n const ref: CiRun['ref'] =\n env.PR_NUMBER !== undefined && env.PR_URL !== undefined && env.PR_TITLE !== undefined\n ? { kind: 'pullRequest', number: env.PR_NUMBER, url: env.PR_URL, title: env.PR_TITLE }\n : { kind: 'branch', name: env.BRANCH }\n return {\n repository: env.REPO,\n commitSha: env.COMMIT_SHA,\n ref,\n runUrl: env.RUN_URL,\n jobs: Object.entries(env.JOBS_JSON ?? {}).map(([id, job]) => ({\n name: displayName(id),\n outcome: job.result,\n })),\n steps: Object.entries(env.STEPS_JSON ?? {}).map(([id, step]) => ({\n name: displayName(id),\n outcome: step.outcome,\n })),\n target: toTarget(env),\n }\n}\n\nfunction toTarget(env: CiEnv): CiTarget {\n switch (env.ACTION) {\n case 'check':\n return { kind: 'check' }\n case 'deploy': {\n const { CLOUDFLARE_ACCOUNT_ID: accountId, CLOUDFLARE_WORKER: worker, SITE_URL: siteUrl } = env\n if (accountId === undefined || worker === undefined || siteUrl === undefined) {\n throw new CiRunParseError(\n ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_WORKER', 'SITE_URL'],\n undefined,\n )\n }\n return { kind: 'deploy', accountId, worker, siteUrl, versionId: env.DEPLOY_VERSION_ID }\n }\n case 'publish':\n if (env.NPM_PACKAGE === undefined) throw new CiRunParseError(['NPM_PACKAGE'], undefined)\n return { kind: 'publish', packageName: env.NPM_PACKAGE, version: env.NPM_VERSION }\n }\n}\n\n// A job or step id such as `check-and-build` reads as \"Check and build\".\nfunction displayName(id: string): string {\n const words = id.replaceAll(/[-_]/g, ' ')\n return words.charAt(0).toUpperCase() + words.slice(1)\n}\n\nconst logos = {\n depot: 'https://avatars.githubusercontent.com/u/68166601?s=64',\n github: 'https://avatars.githubusercontent.com/u/9919?s=64',\n cloudflare: 'https://avatars.githubusercontent.com/u/314135?s=64',\n npm: 'https://avatars.githubusercontent.com/u/6078720?s=64',\n} as const\n\n/** Build the four-line CI message: result, change, links, and identity. */\nexport function buildCiMessage(run: CiRun, commit: CiCommit): SlackMessage {\n const headline = headlineFor(run)\n const change = run.ref.kind === 'pullRequest' ? run.ref.title : commit.subject\n return {\n text: `${headline} — ${change}`,\n blocks: [\n { type: 'section', text: { type: 'mrkdwn', text: `*${headline}*\\n${escape(change)}` } },\n { type: 'context', elements: linkElements(run) },\n { type: 'context', elements: [{ type: 'mrkdwn', text: identityLine(run, commit) }] },\n ],\n }\n}\n\nfunction headlineFor(run: CiRun): string {\n const failedJobs = namesWith(run.jobs, 'failure')\n if (failedJobs) return `:x: CI failed: ${failedJobs}`\n const cancelledJobs = namesWith(run.jobs, 'cancelled')\n if (cancelledJobs) return `:white_circle: CI cancelled: ${cancelledJobs}`\n const failedSteps = namesWith(run.steps, 'failure')\n const { target } = run\n switch (target.kind) {\n case 'check':\n return failedSteps ? `:x: CI failed: ${failedSteps}` : ':white_check_mark: CI passed'\n case 'deploy':\n if (failedSteps) return `:x: Deploy failed: ${failedSteps}`\n return target.versionId === undefined\n ? ':white_check_mark: CI passed'\n : ':white_check_mark: Deployed to production'\n case 'publish':\n if (failedSteps) return `:x: Publish failed: ${failedSteps}`\n return target.version === undefined\n ? ':white_check_mark: CI passed, nothing to publish'\n : `:white_check_mark: Published ${target.packageName} ${target.version}`\n }\n}\n\nfunction namesWith(results: ReadonlyArray<CiStepResult>, outcome: CiOutcome): string {\n return results.flatMap((result) => (result.outcome === outcome ? [result.name] : [])).join(', ')\n}\n\nfunction linkElements(run: CiRun): ReadonlyArray<SlackElement> {\n const runHost = new URL(run.runUrl).hostname\n const runLogo = runHost.endsWith('github.com') ? logos.github : logos.depot\n const elements: SlackElement[] = [\n { type: 'image', image_url: runLogo, alt_text: runHost },\n { type: 'mrkdwn', text: `<${run.runUrl}|CI run>` },\n ]\n const { target } = run\n if (target.kind === 'deploy' && target.versionId !== undefined) {\n const dashboard = `https://dash.cloudflare.com/${target.accountId}/workers/services/view/${target.worker}/production/deployments`\n const site = new URL(target.siteUrl)\n elements.push(\n { type: 'mrkdwn', text: '·' },\n { type: 'image', image_url: logos.cloudflare, alt_text: 'Cloudflare' },\n { type: 'mrkdwn', text: `<${dashboard}|Version \\`${target.versionId.slice(0, 8)}\\`>` },\n { type: 'mrkdwn', text: `· :globe_with_meridians: <${site.origin}|${site.host}>` },\n )\n }\n if (target.kind === 'publish' && target.version !== undefined) {\n const npm = `https://www.npmjs.com/package/${target.packageName}/v/${target.version}`\n elements.push(\n { type: 'mrkdwn', text: '·' },\n { type: 'image', image_url: logos.npm, alt_text: 'npm' },\n { type: 'mrkdwn', text: `<${npm}|${target.packageName}@${target.version}>` },\n )\n }\n return elements\n}\n\nfunction identityLine(run: CiRun, commit: CiCommit): string {\n const repository = run.repository.split('/').at(-1) ?? run.repository\n const ref =\n run.ref.kind === 'pullRequest' ? `<${run.ref.url}|PR #${run.ref.number}>` : run.ref.name\n const commitUrl = `https://github.com/${run.repository}/commit/${run.commitSha}`\n return [\n repository,\n ref,\n `<${commitUrl}|\\`${run.commitSha.slice(0, 7)}\\`>`,\n escape(commit.author),\n ].join(' · ')\n}\n\n// Slack mrkdwn treats these three characters as markup.\nfunction escape(text: string): string {\n return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n}\n","import type { SlackMessage } from './ci-message.ts'\n\n/** Slack refused the message or could not be reached. */\nexport class SlackWebhookError extends Error {\n readonly _tag = 'SlackWebhookError' as const\n\n constructor(\n readonly status: number | undefined,\n override readonly cause: unknown,\n ) {\n super(status === undefined ? 'Slack webhook unreachable' : `Slack webhook returned ${status}`)\n }\n}\n\n/**\n * Post one message to a Slack incoming webhook.\n * @throws {SlackWebhookError} when the request fails or Slack answers with an error status.\n */\nexport async function postSlackMessage(\n webhookUrl: string,\n message: SlackMessage,\n fetcher: typeof fetch = fetch,\n): Promise<void> {\n let response: Response\n try {\n response = await fetcher(webhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(message),\n signal: AbortSignal.timeout(30_000),\n })\n } catch (cause) {\n throw new SlackWebhookError(undefined, cause)\n }\n if (!response.ok) throw new SlackWebhookError(response.status, await response.text())\n}\n"],"mappings":"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"}
|
package/dist/slack.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as parseCiRun, i as buildCiMessage, n as postSlackMessage, r as CiRunParseError, t as SlackWebhookError } from "./slack-webhook-
|
|
1
|
+
import { a as parseCiRun, i as buildCiMessage, n as postSlackMessage, r as CiRunParseError, t as SlackWebhookError } from "./slack-webhook-DSxFrjE9.js";
|
|
2
2
|
|
|
3
3
|
export { CiRunParseError, SlackWebhookError, buildCiMessage, parseCiRun, postSlackMessage };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { W as DomainEvent } from "../index-DUFS0SJ_.js";
|
|
2
|
-
import { A as IOutboxRepository } from "../index-
|
|
2
|
+
import { A as IOutboxRepository } from "../index-CbvNd665.js";
|
|
3
3
|
//#region src/infrastructure/testing/outbox-repository-contract.d.ts
|
|
4
4
|
/** Stored fields observed by the adapter contract, independent of a database library. */
|
|
5
5
|
export type OutboxContractRow = {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "better-ship",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.30.2",
|
|
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": {
|
|
@@ -88,14 +88,15 @@
|
|
|
88
88
|
"@tanstack/start-client-core": "^1.170.34",
|
|
89
89
|
"@tanstack/start-storage-context": "^1.167.36",
|
|
90
90
|
"@types/node": "^26.6.3",
|
|
91
|
+
"@types/pg": "^8.23.1",
|
|
91
92
|
"@types/react": "^19.3.0",
|
|
92
93
|
"@vitest/browser-playwright": "^4.1.11",
|
|
93
94
|
"auth": "^1.7.7",
|
|
94
95
|
"better-auth": "^1.7.7",
|
|
95
96
|
"drizzle-kit": "^0.31.11",
|
|
96
97
|
"drizzle-orm": "^0.45.3",
|
|
98
|
+
"pg": "^8.23.1",
|
|
97
99
|
"playwright": "^1.63.0",
|
|
98
|
-
"postgres": "^3.4.9",
|
|
99
100
|
"posthog-js": "^1.435.6",
|
|
100
101
|
"posthog-node": "^5.55.0",
|
|
101
102
|
"react": "^19.3.0",
|
|
@@ -116,6 +117,7 @@
|
|
|
116
117
|
"@tanstack/react-start": "^1.168.60",
|
|
117
118
|
"better-auth": "^1.7.7",
|
|
118
119
|
"drizzle-orm": "^0.45.3",
|
|
120
|
+
"pg": "^8.23.1",
|
|
119
121
|
"posthog-js": "^1.435.6",
|
|
120
122
|
"posthog-node": "^5.55.0",
|
|
121
123
|
"react": "^19.3.0",
|
|
@@ -161,6 +163,9 @@
|
|
|
161
163
|
},
|
|
162
164
|
"drizzle-orm": {
|
|
163
165
|
"optional": true
|
|
166
|
+
},
|
|
167
|
+
"pg": {
|
|
168
|
+
"optional": true
|
|
164
169
|
}
|
|
165
170
|
},
|
|
166
171
|
"engines": {
|
|
@@ -65,24 +65,28 @@ export class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMess
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* an earlier delivery, so it counts as delivered.
|
|
68
|
+
* Subscribers run one after another, so one unit of work finishes before the next starts.
|
|
69
|
+
* Every subscriber still runs. One failure is rethrown as is. Several are thrown as one `AggregateError`.
|
|
70
|
+
* A `MessageAlreadyProcessedError` means that subscriber already committed on an earlier delivery, so it counts as delivered.
|
|
71
71
|
*/
|
|
72
72
|
private async handleEvent(event: DomainEvent): Promise<void> {
|
|
73
73
|
const subscriptions = this.registry.events[event.name] ?? []
|
|
74
|
-
const outcomes = await Promise.allSettled(
|
|
75
|
-
subscriptions.map((subscription) => subscription(event, this.deps)),
|
|
76
|
-
)
|
|
77
74
|
const failures: unknown[] = []
|
|
78
|
-
for (const [subscriber,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
for (const [subscriber, subscription] of subscriptions.entries()) {
|
|
76
|
+
try {
|
|
77
|
+
await subscription(event, this.deps)
|
|
78
|
+
} catch (reason) {
|
|
79
|
+
if (reason instanceof MessageAlreadyProcessedError) {
|
|
80
|
+
// The bus swallows this error, so this line is the only record that the skip happened.
|
|
81
|
+
logger.info('duplicate_subscriber_ignored', {
|
|
82
|
+
event: event.name,
|
|
83
|
+
id: event.id,
|
|
84
|
+
subscriber,
|
|
85
|
+
})
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
failures.push(reason)
|
|
84
89
|
}
|
|
85
|
-
failures.push(outcome.reason)
|
|
86
90
|
}
|
|
87
91
|
if (failures.length === 1) throw failures[0]
|
|
88
92
|
if (failures.length > 1) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Supplies `Tx` to participating repositories and resolves after their writes commit. Failure rolls back or discards writes.
|
|
3
3
|
* Nested calls reject with `UnitOfWorkAlreadyActiveError` before opening another boundary.
|
|
4
|
-
* Independent concurrent calls are allowed.
|
|
5
4
|
*/
|
|
6
5
|
export interface ITransaction<Tx> {
|
|
7
6
|
transaction<T>(work: (tx: Tx) => Promise<T>): Promise<T>
|
|
@@ -9,7 +9,7 @@ import type { ITransaction } from './transaction.ts'
|
|
|
9
9
|
/**
|
|
10
10
|
* Commits the processed-message record, repository writes, and emitted events together before resolving.
|
|
11
11
|
* An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.
|
|
12
|
-
* Nested calls reject with `UnitOfWorkAlreadyActiveError
|
|
12
|
+
* Nested calls reject with `UnitOfWorkAlreadyActiveError`.
|
|
13
13
|
*/
|
|
14
14
|
export interface IUnitOfWork<Repos> {
|
|
15
15
|
run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>
|
|
@@ -116,7 +116,7 @@ export class QueueClient<
|
|
|
116
116
|
): Promise<ReadonlyArray<Sent>> {
|
|
117
117
|
const sized = sizeMessages(messages)
|
|
118
118
|
await this.sendSizedBatch(sized.filter((item) => !isOversized(item)))
|
|
119
|
-
return sized.
|
|
119
|
+
return sized.flatMap((item) => (isOversized(item) ? [item.message] : []))
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
async sendToDlq(message: Message): Promise<void> {
|