better-ship 0.3.2 → 0.4.1

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.
Files changed (60) hide show
  1. package/dist/application-rpZoTfzU.js +131 -0
  2. package/dist/application-rpZoTfzU.js.map +1 -0
  3. package/dist/application.d.ts +2 -127
  4. package/dist/application.js +2 -84
  5. package/dist/cloudflare.d.ts +301 -1
  6. package/dist/cloudflare.d.ts.map +1 -0
  7. package/dist/cloudflare.js +169 -0
  8. package/dist/cloudflare.js.map +1 -0
  9. package/dist/{core-D0asUCvq.js → core-MjwyZJ4i.js} +26 -6
  10. package/dist/core-MjwyZJ4i.js.map +1 -0
  11. package/dist/core.d.ts +2 -2
  12. package/dist/core.js +2 -2
  13. package/dist/index-BN_tjxME.d.ts +200 -0
  14. package/dist/index-BN_tjxME.d.ts.map +1 -0
  15. package/dist/{index-eL4vb02m.d.ts → index-ToddX0m3.d.ts} +33 -3
  16. package/dist/index-ToddX0m3.d.ts.map +1 -0
  17. package/dist/postgres.d.ts +289 -1
  18. package/dist/postgres.d.ts.map +1 -0
  19. package/dist/postgres.js +169 -0
  20. package/dist/postgres.js.map +1 -0
  21. package/dist/unit-of-work-context-BLNj72v0.js +48 -0
  22. package/dist/unit-of-work-context-BLNj72v0.js.map +1 -0
  23. package/dist/unit-of-work-context-CbUJGHlL.d.ts +15 -0
  24. package/dist/unit-of-work-context-CbUJGHlL.d.ts.map +1 -0
  25. package/package.json +17 -2
  26. package/src/application/event-collector.ts +7 -0
  27. package/src/application/index.ts +7 -1
  28. package/src/application/message-already-processed.error.ts +33 -0
  29. package/src/application/message-bus.ts +3 -3
  30. package/src/application/message-store.ts +12 -0
  31. package/src/application/outbox-relay.ts +8 -0
  32. package/src/application/transaction.ts +8 -0
  33. package/src/application/unit-of-work.errors.ts +11 -0
  34. package/src/application/unit-of-work.ts +54 -0
  35. package/src/core/database.error.ts +37 -0
  36. package/src/core/index.ts +1 -0
  37. package/src/core/logger.ts +13 -4
  38. package/src/infrastructure/cloudflare/base-d1-repository.ts +40 -0
  39. package/src/infrastructure/cloudflare/classify-d1-error.ts +82 -0
  40. package/src/infrastructure/cloudflare/d1-batch-transaction.ts +27 -0
  41. package/src/infrastructure/cloudflare/d1-database-execution.ts +13 -0
  42. package/src/infrastructure/cloudflare/d1-database-types.ts +20 -0
  43. package/src/infrastructure/cloudflare/d1-message-store.ts +41 -0
  44. package/src/infrastructure/cloudflare/d1-schema.ts +43 -0
  45. package/src/infrastructure/cloudflare/index.ts +9 -0
  46. package/src/infrastructure/drizzle-schema.ts +6 -0
  47. package/src/infrastructure/postgres/base-postgres-repository.ts +30 -0
  48. package/src/infrastructure/postgres/classify-postgres-error.ts +90 -0
  49. package/src/infrastructure/postgres/index.ts +15 -0
  50. package/src/infrastructure/postgres/postgres-database-execution.ts +13 -0
  51. package/src/infrastructure/postgres/postgres-database-types.ts +27 -0
  52. package/src/infrastructure/postgres/postgres-message-store.ts +32 -0
  53. package/src/infrastructure/postgres/postgres-schema.ts +33 -0
  54. package/src/infrastructure/postgres/postgres-transaction.ts +22 -0
  55. package/src/infrastructure/unit-of-work-context.ts +55 -0
  56. package/dist/application.d.ts.map +0 -1
  57. package/dist/application.js.map +0 -1
  58. package/dist/core-D0asUCvq.js.map +0 -1
  59. package/dist/index-eL4vb02m.d.ts.map +0 -1
  60. package/src/application/message-bus.errors.ts +0 -16
@@ -1 +1,289 @@
1
- export {}
1
+ import { C as Event, T as MessageId, s as ITransaction, v as IMessageStore } from "./index-BN_tjxME.js";
2
+ import { E as DatabaseFailure, T as DatabaseError } from "./index-ToddX0m3.js";
3
+ import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-CbUJGHlL.js";
4
+ import { ExtractTablesWithRelations } from "drizzle-orm";
5
+ import { PostgresJsDatabase, PostgresJsTransaction, drizzle } from "drizzle-orm/postgres-js";
6
+ //#region src/infrastructure/postgres/postgres-database-types.d.ts
7
+ /** The postgres.js client passed to Drizzle, including clients configured through Hyperdrive. */
8
+ type PostgresConnection = ReturnType<typeof drizzle>['$client'];
9
+ /** The application's Drizzle Postgres database, preserving its schema and relational query types. */
10
+ type PostgresDb<Schema extends DrizzleSchema> = PostgresJsDatabase<Schema>;
11
+ /** Exposes Drizzle query methods without insert, update, delete, transaction, or raw execution methods. */
12
+ type PostgresReadDb<Schema extends DrizzleSchema> = Pick<PostgresDb<Schema>, 'select' | 'selectDistinct' | 'selectDistinctOn' | 'query'>;
13
+ /** The native Drizzle transaction with the application's full schema and derived relation types. */
14
+ type PostgresDbTransaction<Schema extends DrizzleSchema> = PostgresJsTransaction<Schema, ExtractTablesWithRelations<Schema>>;
15
+ /** The database transaction supplied to participating repositories by the Postgres adapter. */
16
+ type PostgresTx<Schema extends DrizzleSchema> = {
17
+ readonly db: PostgresDbTransaction<Schema>;
18
+ };
19
+ //#endregion
20
+ //#region src/infrastructure/postgres/base-postgres-repository.d.ts
21
+ /** A domain aggregate that accumulates events for the unit of work to drain. */
22
+ interface AggregateEvents {
23
+ collectEvents(): ReadonlyArray<Event>;
24
+ clearEvents(): void;
25
+ }
26
+ /** Shared plumbing for a repository that writes inside a Postgres unit of work. */
27
+ export declare abstract class BasePostgresRepository<Schema extends DrizzleSchema> {
28
+ protected readonly db: PostgresDbTransaction<Schema>;
29
+ constructor(db: PostgresDbTransaction<Schema>);
30
+ /** Execute one read with database error conversion and no automatic retries. */
31
+ protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T>;
32
+ /** Drain an aggregate's pending events into the active boundary. */
33
+ protected dispatchEvents(aggregate: AggregateEvents): void;
34
+ }
35
+ //#endregion
36
+ //#region src/infrastructure/postgres/postgres-message-store.d.ts
37
+ /** Processed messages and outbox events share the active Postgres transaction; the insert result establishes the claim. */
38
+ export declare class PostgresMessageStore<Schema extends DrizzleSchema> implements IMessageStore {
39
+ private readonly db;
40
+ constructor(db: PostgresDbTransaction<Schema>);
41
+ claim(messageId: MessageId): Promise<void>;
42
+ persistEvents(events: ReadonlyArray<Event>): Promise<void>;
43
+ }
44
+ //#endregion
45
+ //#region src/infrastructure/postgres/postgres-schema.d.ts
46
+ /** One row per committed message. The primary key is the idempotency claim. */
47
+ export declare const processedMessages: import("drizzle-orm/pg-core").PgTableWithColumns<{
48
+ name: "processed_messages";
49
+ schema: undefined;
50
+ columns: {
51
+ messageId: import("drizzle-orm/pg-core").PgColumn<{
52
+ name: "message_id";
53
+ tableName: "processed_messages";
54
+ dataType: "string";
55
+ columnType: "PgText";
56
+ data: string;
57
+ driverParam: string;
58
+ notNull: true;
59
+ hasDefault: false;
60
+ isPrimaryKey: true;
61
+ isAutoincrement: false;
62
+ hasRuntimeDefault: false;
63
+ enumValues: [string, ...string[]];
64
+ baseColumn: never;
65
+ identity: undefined;
66
+ generated: undefined;
67
+ }, {}, {}>;
68
+ processedAt: import("drizzle-orm/pg-core").PgColumn<{
69
+ name: "processed_at";
70
+ tableName: "processed_messages";
71
+ dataType: "date";
72
+ columnType: "PgTimestamp";
73
+ data: Date;
74
+ driverParam: string;
75
+ notNull: true;
76
+ hasDefault: true;
77
+ isPrimaryKey: false;
78
+ isAutoincrement: false;
79
+ hasRuntimeDefault: false;
80
+ enumValues: undefined;
81
+ baseColumn: never;
82
+ identity: undefined;
83
+ generated: undefined;
84
+ }, {}, {}>;
85
+ };
86
+ dialect: 'pg';
87
+ }>;
88
+ /** Events committed with their state change, waiting for the relay to deliver them. */
89
+ export declare const messageOutbox: import("drizzle-orm/pg-core").PgTableWithColumns<{
90
+ name: "message_outbox";
91
+ schema: undefined;
92
+ columns: {
93
+ id: import("drizzle-orm/pg-core").PgColumn<{
94
+ name: "id";
95
+ tableName: "message_outbox";
96
+ dataType: "string";
97
+ columnType: "PgText";
98
+ data: string;
99
+ driverParam: string;
100
+ notNull: true;
101
+ hasDefault: false;
102
+ isPrimaryKey: true;
103
+ isAutoincrement: false;
104
+ hasRuntimeDefault: false;
105
+ enumValues: [string, ...string[]];
106
+ baseColumn: never;
107
+ identity: undefined;
108
+ generated: undefined;
109
+ }, {}, {}>;
110
+ payload: import("drizzle-orm/pg-core").PgColumn<{
111
+ name: "payload";
112
+ tableName: "message_outbox";
113
+ dataType: "json";
114
+ columnType: "PgJsonb";
115
+ data: Event;
116
+ driverParam: unknown;
117
+ notNull: true;
118
+ hasDefault: false;
119
+ isPrimaryKey: false;
120
+ isAutoincrement: false;
121
+ hasRuntimeDefault: false;
122
+ enumValues: undefined;
123
+ baseColumn: never;
124
+ identity: undefined;
125
+ generated: undefined;
126
+ }, {}, {
127
+ $type: Event;
128
+ }>;
129
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
130
+ name: "created_at";
131
+ tableName: "message_outbox";
132
+ dataType: "date";
133
+ columnType: "PgTimestamp";
134
+ data: Date;
135
+ driverParam: string;
136
+ notNull: true;
137
+ hasDefault: true;
138
+ isPrimaryKey: false;
139
+ isAutoincrement: false;
140
+ hasRuntimeDefault: false;
141
+ enumValues: undefined;
142
+ baseColumn: never;
143
+ identity: undefined;
144
+ generated: undefined;
145
+ }, {}, {}>;
146
+ availableAt: import("drizzle-orm/pg-core").PgColumn<{
147
+ name: "available_at";
148
+ tableName: "message_outbox";
149
+ dataType: "date";
150
+ columnType: "PgTimestamp";
151
+ data: Date;
152
+ driverParam: string;
153
+ notNull: true;
154
+ hasDefault: true;
155
+ isPrimaryKey: false;
156
+ isAutoincrement: false;
157
+ hasRuntimeDefault: false;
158
+ enumValues: undefined;
159
+ baseColumn: never;
160
+ identity: undefined;
161
+ generated: undefined;
162
+ }, {}, {}>;
163
+ attempts: import("drizzle-orm/pg-core").PgColumn<{
164
+ name: "attempts";
165
+ tableName: "message_outbox";
166
+ dataType: "number";
167
+ columnType: "PgInteger";
168
+ data: number;
169
+ driverParam: string | number;
170
+ notNull: true;
171
+ hasDefault: true;
172
+ isPrimaryKey: false;
173
+ isAutoincrement: false;
174
+ hasRuntimeDefault: false;
175
+ enumValues: undefined;
176
+ baseColumn: never;
177
+ identity: undefined;
178
+ generated: undefined;
179
+ }, {}, {}>;
180
+ claimToken: import("drizzle-orm/pg-core").PgColumn<{
181
+ name: "claim_token";
182
+ tableName: "message_outbox";
183
+ dataType: "string";
184
+ columnType: "PgText";
185
+ data: string;
186
+ driverParam: string;
187
+ notNull: false;
188
+ hasDefault: false;
189
+ isPrimaryKey: false;
190
+ isAutoincrement: false;
191
+ hasRuntimeDefault: false;
192
+ enumValues: [string, ...string[]];
193
+ baseColumn: never;
194
+ identity: undefined;
195
+ generated: undefined;
196
+ }, {}, {}>;
197
+ claimExpiresAt: import("drizzle-orm/pg-core").PgColumn<{
198
+ name: "claim_expires_at";
199
+ tableName: "message_outbox";
200
+ dataType: "date";
201
+ columnType: "PgTimestamp";
202
+ data: Date;
203
+ driverParam: string;
204
+ notNull: false;
205
+ hasDefault: false;
206
+ isPrimaryKey: false;
207
+ isAutoincrement: false;
208
+ hasRuntimeDefault: false;
209
+ enumValues: undefined;
210
+ baseColumn: never;
211
+ identity: undefined;
212
+ generated: undefined;
213
+ }, {}, {}>;
214
+ publishedAt: import("drizzle-orm/pg-core").PgColumn<{
215
+ name: "published_at";
216
+ tableName: "message_outbox";
217
+ dataType: "date";
218
+ columnType: "PgTimestamp";
219
+ data: Date;
220
+ driverParam: string;
221
+ notNull: false;
222
+ hasDefault: false;
223
+ isPrimaryKey: false;
224
+ isAutoincrement: false;
225
+ hasRuntimeDefault: false;
226
+ enumValues: undefined;
227
+ baseColumn: never;
228
+ identity: undefined;
229
+ generated: undefined;
230
+ }, {}, {}>;
231
+ failedAt: import("drizzle-orm/pg-core").PgColumn<{
232
+ name: "failed_at";
233
+ tableName: "message_outbox";
234
+ dataType: "date";
235
+ columnType: "PgTimestamp";
236
+ data: Date;
237
+ driverParam: string;
238
+ notNull: false;
239
+ hasDefault: false;
240
+ isPrimaryKey: false;
241
+ isAutoincrement: false;
242
+ hasRuntimeDefault: false;
243
+ enumValues: undefined;
244
+ baseColumn: never;
245
+ identity: undefined;
246
+ generated: undefined;
247
+ }, {}, {}>;
248
+ lastError: import("drizzle-orm/pg-core").PgColumn<{
249
+ name: "last_error";
250
+ tableName: "message_outbox";
251
+ dataType: "string";
252
+ columnType: "PgText";
253
+ data: string;
254
+ driverParam: string;
255
+ notNull: false;
256
+ hasDefault: false;
257
+ isPrimaryKey: false;
258
+ isAutoincrement: false;
259
+ hasRuntimeDefault: false;
260
+ enumValues: [string, ...string[]];
261
+ baseColumn: never;
262
+ identity: undefined;
263
+ generated: undefined;
264
+ }, {}, {}>;
265
+ };
266
+ dialect: 'pg';
267
+ }>;
268
+ //#endregion
269
+ //#region src/infrastructure/postgres/postgres-transaction.d.ts
270
+ /**
271
+ * The Postgres transaction boundary uses native Drizzle transactions through postgres.js.
272
+ * Drizzle commits when `work` returns and rolls back when it throws.
273
+ */
274
+ export declare class PostgresTransaction<Schema extends DrizzleSchema> implements ITransaction<PostgresTx<Schema>> {
275
+ private readonly db;
276
+ constructor(db: PostgresDb<Schema>);
277
+ transaction<T>(work: (tx: PostgresTx<Schema>) => Promise<T>): Promise<T>;
278
+ }
279
+ //#endregion
280
+ //#region src/infrastructure/postgres/postgres-database-execution.d.ts
281
+ /** Execute once without retries; convert recognized Postgres failures and preserve other exceptions unchanged. */
282
+ export declare function executeDatabase<T>(operation: string, work: () => PromiseLike<T>): Promise<T>;
283
+ //#endregion
284
+ //#region src/infrastructure/postgres/classify-postgres-error.d.ts
285
+ /** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */
286
+ export declare function classifyPostgresError(cause: unknown, operation: string): DatabaseError | undefined;
287
+ //#endregion
288
+ export { type AggregateEvents, DatabaseError, type DatabaseFailure, type PostgresConnection, type PostgresDb, type PostgresDbTransaction, type PostgresReadDb, type PostgresTx, eventCollector };
289
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +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-database-execution.ts","../src/infrastructure/postgres/classify-postgres-error.ts"],"mappings":";;;;;;;KAMY,qBAAqB,kBAAkB;;KAGvC,WAAW,eAAe,iBAAiB,mBAAmB;;KAG9D,eAAe,eAAe,iBAAiB,KACzD,WAAW;;KAKD,sBAAsB,eAAe,iBAAiB,sBAChE,QACA,2BAA2B;;KAIjB,WAAW,eAAe;WAC3B,IAAI,sBAAsB;;;;;UCjBpB;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;;;;;qBCXzB,qBAAqB,eAAe,0BAA0B;mBAC5C;EAA7B,YAA6B,IAAI,sBAAsB;EAEjD,MAAM,WAAW,YAAY;EAU7B,cAAc,QAAQ,cAAc,SAAS;;;;;qBCnBxC,iDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAMA,6CAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCDb,oBAAoB,eAAe,0BAA0B,aACxE,WAAW;mBAEkB;EAA7B,YAA6B,IAAI,WAAW;EAE5C,YAAY,GAAG,OAAO,IAAI,WAAW,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ;;;;wBC2BK,sBACd,gBACA,oBACC"}
package/dist/postgres.js CHANGED
@@ -0,0 +1,169 @@
1
+ import { m as AppError, p as DatabaseError } from "./core-MjwyZJ4i.js";
2
+ import { i as MessageAlreadyProcessedError } from "./application-rpZoTfzU.js";
3
+ import { n as eventCollector, r as runWithUnitOfWorkContext } from "./unit-of-work-context-BLNj72v0.js";
4
+ import { DrizzleQueryError } from "drizzle-orm/errors";
5
+ import { isNull } from "drizzle-orm";
6
+ import { index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
7
+
8
+ //#region src/infrastructure/postgres/classify-postgres-error.ts
9
+ const TRANSIENT_CODES = /* @__PURE__ */ new Set([
10
+ "40001",
11
+ "40P01",
12
+ "08000",
13
+ "08001",
14
+ "08003",
15
+ "08006",
16
+ "57P01",
17
+ "57P02",
18
+ "57P03",
19
+ "CONNECT_TIMEOUT",
20
+ "CONNECTION_CLOSED",
21
+ "ECONNRESET",
22
+ "ECONNREFUSED",
23
+ "EPIPE",
24
+ "ETIMEDOUT",
25
+ "EAI_AGAIN"
26
+ ]);
27
+ const TERMINAL_CODES = /* @__PURE__ */ new Set([
28
+ "57014",
29
+ "CONNECTION_ENDED",
30
+ "CONNECTION_DESTROYED",
31
+ "ECANCELED"
32
+ ]);
33
+ const DRIVER_CODES = /* @__PURE__ */ new Set([
34
+ "CONNECT_TIMEOUT",
35
+ "CONNECTION_CLOSED",
36
+ "CONNECTION_ENDED",
37
+ "CONNECTION_DESTROYED"
38
+ ]);
39
+ const TERMINAL_CLASSES = /* @__PURE__ */ new Set([
40
+ "0A",
41
+ "22",
42
+ "23",
43
+ "25",
44
+ "28",
45
+ "3D",
46
+ "3F",
47
+ "42",
48
+ "54"
49
+ ]);
50
+ /** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */
51
+ function classifyPostgresError(cause, operation) {
52
+ let error = cause;
53
+ const seen = /* @__PURE__ */ new Set();
54
+ while (error instanceof DrizzleQueryError && !seen.has(error)) {
55
+ seen.add(error);
56
+ error = error.cause;
57
+ }
58
+ if (error instanceof AppError || error instanceof Error && error.name === "AbortError") return void 0;
59
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
60
+ const serverError = error instanceof Error && error.name === "PostgresError" && code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
61
+ const driverError = code !== void 0 && DRIVER_CODES.has(code);
62
+ if (!(cause instanceof DrizzleQueryError) && !serverError && !driverError) return void 0;
63
+ return new DatabaseError(operation, classificationOf(code), {
64
+ database: "postgres",
65
+ code,
66
+ table_name: error instanceof Error && "table_name" in error && typeof error.table_name === "string" ? error.table_name : void 0,
67
+ column_name: error instanceof Error && "column_name" in error && typeof error.column_name === "string" ? error.column_name : void 0,
68
+ constraint_name: error instanceof Error && "constraint_name" in error && typeof error.constraint_name === "string" ? error.constraint_name : void 0
69
+ }, { cause });
70
+ }
71
+ function classificationOf(code) {
72
+ if (code === void 0) return "unknown";
73
+ if (TRANSIENT_CODES.has(code)) return "transient";
74
+ if (TERMINAL_CODES.has(code) || TERMINAL_CLASSES.has(code.slice(0, 2))) return "terminal";
75
+ return "unknown";
76
+ }
77
+
78
+ //#endregion
79
+ //#region src/infrastructure/postgres/postgres-database-execution.ts
80
+ /** Execute once without retries; convert recognized Postgres failures and preserve other exceptions unchanged. */
81
+ async function executeDatabase(operation, work) {
82
+ try {
83
+ return await work();
84
+ } catch (cause) {
85
+ throw classifyPostgresError(cause, operation) ?? cause;
86
+ }
87
+ }
88
+
89
+ //#endregion
90
+ //#region src/infrastructure/postgres/base-postgres-repository.ts
91
+ /** Shared plumbing for a repository that writes inside a Postgres unit of work. */
92
+ var BasePostgresRepository = class {
93
+ db;
94
+ constructor(db) {
95
+ this.db = db;
96
+ }
97
+ /** Execute one read with database error conversion and no automatic retries. */
98
+ read(operation, query) {
99
+ return executeDatabase(operation, query);
100
+ }
101
+ /** Drain an aggregate's pending events into the active boundary. */
102
+ dispatchEvents(aggregate) {
103
+ const events = aggregate.collectEvents();
104
+ if (events.length === 0) return;
105
+ for (const event of events) eventCollector.emit(event);
106
+ aggregate.clearEvents();
107
+ }
108
+ };
109
+
110
+ //#endregion
111
+ //#region src/infrastructure/postgres/postgres-schema.ts
112
+ /** One row per committed message. The primary key is the idempotency claim. */
113
+ const processedMessages = pgTable("processed_messages", {
114
+ messageId: text("message_id").primaryKey(),
115
+ processedAt: timestamp("processed_at", { withTimezone: true }).notNull().defaultNow()
116
+ });
117
+ /** Events committed with their state change, waiting for the relay to deliver them. */
118
+ const messageOutbox = pgTable("message_outbox", {
119
+ id: text("id").primaryKey(),
120
+ payload: jsonb("payload").$type().notNull(),
121
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
122
+ availableAt: timestamp("available_at", { withTimezone: true }).notNull().defaultNow(),
123
+ attempts: integer("attempts").notNull().default(0),
124
+ claimToken: text("claim_token"),
125
+ claimExpiresAt: timestamp("claim_expires_at", { withTimezone: true }),
126
+ publishedAt: timestamp("published_at", { withTimezone: true }),
127
+ failedAt: timestamp("failed_at", { withTimezone: true }),
128
+ lastError: text("last_error")
129
+ }, (table) => [index("idx_message_outbox_due").on(table.availableAt, table.createdAt).where(isNull(table.publishedAt)), index("idx_message_outbox_claim_token").on(table.claimToken)]);
130
+
131
+ //#endregion
132
+ //#region src/infrastructure/postgres/postgres-message-store.ts
133
+ /** Processed messages and outbox events share the active Postgres transaction; the insert result establishes the claim. */
134
+ var PostgresMessageStore = class {
135
+ db;
136
+ constructor(db) {
137
+ this.db = db;
138
+ }
139
+ async claim(messageId) {
140
+ if ((await this.db.insert(processedMessages).values({ messageId }).onConflictDoNothing().returning({ messageId: processedMessages.messageId })).length === 0) throw new MessageAlreadyProcessedError(messageId);
141
+ }
142
+ async persistEvents(events) {
143
+ if (events.length === 0) return;
144
+ await this.db.insert(messageOutbox).values(events.map((event) => ({
145
+ id: event.id,
146
+ payload: event
147
+ })));
148
+ }
149
+ };
150
+
151
+ //#endregion
152
+ //#region src/infrastructure/postgres/postgres-transaction.ts
153
+ /**
154
+ * The Postgres transaction boundary uses native Drizzle transactions through postgres.js.
155
+ * Drizzle commits when `work` returns and rolls back when it throws.
156
+ */
157
+ var PostgresTransaction = class {
158
+ db;
159
+ constructor(db) {
160
+ this.db = db;
161
+ }
162
+ transaction(work) {
163
+ return runWithUnitOfWorkContext("postgres", () => executeDatabase("unit of work transaction", () => this.db.transaction((db) => work({ db }))));
164
+ }
165
+ };
166
+
167
+ //#endregion
168
+ export { BasePostgresRepository, DatabaseError, PostgresMessageStore, PostgresTransaction, classifyPostgresError, eventCollector, executeDatabase, messageOutbox, processedMessages };
169
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +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"],"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 { classifyPostgresError } from './classify-postgres-error.ts'\n\n/** Execute once without retries; convert recognized Postgres failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyPostgresError(cause, operation) ?? cause\n }\n}\n","import type { Event } from '@/application'\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<Event>\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 { Event } from '@/application'\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<Event>().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 {\n MessageAlreadyProcessedError,\n type Event,\n type IMessageStore,\n type MessageId,\n} from '@/application'\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<Event>): 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"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { t as UnitOfWorkAlreadyActiveError } from "./application-rpZoTfzU.js";
2
+ import { panic } from "better-result";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+
5
+ //#region src/infrastructure/unit-of-work-context.ts
6
+ const storage = new AsyncLocalStorage();
7
+ /** Reject nested transactions before database work starts; preserve independent concurrent execution. */
8
+ async function runWithUnitOfWorkContext(database, work) {
9
+ if (storage.getStore() !== void 0) throw new UnitOfWorkAlreadyActiveError();
10
+ const context = database === "d1" ? {
11
+ database,
12
+ events: [],
13
+ statements: []
14
+ } : {
15
+ database,
16
+ events: []
17
+ };
18
+ return storage.run(context, work);
19
+ }
20
+ /** Inject this shared scope into the unit of work and expose its emitter to application handlers. */
21
+ const eventCollector = {
22
+ emit(event) {
23
+ currentContext().events.push(event);
24
+ },
25
+ collect() {
26
+ return [...currentContext().events];
27
+ }
28
+ };
29
+ /** Queue one native D1 statement for the active batch; other contexts cannot accept D1 writes. */
30
+ function stageD1Statement(statement) {
31
+ d1Context().statements.push(statement);
32
+ }
33
+ /** Read the active D1 batch in staging order for commit. */
34
+ function collectD1Statements() {
35
+ return [...d1Context().statements];
36
+ }
37
+ function d1Context() {
38
+ const context = currentContext();
39
+ if (context.database !== "d1") return panic("D1 statements require a D1 unit of work");
40
+ return context;
41
+ }
42
+ function currentContext() {
43
+ return storage.getStore() ?? panic("operation requires an active unit of work");
44
+ }
45
+
46
+ //#endregion
47
+ export { stageD1Statement as i, eventCollector as n, runWithUnitOfWorkContext as r, collectD1Statements as t };
48
+ //# sourceMappingURL=unit-of-work-context-BLNj72v0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit-of-work-context-BLNj72v0.js","names":[],"sources":["../src/infrastructure/unit-of-work-context.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\n\nimport { panic } from 'better-result'\nimport type { BatchItem } from 'drizzle-orm/batch'\n\nimport { UnitOfWorkAlreadyActiveError, type Event, type IEventCollector } from '@/application'\n\ntype UnitOfWorkContext = { readonly events: Event[] } & (\n | { readonly database: 'd1'; readonly statements: BatchItem<'sqlite'>[] }\n | { readonly database: 'postgres' }\n)\n\n// One async scope isolates events and D1 statements and rejects nesting across both adapters.\nconst storage = new AsyncLocalStorage<UnitOfWorkContext>()\n\n/** Reject nested transactions before database work starts; preserve independent concurrent execution. */\nexport async function runWithUnitOfWorkContext<T>(\n database: UnitOfWorkContext['database'],\n work: () => Promise<T>,\n): Promise<T> {\n if (storage.getStore() !== undefined) throw new UnitOfWorkAlreadyActiveError()\n const context: UnitOfWorkContext =\n database === 'd1' ? { database, events: [], statements: [] } : { database, events: [] }\n return storage.run(context, work)\n}\n\n/** Inject this shared scope into the unit of work and expose its emitter to application handlers. */\nexport const eventCollector: IEventCollector = {\n emit(event) {\n currentContext().events.push(event)\n },\n collect() {\n return [...currentContext().events]\n },\n}\n\n/** Queue one native D1 statement for the active batch; other contexts cannot accept D1 writes. */\nexport function stageD1Statement(statement: BatchItem<'sqlite'>): void {\n d1Context().statements.push(statement)\n}\n\n/** Read the active D1 batch in staging order for commit. */\nexport function collectD1Statements(): ReadonlyArray<BatchItem<'sqlite'>> {\n return [...d1Context().statements]\n}\n\nfunction d1Context(): Extract<UnitOfWorkContext, { database: 'd1' }> {\n const context = currentContext()\n if (context.database !== 'd1') return panic('D1 statements require a D1 unit of work')\n return context\n}\n\nfunction currentContext(): UnitOfWorkContext {\n return storage.getStore() ?? panic('operation requires an active unit of work')\n}\n"],"mappings":";;;;;AAaA,MAAM,UAAU,IAAI,kBAAqC;;AAGzD,eAAsB,yBACpB,UACA,MACY;CACZ,IAAI,QAAQ,SAAS,MAAM,QAAW,MAAM,IAAI,6BAA6B;CAC7E,MAAM,UACJ,aAAa,OAAO;EAAE;EAAU,QAAQ,CAAC;EAAG,YAAY,CAAC;CAAE,IAAI;EAAE;EAAU,QAAQ,CAAC;CAAE;CACxF,OAAO,QAAQ,IAAI,SAAS,IAAI;AAClC;;AAGA,MAAa,iBAAkC;CAC7C,KAAK,OAAO;EACV,eAAe,CAAC,CAAC,OAAO,KAAK,KAAK;CACpC;CACA,UAAU;EACR,OAAO,CAAC,GAAG,eAAe,CAAC,CAAC,MAAM;CACpC;AACF;;AAGA,SAAgB,iBAAiB,WAAsC;CACrE,UAAU,CAAC,CAAC,WAAW,KAAK,SAAS;AACvC;;AAGA,SAAgB,sBAA0D;CACxE,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,UAAU;AACnC;AAEA,SAAS,YAA4D;CACnE,MAAM,UAAU,eAAe;CAC/B,IAAI,QAAQ,aAAa,MAAM,OAAO,MAAM,yCAAyC;CACrF,OAAO;AACT;AAEA,SAAS,iBAAoC;CAC3C,OAAO,QAAQ,SAAS,KAAK,MAAM,2CAA2C;AAChF"}
@@ -0,0 +1,15 @@
1
+ import { o as IEventCollector } from "./index-BN_tjxME.js";
2
+ import { BatchItem } from "drizzle-orm/batch";
3
+ //#region src/infrastructure/drizzle-schema.d.ts
4
+ /**
5
+ * The app's drizzle schema: the tables it declared, keyed by name. This is drizzle's own
6
+ * constraint on every database and transaction type, restated once so the adapters name it.
7
+ */
8
+ type DrizzleSchema = Record<string, unknown>;
9
+ //#endregion
10
+ //#region src/infrastructure/unit-of-work-context.d.ts
11
+ /** Inject this shared scope into the unit of work and expose its emitter to application handlers. */
12
+ declare const eventCollector: IEventCollector;
13
+ //#endregion
14
+ export { DrizzleSchema as n, eventCollector as t };
15
+ //# sourceMappingURL=unit-of-work-context-CbUJGHlL.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit-of-work-context-CbUJGHlL.d.ts","names":[],"sources":["../src/infrastructure/drizzle-schema.ts","../src/infrastructure/unit-of-work-context.ts"],"mappings":";;;;;;;KAKY,gBAAgB;;;;cCsBf,gBAAgB"}
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.3.2",
4
+ "version": "0.4.1",
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": {
@@ -30,11 +30,24 @@
30
30
  "better-result": "3.0.1"
31
31
  },
32
32
  "devDependencies": {
33
+ "@cloudflare/vitest-plugin": "1.1.4",
33
34
  "@cloudflare/workers-types": "5.20260905.1",
35
+ "@types/node": "26.4.1",
36
+ "drizzle-kit": "0.31.10",
37
+ "drizzle-orm": "0.45.2",
38
+ "postgres": "3.4.9",
34
39
  "tsdown": "0.23.0",
35
40
  "typescript": "7.0.2",
36
41
  "vitest": "4.1.11"
37
42
  },
43
+ "peerDependencies": {
44
+ "drizzle-orm": "0.45.2"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "drizzle-orm": {
48
+ "optional": true
49
+ }
50
+ },
38
51
  "engines": {
39
52
  "node": ">=22"
40
53
  },
@@ -44,6 +57,8 @@
44
57
  "typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json",
45
58
  "lint": "oxlint . --quiet",
46
59
  "lint:fix": "oxlint . --fix --quiet",
47
- "test": "vitest run --passWithNoTests"
60
+ "test": "vitest run",
61
+ "db:generate:d1": "drizzle-kit generate --config drizzle.d1.config.ts",
62
+ "db:generate:postgres": "drizzle-kit generate --config drizzle.postgres.config.ts"
48
63
  }
49
64
  }
@@ -0,0 +1,7 @@
1
+ import type { Event } from './messages.ts'
2
+
3
+ /** Records events in the current unit of work; the unit of work collects a snapshot for outbox persistence. */
4
+ export interface IEventCollector {
5
+ emit(event: Event): void
6
+ collect(): ReadonlyArray<Event>
7
+ }
@@ -1,7 +1,8 @@
1
- export { DuplicateMessageError } from './message-bus.errors.ts'
2
1
  export type { CommandHandler, EventHandler, QueryHandler } from './handlers.ts'
2
+ export type { IMessageStore } from './message-store.ts'
3
3
  export type { Command, Event, Message, MessageId, Query } from './messages.ts'
4
4
  export { MessageBus, type IMessageBus } from './message-bus.ts'
5
+ export type { IOutboxRelay } from './outbox-relay.ts'
5
6
  export type {
6
7
  CommandRegistry,
7
8
  EventRegistry,
@@ -11,3 +12,8 @@ export type {
11
12
  QueryRegistry,
12
13
  RegistryMessage,
13
14
  } from './registry.ts'
15
+ export type { ITransaction } from './transaction.ts'
16
+ export { UnitOfWork, type IUnitOfWork, type UnitOfWorkDeps } from './unit-of-work.ts'
17
+ export type { IEventCollector } from './event-collector.ts'
18
+ export { MessageAlreadyProcessedError } from './message-already-processed.error.ts'
19
+ export { UnitOfWorkAlreadyActiveError } from './unit-of-work.errors.ts'