better-ship 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/application-BpSwCKJ5.js +131 -0
  2. package/dist/application-BpSwCKJ5.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-D3G-2gIx.js} +18 -2
  10. package/dist/core-D3G-2gIx.js.map +1 -0
  11. package/dist/core.d.ts +2 -2
  12. package/dist/core.js +2 -2
  13. package/dist/{index-eL4vb02m.d.ts → index-CT1yZd4D.d.ts} +31 -2
  14. package/dist/index-CT1yZd4D.d.ts.map +1 -0
  15. package/dist/index-CkaFqbbV.d.ts +200 -0
  16. package/dist/index-CkaFqbbV.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-CILhzKWb.js +48 -0
  22. package/dist/unit-of-work-context-CILhzKWb.js.map +1 -0
  23. package/dist/unit-of-work-context-DOkIB7HK.d.ts +15 -0
  24. package/dist/unit-of-work-context-DOkIB7HK.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/infrastructure/cloudflare/base-d1-repository.ts +40 -0
  38. package/src/infrastructure/cloudflare/classify-d1-error.ts +82 -0
  39. package/src/infrastructure/cloudflare/d1-batch-transaction.ts +27 -0
  40. package/src/infrastructure/cloudflare/d1-database-execution.ts +13 -0
  41. package/src/infrastructure/cloudflare/d1-database-types.ts +20 -0
  42. package/src/infrastructure/cloudflare/d1-message-store.ts +41 -0
  43. package/src/infrastructure/cloudflare/d1-schema.ts +43 -0
  44. package/src/infrastructure/cloudflare/index.ts +9 -0
  45. package/src/infrastructure/drizzle-schema.ts +6 -0
  46. package/src/infrastructure/postgres/base-postgres-repository.ts +30 -0
  47. package/src/infrastructure/postgres/classify-postgres-error.ts +90 -0
  48. package/src/infrastructure/postgres/index.ts +15 -0
  49. package/src/infrastructure/postgres/postgres-database-execution.ts +13 -0
  50. package/src/infrastructure/postgres/postgres-database-types.ts +27 -0
  51. package/src/infrastructure/postgres/postgres-message-store.ts +32 -0
  52. package/src/infrastructure/postgres/postgres-schema.ts +33 -0
  53. package/src/infrastructure/postgres/postgres-transaction.ts +22 -0
  54. package/src/infrastructure/unit-of-work-context.ts +55 -0
  55. package/dist/application.d.ts.map +0 -1
  56. package/dist/application.js.map +0 -1
  57. package/dist/core-D0asUCvq.js.map +0 -1
  58. package/dist/index-eL4vb02m.d.ts.map +0 -1
  59. package/src/application/message-bus.errors.ts +0 -16
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare.d.ts","names":[],"sources":["../src/infrastructure/cloudflare/d1-database-types.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-database-execution.ts"],"mappings":";;;;;;;KAKY,eAAe;;KAGf,KAAK,eAAe,iBAAiB,kBAAkB;;KAGvD,SAAS,eAAe,iBAAiB,KACnD,KAAK;;KAKK,KAAK,eAAe;WACrB,IAAI,KAAK;;;;;UCRH;EACf,iBAAiB,cAAc;EAC/B;;;;;;8BAOoB,iBAAiB,eAAe;qBACrB,IAAI,KAAK;EAAxC,YAA+B,IAAI,KAAK;;YAG9B,MAAM,WAAW;;YAKjB,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;;;;wBCVtB,gBAAgB,gBAAgB,qBAAyB;;;;qBCT5D,eAAe,eAAe,uBACjC,iBAAiB,mBACd;EAEL,MAAM,WAAW,YAAY;EAc7B,cAAc,QAAQ,cAAc,SAAS;;;;;qBC1BxC,qDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAYA,iDAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCPb,mBAAmB,eAAe,0BAA0B,aACvE,KAAK;mBAEwB;EAA7B,YAA6B,IAAI,KAAK;EAEhC,YAAY,GAAG,OAAO,IAAI,KAAK,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ"}
@@ -0,0 +1,169 @@
1
+ import { m as AppError, p as DatabaseError } from "./core-D3G-2gIx.js";
2
+ import { i as MessageAlreadyProcessedError } from "./application-BpSwCKJ5.js";
3
+ import { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-CILhzKWb.js";
4
+ import { DrizzleQueryError } from "drizzle-orm/errors";
5
+ import { eq, sql } from "drizzle-orm";
6
+ import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
7
+
8
+ //#region src/infrastructure/cloudflare/classify-d1-error.ts
9
+ const KIND_BY_CODE = /* @__PURE__ */ new Map([
10
+ ["SQLITE_CONSTRAINT_PRIMARYKEY", "unique"],
11
+ ["SQLITE_CONSTRAINT_UNIQUE", "unique"],
12
+ ["SQLITE_CONSTRAINT_NOTNULL", "not_null"],
13
+ ["SQLITE_CONSTRAINT_FOREIGNKEY", "foreign_key"],
14
+ ["SQLITE_CONSTRAINT_CHECK", "check"]
15
+ ]);
16
+ const KIND_BY_PROSE = [
17
+ ["UNIQUE constraint failed", "unique"],
18
+ ["NOT NULL constraint failed", "not_null"],
19
+ ["FOREIGN KEY constraint failed", "foreign_key"],
20
+ ["CHECK constraint failed", "check"]
21
+ ];
22
+ /**
23
+ * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.
24
+ * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.
25
+ */
26
+ function classifyD1Error(cause, operation = "D1 query") {
27
+ if (cause instanceof AppError || cause instanceof Error && cause.name === "AbortError") return void 0;
28
+ const message = messagesOf(cause);
29
+ const kind = kindOf(message);
30
+ const classification = kind === void 0 ? classificationOf(message) : "terminal";
31
+ const code = message.match(/\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\b/)?.[0];
32
+ if (code === void 0 && classification === "unknown" && !(cause instanceof DrizzleQueryError)) return void 0;
33
+ const location = message.match(/constraint failed: (\w+)\.(\w+)(?=\s*(?::\s*SQLITE_|$|\n))/);
34
+ const check = kind === "check" ? message.match(/CHECK constraint failed: ([A-Za-z_]\w*)/) : null;
35
+ return new DatabaseError(operation, classification, {
36
+ database: "d1",
37
+ code,
38
+ kind,
39
+ table: location?.[1],
40
+ column: location?.[2],
41
+ constraintName: check?.[1]
42
+ }, { cause });
43
+ }
44
+ function kindOf(message) {
45
+ const code = message.match(/SQLITE_CONSTRAINT_\w+/)?.[0];
46
+ if (code !== void 0) return KIND_BY_CODE.get(code);
47
+ return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1];
48
+ }
49
+ function messagesOf(cause) {
50
+ const messages = [];
51
+ const seen = /* @__PURE__ */ new Set();
52
+ let current = cause;
53
+ while (current instanceof Error && !seen.has(current)) {
54
+ seen.add(current);
55
+ if (!(current instanceof DrizzleQueryError)) messages.push(current.message);
56
+ current = current.cause;
57
+ }
58
+ if (typeof current === "string") messages.push(current);
59
+ return messages.join("\n");
60
+ }
61
+ const TERMINAL_FAILURE = /\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\b|\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i;
62
+ const TRANSIENT_FAILURE = /Network connection lost\.|Replica disconnected from primary\.|D1 DB reset because its code was updated\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\.|Cannot resolve D1 DB due to transient issue on remote node\.|Can't read from request stream because client disconnected\./i;
63
+ function classificationOf(message) {
64
+ if (TERMINAL_FAILURE.test(message)) return "terminal";
65
+ if (TRANSIENT_FAILURE.test(message)) return "transient";
66
+ return "unknown";
67
+ }
68
+
69
+ //#endregion
70
+ //#region src/infrastructure/cloudflare/d1-database-execution.ts
71
+ /** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */
72
+ async function executeDatabase(operation, work) {
73
+ try {
74
+ return await work();
75
+ } catch (cause) {
76
+ throw classifyD1Error(cause, operation) ?? cause;
77
+ }
78
+ }
79
+
80
+ //#endregion
81
+ //#region src/infrastructure/cloudflare/base-d1-repository.ts
82
+ /**
83
+ * Shared plumbing for a repository that writes inside a D1 unit of work.
84
+ * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.
85
+ */
86
+ var BaseD1Repository = class {
87
+ db;
88
+ constructor(db) {
89
+ this.db = db;
90
+ }
91
+ /** Add a statement to the current D1 batch without executing it. */
92
+ stage(statement) {
93
+ stageD1Statement(statement);
94
+ }
95
+ /** Execute one read with database error conversion and no automatic retries. */
96
+ read(operation, query) {
97
+ return executeDatabase(operation, query);
98
+ }
99
+ /** Drain an aggregate's pending events into the active boundary. */
100
+ dispatchEvents(aggregate) {
101
+ const events = aggregate.collectEvents();
102
+ if (events.length === 0) return;
103
+ for (const event of events) eventCollector.emit(event);
104
+ aggregate.clearEvents();
105
+ }
106
+ };
107
+
108
+ //#endregion
109
+ //#region src/infrastructure/cloudflare/d1-schema.ts
110
+ /** One row per committed message. The primary key is the idempotency claim. */
111
+ const processedMessages = sqliteTable("processed_messages", {
112
+ messageId: text("message_id").primaryKey(),
113
+ processedAt: integer("processed_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
114
+ }, (table) => [index("idx_processed_messages_processed_at").on(table.processedAt)]);
115
+ /** Events committed with their state change, waiting for the relay to deliver them. */
116
+ const messageOutbox = sqliteTable("message_outbox", {
117
+ id: text("id").primaryKey(),
118
+ payload: text("payload", { mode: "json" }).$type().notNull(),
119
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
120
+ availableAt: integer("available_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
121
+ attempts: integer("attempts").notNull().default(0),
122
+ claimToken: text("claim_token"),
123
+ claimExpiresAt: integer("claim_expires_at", { mode: "timestamp" }),
124
+ publishedAt: integer("published_at", { mode: "timestamp" }),
125
+ failedAt: integer("failed_at", { mode: "timestamp" }),
126
+ lastError: text("last_error")
127
+ }, (table) => [index("idx_message_outbox_due").on(table.availableAt, table.createdAt).where(sql`${table.publishedAt} IS NULL`), index("idx_message_outbox_claim_token").on(table.claimToken)]);
128
+
129
+ //#endregion
130
+ //#region src/infrastructure/cloudflare/d1-message-store.ts
131
+ /** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */
132
+ var D1MessageStore = class extends BaseD1Repository {
133
+ async claim(messageId) {
134
+ if (await this.read("claim message", () => this.db.select({ messageId: processedMessages.messageId }).from(processedMessages).where(eq(processedMessages.messageId, messageId)).get()) !== void 0) throw new MessageAlreadyProcessedError(messageId);
135
+ this.stage(this.db.insert(processedMessages).values({ messageId }));
136
+ }
137
+ async persistEvents(events) {
138
+ if (events.length === 0) return;
139
+ this.stage(this.db.insert(messageOutbox).values(events.map((event) => ({
140
+ id: event.id,
141
+ payload: event
142
+ }))));
143
+ }
144
+ };
145
+
146
+ //#endregion
147
+ //#region src/infrastructure/cloudflare/d1-batch-transaction.ts
148
+ /**
149
+ * The D1 transaction boundary: every write is staged during `work` and committed as one
150
+ * `db.batch()`. A throw inside `work` means the batch never runs.
151
+ */
152
+ var D1BatchTransaction = class {
153
+ db;
154
+ constructor(db) {
155
+ this.db = db;
156
+ }
157
+ async transaction(work) {
158
+ return runWithUnitOfWorkContext("d1", async () => {
159
+ const value = await work({ db: this.db });
160
+ const [first, ...rest] = collectD1Statements();
161
+ if (first !== void 0) await executeDatabase("commit unit of work", () => this.db.batch([first, ...rest]));
162
+ return value;
163
+ });
164
+ }
165
+ };
166
+
167
+ //#endregion
168
+ export { BaseD1Repository, D1BatchTransaction, D1MessageStore, DatabaseError, classifyD1Error, eventCollector, executeDatabase, messageOutbox, processedMessages };
169
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare.js","names":[],"sources":["../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type D1ConstraintKind, type FailureClassification } from '@/core'\n\nconst KIND_BY_CODE: ReadonlyMap<string, D1ConstraintKind> = new Map([\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique'],\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key'],\n ['SQLITE_CONSTRAINT_CHECK', 'check'],\n])\n\nconst KIND_BY_PROSE: ReadonlyArray<readonly [string, D1ConstraintKind]> = [\n ['UNIQUE constraint failed', 'unique'],\n ['NOT NULL constraint failed', 'not_null'],\n ['FOREIGN KEY constraint failed', 'foreign_key'],\n ['CHECK constraint failed', 'check'],\n]\n\n/**\n * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.\n * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.\n */\nexport function classifyD1Error(cause: unknown, operation = 'D1 query'): DatabaseError | undefined {\n if (cause instanceof AppError || (cause instanceof Error && cause.name === 'AbortError'))\n return undefined\n const message = messagesOf(cause)\n const kind = kindOf(message)\n const classification = kind === undefined ? classificationOf(message) : 'terminal'\n const code = message.match(/\\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\\b/)?.[0]\n if (code === undefined && classification === 'unknown' && !(cause instanceof DrizzleQueryError))\n return undefined\n // A composite unique constraint must not be mistaken for the processed-message primary key.\n const location = message.match(/constraint failed: (\\w+)\\.(\\w+)(?=\\s*(?::\\s*SQLITE_|$|\\n))/)\n const check = kind === 'check' ? message.match(/CHECK constraint failed: ([A-Za-z_]\\w*)/) : null\n return new DatabaseError(\n operation,\n classification,\n {\n database: 'd1',\n code,\n kind,\n table: location?.[1],\n column: location?.[2],\n constraintName: check?.[1],\n },\n { cause },\n )\n}\n\nfunction kindOf(message: string): D1ConstraintKind | undefined {\n const code = message.match(/SQLITE_CONSTRAINT_\\w+/)?.[0]\n if (code !== undefined) return KIND_BY_CODE.get(code)\n return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1]\n}\n\n// Drizzle query messages contain SQL and values, which are not evidence about the database failure.\nfunction messagesOf(cause: unknown): string {\n const messages: string[] = []\n const seen = new Set<unknown>()\n let current = cause\n while (current instanceof Error && !seen.has(current)) {\n seen.add(current)\n if (!(current instanceof DrizzleQueryError)) messages.push(current.message)\n current = current.cause\n }\n // oxlint-disable-next-line anti-slop/no-runtime-typeof -- D1 can expose a raw string cause; this boundary accepts only that representation.\n if (typeof current === 'string') messages.push(current)\n return messages.join('\\n')\n}\n\n// Cloudflare documents these conditions separately from transient storage resets.\nconst TERMINAL_FAILURE =\n /\\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\\b|\\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i\nconst TRANSIENT_FAILURE =\n /Network connection lost\\.|Replica disconnected from primary\\.|D1 DB reset because its code was updated\\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\\.|Cannot resolve D1 DB due to transient issue on remote node\\.|Can't read from request stream because client disconnected\\./i\n\nfunction classificationOf(message: string): FailureClassification {\n if (TERMINAL_FAILURE.test(message)) return 'terminal'\n if (TRANSIENT_FAILURE.test(message)) return 'transient'\n return 'unknown'\n}\n","import { classifyD1Error } from './classify-d1-error.ts'\n\n/** Execute once without retries; convert recognized D1 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 classifyD1Error(cause, operation) ?? cause\n }\n}\n","import type { BatchItem } from 'drizzle-orm/batch'\n\nimport type { Event } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector, stageD1Statement } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-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/**\n * Shared plumbing for a repository that writes inside a D1 unit of work.\n * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.\n */\nexport abstract class BaseD1Repository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: D1Db<Schema>) {}\n\n /** Add a statement to the current D1 batch without executing it. */\n protected stage(statement: BatchItem<'sqlite'>): void {\n stageD1Statement(statement)\n }\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 { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\nimport type { Event } from '@/application'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = sqliteTable(\n 'processed_messages',\n {\n messageId: text('message_id').primaryKey(),\n processedAt: integer('processed_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n },\n (table) => [index('idx_processed_messages_processed_at').on(table.processedAt)],\n)\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = sqliteTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: text('payload', { mode: 'json' }).$type<Event>().notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n availableAt: integer('available_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: integer('claim_expires_at', { mode: 'timestamp' }),\n publishedAt: integer('published_at', { mode: 'timestamp' }),\n failedAt: integer('failed_at', { mode: 'timestamp' }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(sql`${table.publishedAt} IS NULL`),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { eq } from 'drizzle-orm'\n\nimport {\n MessageAlreadyProcessedError,\n type Event,\n type IMessageStore,\n type MessageId,\n} from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { BaseD1Repository } from './base-d1-repository.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */\nexport class D1MessageStore<Schema extends DrizzleSchema>\n extends BaseD1Repository<Schema>\n implements IMessageStore\n{\n async claim(messageId: MessageId): Promise<void> {\n const processed = await this.read('claim message', () =>\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(eq(processedMessages.messageId, messageId))\n .get(),\n )\n if (processed !== undefined) throw new MessageAlreadyProcessedError(messageId)\n\n // Concurrent deliveries can pass the read; the primary key enforces the claim at commit.\n this.stage(this.db.insert(processedMessages).values({ messageId }))\n }\n\n async persistEvents(events: ReadonlyArray<Event>): Promise<void> {\n if (events.length === 0) return\n this.stage(\n this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event }))),\n )\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { collectD1Statements, runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db, D1Tx } from './d1-database-types.ts'\n\n/**\n * The D1 transaction boundary: every write is staged during `work` and committed as one\n * `db.batch()`. A throw inside `work` means the batch never runs.\n */\nexport class D1BatchTransaction<Schema extends DrizzleSchema> implements ITransaction<\n D1Tx<Schema>\n> {\n constructor(private readonly db: D1Db<Schema>) {}\n\n async transaction<T>(work: (tx: D1Tx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('d1', async () => {\n const value = await work({ db: this.db })\n const [first, ...rest] = collectD1Statements()\n if (first !== undefined) {\n await executeDatabase('commit unit of work', () => this.db.batch([first, ...rest]))\n }\n return value\n })\n }\n}\n"],"mappings":"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"}
@@ -15,6 +15,22 @@ var AppError = class extends Error {
15
15
  }
16
16
  };
17
17
 
18
+ //#endregion
19
+ //#region src/core/database.error.ts
20
+ /** Classification describes the failure; callers must separately establish whether repetition is safe. */
21
+ var DatabaseError = class extends AppError {
22
+ operation;
23
+ classification;
24
+ details;
25
+ _tag = "DatabaseError";
26
+ constructor(operation, classification, details, options) {
27
+ super(`Database operation failed: ${operation}`, options);
28
+ this.operation = operation;
29
+ this.classification = classification;
30
+ this.details = details;
31
+ }
32
+ };
33
+
18
34
  //#endregion
19
35
  //#region src/core/defects.ts
20
36
  /**
@@ -278,5 +294,5 @@ function createLogger(module, config) {
278
294
  }
279
295
 
280
296
  //#endregion
281
- export { createLogger as a, isAbortError as c, notImplemented as d, unreachable as f, consoleSink as i, isTransientTransportError as l, Logger as n, setLogSink as o, AppError as p, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, shouldRetryFailure as u };
282
- //# sourceMappingURL=core-D0asUCvq.js.map
297
+ export { createLogger as a, isAbortError as c, notImplemented as d, unreachable as f, consoleSink as i, isTransientTransportError as l, AppError as m, Logger as n, setLogSink as o, DatabaseError as p, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, shouldRetryFailure as u };
298
+ //# sourceMappingURL=core-D3G-2gIx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-D3G-2gIx.js","names":[],"sources":["../src/core/app-error.ts","../src/core/database.error.ts","../src/core/defects.ts","../src/core/failure-classification.ts","../src/core/transport-classification.ts","../src/core/logger.ts"],"sourcesContent":["import type { FailureClassification } from './failure-classification.ts'\n\n/**\n * The base of every expected failure. A subclass declares its tag and its classification,\n * so no boundary has to guess what kind of failure it holds. A boundary tells our errors from\n * raw throws with `instanceof AppError`; a value that fails it was never judged.\n *\n * A `readonly` field with a literal initializer keeps the literal type, so\n * `readonly _tag = 'StoreUnavailable'` is enough for `Result` unions and `matchError`.\n */\nexport abstract class AppError extends Error {\n abstract readonly _tag: string\n abstract readonly classification: FailureClassification\n\n override get name(): string {\n return this._tag\n }\n}\n","import { AppError } from './app-error.ts'\nimport type { FailureClassification } from './failure-classification.ts'\n\n/** SQLite reports these constraint categories through D1. */\nexport type D1ConstraintKind = 'unique' | 'not_null' | 'foreign_key' | 'check'\n\n/** Database evidence for callers that interpret a specific code or constraint. */\nexport type DatabaseFailure =\n | {\n readonly database: 'd1'\n readonly code: string | undefined\n readonly kind: D1ConstraintKind | undefined\n readonly table: string | undefined\n readonly column: string | undefined\n readonly constraintName: string | undefined\n }\n | {\n readonly database: 'postgres'\n readonly code: string | undefined\n readonly table_name: string | undefined\n readonly column_name: string | undefined\n readonly constraint_name: string | undefined\n }\n\n/** Classification describes the failure; callers must separately establish whether repetition is safe. */\nexport class DatabaseError extends AppError {\n readonly _tag = 'DatabaseError'\n\n constructor(\n readonly operation: string,\n readonly classification: FailureClassification,\n readonly details: DatabaseFailure,\n options: { readonly cause: unknown },\n ) {\n super(`Database operation failed: ${operation}`, options)\n }\n}\n","import { panic } from 'better-result'\n\n/**\n * Mark a line the types say cannot run. A union member without a branch stops compilation here.\n *\n * @throws Panic when a value outside the union arrives at runtime.\n */\nexport function unreachable(value: never): never {\n return panic(`Unreachable: ${String(value)}`)\n}\n\n/**\n * Mark a body that is not written yet. A defect, not an expected failure.\n *\n * @param what - The behavior the body will provide, such as `invoice export`.\n * @throws Panic always.\n */\nexport function notImplemented(what: string): never {\n return panic(`Not implemented: ${what}`)\n}\n","/**\n * What one failure is, before any policy decides what to do about it.\n *\n * `transient`: the world may differ on the next attempt. `terminal`: repeating gives the\n * same answer. `unknown`: nobody judged this error, so it came from outside unwrapped.\n */\nexport type FailureClassification = 'transient' | 'terminal' | 'unknown'\n\n/**\n * Who is asking to repeat the work.\n *\n * An immediate owner holds a caller and a socket open. A durable owner already stored the\n * work, so it can afford to try again on a failure nobody classified.\n */\nexport type RetryOwner = 'immediate' | 'durable'\n\n/** Decide whether one retry owner may repeat a failed operation. */\nexport function shouldRetryFailure(args: {\n readonly classification: FailureClassification\n readonly repeatSafe: boolean\n readonly owner: RetryOwner\n}): boolean {\n if (!args.repeatSafe) return false\n if (args.classification === 'transient') return true\n if (args.classification === 'terminal') return false\n return args.owner === 'durable'\n}\n","/**\n * Messages that the platform-neutral transports raise for a failure that may pass.\n *\n * A boundary that knows its own runtime adds its own patterns before these.\n */\nconst TRANSIENT_TRANSPORT_PATTERNS = [\n /network/,\n /fetch failed/,\n /timeout/,\n /timed?\\s*out/,\n /connection.*(lost|reset|refused|closed|aborted)/,\n /econnreset/,\n /econnrefused/,\n /etimedout/,\n /eai_again/,\n]\n\n/** Cancellation is a decision, not a failure to repeat. */\nexport function isAbortError(cause: unknown): boolean {\n return cause instanceof Error && cause.name === 'AbortError'\n}\n\n/** Match known transient transport messages while excluding explicit cancellation. */\nexport function isTransientTransportError(cause: unknown): boolean {\n if (!(cause instanceof Error) || isAbortError(cause)) return false\n const message = cause.message.toLowerCase()\n return TRANSIENT_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message))\n}\n","/**\n * Module-scoped logging for every host.\n *\n * A logger emits one plain-data entry per log. The default sink hands it to `console` as an\n * entry object for a log service or as a colored line for a person, selected at startup.\n */\n\n/** A value a log entry may carry. Errors enter only through `Logger.error`. */\nexport type LogValue =\n | string\n | number\n | boolean\n | null\n | undefined\n | ReadonlyArray<LogValue>\n | LogFields\n\n/** Named log values. */\nexport type LogFields = { readonly [key: string]: LogValue }\n\n/**\n * The supported log severity levels, in ascending order.\n *\n * A const object, not an enum, so a parsed environment value typed `'INFO'` is a `LogLevel`.\n */\nexport const LogLevel = {\n DEBUG: 'DEBUG',\n INFO: 'INFO',\n WARN: 'WARN',\n ERROR: 'ERROR',\n} as const\nexport type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]\n\n/** An error as plain data: name, message, stack, allowlisted fields, and the cause chain. */\nexport type SerializedError = {\n readonly name: string\n readonly message: string\n readonly stack?: string\n readonly fields?: LogFields\n readonly cause?: SerializedError\n}\n\n/** One log, as the sink receives it. Plain data, safe to stringify. */\nexport type LogEntry = {\n readonly timestamp: string\n readonly level: LogLevel\n readonly module: string\n readonly message: string\n readonly data: ReadonlyArray<LogValue>\n /** Present on `Logger.error` entries that carried an error. */\n readonly error?: SerializedError\n}\n\n/** The context an error log carries. */\nexport type ErrorLogContext = {\n readonly error?: unknown\n readonly userId?: string\n readonly details?: LogFields\n}\n\n/** What a logger lets through: a level and everything above it, or nothing. */\nexport type LogThreshold = LogLevel | 'OFF'\n\n/** Overrides the application settings for one logger. */\nexport interface LoggerConfig {\n readonly level?: LogThreshold\n}\n\n/** `json` writes the entry object. `pretty` writes one readable line for a person. */\nexport type LogFormat = 'json' | 'pretty'\n\n/**\n * The application settings, set once at the composition root.\n *\n * An omitted field keeps its default. An explicit `undefined` is a compile error, so the root\n * parses an environment value before passing it. The logger reads no environment itself.\n */\nexport interface LoggerSettings extends LoggerConfig {\n readonly format?: LogFormat\n}\n\n// The production server setting, so a root that never configures still logs safely.\nconst DEFAULT_SETTINGS: Required<LoggerSettings> = {\n level: LogLevel.INFO,\n format: 'json',\n}\n\n// The application configures once at startup; loggers can exist before startup completes.\nlet settings = DEFAULT_SETTINGS\n\n/**\n * Set application settings, including for existing loggers. Per-logger overrides take priority.\n * Call at startup, never per request. An omitted field resets to INFO and JSON output.\n */\nexport function configureLogger(config: LoggerSettings): void {\n settings = {\n level: config.level ?? DEFAULT_SETTINGS.level,\n format: config.format ?? DEFAULT_SETTINGS.format,\n }\n}\n\n/** The error data sent to the configured error hook. */\nexport interface ErrorCaptureEntry {\n readonly error: unknown\n readonly distinctId: string | undefined\n readonly context: LogFields\n}\n\n/** A function that forwards one logged error to an error service. */\nexport type LoggerErrorHook = (entry: ErrorCaptureEntry) => void\n\n/** Where every log entry goes. */\nexport type LogSink = (entry: LogEntry) => void\n\nlet errorHook: LoggerErrorHook | null = null\n\n/**\n * Register the function that receives each logged error.\n *\n * Set once at the process entry point. A later call replaces the earlier hook.\n */\nexport function setLoggerErrorHook(fn: LoggerErrorHook): void {\n errorHook = fn\n}\n\n// One console method per level, so a host that filters by method can tell them apart.\nconst CONSOLE_METHOD = {\n [LogLevel.DEBUG]: 'debug',\n [LogLevel.INFO]: 'info',\n [LogLevel.WARN]: 'warn',\n [LogLevel.ERROR]: 'error',\n} as const satisfies Record<LogLevel, 'debug' | 'info' | 'warn' | 'error'>\n\n// Built from the code point so no raw control character sits in the source.\nconst ESC = String.fromCharCode(27)\nconst ANSI_RESET = `${ESC}[0m`\nconst ANSI_GRAY = `${ESC}[90m`\nconst ANSI_BLUE = `${ESC}[34m`\n\n// Severity reads at a glance: quiet levels stay dim, and an error is the only bold line.\nconst LEVEL_ANSI = {\n DEBUG: `${ESC}[2;36m`,\n INFO: ANSI_BLUE,\n WARN: `${ESC}[33m`,\n ERROR: `${ESC}[1;31m`,\n} satisfies Record<LogLevel, string>\n\nfunction prettyLine(entry: LogEntry): string {\n // Node prints debug and info exactly like log, so the level must be in the text too.\n const level = `[${entry.level}]`\n const module = `[${entry.module}]`\n // A browser console colors by method on its own; a terminal needs ANSI to do the same.\n if ('window' in globalThis) return `${entry.timestamp} ${level} ${module} ${entry.message}`\n const timestamp = `${ANSI_GRAY}${entry.timestamp}${ANSI_RESET}`\n const coloredLevel = `${LEVEL_ANSI[entry.level]}${level}${ANSI_RESET}`\n const coloredModule = `${ANSI_BLUE}${module}${ANSI_RESET}`\n return `${timestamp} ${coloredLevel} ${coloredModule} ${entry.message}`\n}\n\n/**\n * JSON format writes the entry object. Pretty format writes one line, then the data,\n * the error, and the stack as separate console arguments.\n */\nexport const consoleSink: LogSink = (entry) => {\n // Resolved per call, so a console replaced after import still receives the output.\n const write = console[CONSOLE_METHOD[entry.level]]\n if (settings.format === 'json') {\n write(entry)\n return\n }\n const line = prettyLine(entry)\n if (entry.error === undefined) {\n write(line, ...entry.data)\n return\n }\n // A stack inside an object prints as one quoted string; as its own argument it prints as lines.\n const { stack, ...error } = entry.error\n if (stack === undefined) write(line, ...entry.data, error)\n else write(line, ...entry.data, error, `\\n${stack}`)\n}\n\nlet logSink: LogSink = consoleSink\n\n/**\n * Send every log entry somewhere other than `console`.\n *\n * Set once at the process entry point, never per module. A logger is created\n * by name and nothing else, so its destination is a fact about the process.\n */\nexport function setLogSink(fn: LogSink): void {\n logSink = fn\n}\n\nconst LOG_LEVELS = Object.values(LogLevel)\nconst MAX_ERROR_CAUSE_DEPTH = 3\n\n/**\n * Diagnostics worth keeping off an error, named one by one.\n *\n * An allowlist rather than every own property: an error raised by a library we\n * do not control may hang a request or a user payload off itself, and a log is\n * the wrong place to discover that. `_tag` and `classification` are what every `AppError`\n * declares. `remote` and the three flags after it are the ones workerd sets itself.\n */\nconst KEPT_ERROR_FIELDS = [\n '_tag',\n 'classification',\n 'code',\n 'operation',\n 'status',\n 'statusCode',\n 'remote',\n 'retryable',\n 'overloaded',\n 'durableObjectReset',\n] as const\n\ntype ErrorDiagnostics = Error & { readonly [K in (typeof KEPT_ERROR_FIELDS)[number]]?: LogValue }\n\ntype MutableSerializedError = { -readonly [K in keyof SerializedError]: SerializedError[K] }\n\nconst serializeError = (error: Error, depth = 0): SerializedError => {\n if (depth > MAX_ERROR_CAUSE_DEPTH) return { name: 'Error', message: '[cause chain truncated]' }\n\n // Every field is optional, so an `Error` is already one of these.\n const carrier: ErrorDiagnostics = error\n const kept = KEPT_ERROR_FIELDS.filter((key) => carrier[key] !== undefined)\n const serialized: MutableSerializedError = { name: error.name, message: error.message }\n if (depth === 0 && error.stack !== undefined) serialized.stack = error.stack\n if (kept.length > 0)\n serialized.fields = Object.fromEntries(kept.map((key) => [key, carrier[key]]))\n if (error.cause instanceof Error) serialized.cause = serializeError(error.cause, depth + 1)\n else if (error.cause !== undefined)\n serialized.cause = { name: 'Error', message: '[non-error cause]' }\n return serialized\n}\n\n/** Write module-scoped logs with application defaults and optional overrides. */\nexport class Logger {\n private readonly config: LoggerConfig\n\n /** Create a logger for one module. */\n constructor(\n private readonly module: string,\n overrideConfig?: LoggerConfig,\n ) {\n this.config = { ...overrideConfig }\n }\n\n private log(\n level: LogLevel,\n message: string,\n data: ReadonlyArray<LogValue>,\n error?: SerializedError,\n ): void {\n const threshold = this.config.level ?? settings.level\n if (threshold === 'OFF') return\n if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf(threshold)) return\n\n const entry = { timestamp: new Date().toISOString(), level, module: this.module, message, data }\n logSink(error === undefined ? entry : { ...entry, error })\n }\n\n /** Write a debug log. */\n debug(message: string, ...values: ReadonlyArray<LogValue>): void {\n this.log(LogLevel.DEBUG, message, values)\n }\n\n /** Write an information log. */\n info(message: string, ...values: ReadonlyArray<LogValue>): void {\n this.log(LogLevel.INFO, message, values)\n }\n\n /** Write a warning log. */\n warn(message: string, ...values: ReadonlyArray<LogValue>): void {\n this.log(LogLevel.WARN, message, values)\n }\n\n /**\n * Write an error log and send the error to the configured hook.\n *\n * The details say which call this was, and the error says what went wrong\n * inside it. The entry carries the details as data and the error beside them.\n */\n error(message: string, context: ErrorLogContext = {}): void {\n const details = context.details ?? {}\n const thrown = context.error\n let error: SerializedError | undefined\n if (thrown instanceof Error) {\n error = serializeError(thrown)\n } else if (thrown !== undefined) {\n // Not an `Error`, so there is no name or stack to keep, only its JSON form.\n let json: string\n try {\n json = JSON.stringify(thrown)\n } catch {\n json = '[non-serializable error]'\n }\n error = { name: 'NonError', message: json }\n }\n this.log(LogLevel.ERROR, message, context.details === undefined ? [] : [details], error)\n\n if (errorHook !== null && thrown !== undefined) {\n errorHook({ error: thrown, distinctId: context.userId, context: details })\n }\n }\n}\n\n/** Create a logger for one module. */\nexport function createLogger(module: string, config?: LoggerConfig): Logger {\n return new Logger(module, config)\n}\n"],"mappings":"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"}
package/dist/core.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { C as unreachable, D as shouldRetryFailure, E as RetryOwner, S as notImplemented, T as FailureClassification, _ as createLogger, a as LogFormat, b as isAbortError, c as LogThreshold, d as LoggerConfig, f as LoggerErrorHook, g as consoleSink, h as configureLogger, i as LogFields, l as LogValue, m as SerializedError, n as ErrorLogContext, o as LogLevel, p as LoggerSettings, r as LogEntry, s as LogSink, t as ErrorCaptureEntry, u as Logger, v as setLogSink, w as AppError, x as isTransientTransportError, y as setLoggerErrorHook } from "./index-eL4vb02m.js";
2
- export { AppError, type ErrorCaptureEntry, type ErrorLogContext, type FailureClassification, type LogEntry, type LogFields, type LogFormat, LogLevel, type LogSink, type LogThreshold, type LogValue, Logger, type LoggerConfig, type LoggerErrorHook, type LoggerSettings, type RetryOwner, type SerializedError, configureLogger, consoleSink, createLogger, isAbortError, isTransientTransportError, notImplemented, setLogSink, setLoggerErrorHook, shouldRetryFailure, unreachable };
1
+ import { A as shouldRetryFailure, C as unreachable, D as AppError, E as DatabaseFailure, O as FailureClassification, S as notImplemented, T as DatabaseError, _ as createLogger, a as LogFormat, b as isAbortError, c as LogThreshold, d as LoggerConfig, f as LoggerErrorHook, g as consoleSink, h as configureLogger, i as LogFields, k as RetryOwner, l as LogValue, m as SerializedError, n as ErrorLogContext, o as LogLevel, p as LoggerSettings, r as LogEntry, s as LogSink, t as ErrorCaptureEntry, u as Logger, v as setLogSink, w as D1ConstraintKind, x as isTransientTransportError, y as setLoggerErrorHook } from "./index-CT1yZd4D.js";
2
+ export { AppError, type D1ConstraintKind, DatabaseError, type DatabaseFailure, type ErrorCaptureEntry, type ErrorLogContext, type FailureClassification, type LogEntry, type LogFields, type LogFormat, LogLevel, type LogSink, type LogThreshold, type LogValue, Logger, type LoggerConfig, type LoggerErrorHook, type LoggerSettings, type RetryOwner, type SerializedError, configureLogger, consoleSink, createLogger, isAbortError, isTransientTransportError, notImplemented, setLogSink, setLoggerErrorHook, shouldRetryFailure, unreachable };
package/dist/core.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as createLogger, c as isAbortError, d as notImplemented, f as unreachable, i as consoleSink, l as isTransientTransportError, n as Logger, o as setLogSink, p as AppError, r as configureLogger, s as setLoggerErrorHook, t as LogLevel, u as shouldRetryFailure } from "./core-D0asUCvq.js";
1
+ import { a as createLogger, c as isAbortError, d as notImplemented, f as unreachable, i as consoleSink, l as isTransientTransportError, m as AppError, n as Logger, o as setLogSink, p as DatabaseError, r as configureLogger, s as setLoggerErrorHook, t as LogLevel, u as shouldRetryFailure } from "./core-D3G-2gIx.js";
2
2
 
3
- export { AppError, LogLevel, Logger, configureLogger, consoleSink, createLogger, isAbortError, isTransientTransportError, notImplemented, setLogSink, setLoggerErrorHook, shouldRetryFailure, unreachable };
3
+ export { AppError, DatabaseError, LogLevel, Logger, configureLogger, consoleSink, createLogger, isAbortError, isTransientTransportError, notImplemented, setLogSink, setLoggerErrorHook, shouldRetryFailure, unreachable };
@@ -35,6 +35,35 @@ declare abstract class AppError extends Error {
35
35
  get name(): string;
36
36
  }
37
37
  //#endregion
38
+ //#region src/core/database.error.d.ts
39
+ /** SQLite reports these constraint categories through D1. */
40
+ type D1ConstraintKind = 'unique' | 'not_null' | 'foreign_key' | 'check';
41
+ /** Database evidence for callers that interpret a specific code or constraint. */
42
+ type DatabaseFailure = {
43
+ readonly database: 'd1';
44
+ readonly code: string | undefined;
45
+ readonly kind: D1ConstraintKind | undefined;
46
+ readonly table: string | undefined;
47
+ readonly column: string | undefined;
48
+ readonly constraintName: string | undefined;
49
+ } | {
50
+ readonly database: 'postgres';
51
+ readonly code: string | undefined;
52
+ readonly table_name: string | undefined;
53
+ readonly column_name: string | undefined;
54
+ readonly constraint_name: string | undefined;
55
+ };
56
+ /** Classification describes the failure; callers must separately establish whether repetition is safe. */
57
+ declare class DatabaseError extends AppError {
58
+ readonly operation: string;
59
+ readonly classification: FailureClassification;
60
+ readonly details: DatabaseFailure;
61
+ readonly _tag = "DatabaseError";
62
+ constructor(operation: string, classification: FailureClassification, details: DatabaseFailure, options: {
63
+ readonly cause: unknown;
64
+ });
65
+ }
66
+ //#endregion
38
67
  //#region src/core/defects.d.ts
39
68
  /**
40
69
  * Mark a line the types say cannot run. A union member without a branch stops compilation here.
@@ -179,5 +208,5 @@ declare class Logger {
179
208
  /** Create a logger for one module. */
180
209
  declare function createLogger(module: string, config?: LoggerConfig): Logger;
181
210
  //#endregion
182
- export { unreachable as C, shouldRetryFailure as D, RetryOwner as E, notImplemented as S, FailureClassification as T, createLogger as _, LogFormat as a, isAbortError as b, LogThreshold as c, LoggerConfig as d, LoggerErrorHook as f, consoleSink as g, configureLogger as h, LogFields as i, LogValue as l, SerializedError as m, ErrorLogContext as n, LogLevel as o, LoggerSettings as p, LogEntry as r, LogSink as s, ErrorCaptureEntry as t, Logger as u, setLogSink as v, AppError as w, isTransientTransportError as x, setLoggerErrorHook as y };
183
- //# sourceMappingURL=index-eL4vb02m.d.ts.map
211
+ export { shouldRetryFailure as A, unreachable as C, AppError as D, DatabaseFailure as E, FailureClassification as O, notImplemented as S, DatabaseError as T, createLogger as _, LogFormat as a, isAbortError as b, LogThreshold as c, LoggerConfig as d, LoggerErrorHook as f, consoleSink as g, configureLogger as h, LogFields as i, RetryOwner as k, LogValue as l, SerializedError as m, ErrorLogContext as n, LogLevel as o, LoggerSettings as p, LogEntry as r, LogSink as s, ErrorCaptureEntry as t, Logger as u, setLogSink as v, D1ConstraintKind as w, isTransientTransportError as x, setLoggerErrorHook as y };
212
+ //# sourceMappingURL=index-CT1yZd4D.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-CT1yZd4D.d.ts","names":[],"sources":["../src/core/failure-classification.ts","../src/core/app-error.ts","../src/core/database.error.ts","../src/core/defects.ts","../src/core/transport-classification.ts","../src/core/logger.ts"],"mappings":";;;;;;;KAMY;;;;;;;KAQA;;iBAGI,mBAAmB;WACxB,gBAAgB;WAChB;WACA,OAAO;;;;;;;;;;;;uBCVI,iBAAiB;oBACnB;oBACA,gBAAgB;MAErB;;;;;KCVH;;KAGA;WAEG;WACA;WACA,MAAM;WACN;WACA;WACA;;WAGA;WACA;WACA;WACA;WACA;;;cAIF,sBAAsB;WAItB;WACA,gBAAgB;WAChB,SAAS;WALX;EAET,YACW,mBACA,gBAAgB,uBAChB,SAAS,iBAClB;aAAoB;;;;;;;;;;iBCzBR,YAAY;;;;;;;iBAUZ,eAAe;;;;iBCCf,aAAa;;iBAKb,0BAA0B;;;;;;;;;;KCf9B,0DAMR,cAAc,YACd;;KAGQ;YAAwB,cAAc;;;;;;;cAOrC;WACX;WACA;WACA;WACA;;KAEU,mBAAmB,uBAAuB;;KAG1C;WACD;WACA;WACA;WACA,SAAS;WACT,QAAQ;;;KAIP;WACD;WACA,OAAO;WACP;WACA;WACA,MAAM,cAAc;;WAEpB,QAAQ;;;KAIP;WACD;WACA;WACA,UAAU;;;KAIT,eAAe;;UAGV;WACN,QAAQ;;;KAIP;;;;;;;UAQK,uBAAuB;WAC7B,SAAS;;;;;;iBAgBJ,gBAAgB,QAAQ;;UAQvB;WACN;WACA;WACA,SAAS;;;KAIR,mBAAmB,OAAO;;KAG1B,WAAW,OAAO;;;;;;iBASd,mBAAmB,IAAI;;;;;cA0C1B,aAAa;;;;;;;iBA0BV,WAAW,IAAI;;cAiDlB;mBAKQ;mBAJF;;EAGjB,YACmB,gBACjB,iBAAiB;UAKX;;EAeR,MAAM,oBAAoB,QAAQ,cAAc;;EAKhD,KAAK,oBAAoB,QAAQ,cAAc;;EAK/C,KAAK,oBAAoB,QAAQ,cAAc;;;;;;;EAU/C,MAAM,iBAAiB,UAAS;;;iBAyBlB,aAAa,gBAAgB,SAAS,eAAe"}
@@ -0,0 +1,200 @@
1
+ import { D as AppError } from "./index-CT1yZd4D.js";
2
+ //#region src/application/messages.d.ts
3
+ /**
4
+ * The identity of a command or event. Receipts and the outbox are keyed by it.
5
+ *
6
+ * Internal messages use UUID v7, so receipts sort by creation. External messages use the
7
+ * provider's own key, `stripe:event:evt_123`, so a redelivered webhook is a duplicate by
8
+ * construction. The app parses and generates ids; this package only carries them.
9
+ */
10
+ type MessageId = string;
11
+ /** An action to perform. One handler, returns a value. The product adds its payload. */
12
+ type Command<Name extends string = string> = {
13
+ readonly type: 'command';
14
+ readonly name: Name;
15
+ readonly id: MessageId;
16
+ };
17
+ /** A request for data. One handler, returns a value. No id: nothing claims a read. */
18
+ type Query<Name extends string = string> = {
19
+ readonly type: 'query';
20
+ readonly name: Name;
21
+ };
22
+ /** A fact that happened. Zero or more subscribers, returns nothing. The product adds its payload. */
23
+ type Event<Name extends string = string> = {
24
+ readonly type: 'event';
25
+ readonly name: Name;
26
+ readonly id: MessageId;
27
+ };
28
+ /** The routing fields the bus reads. `type` selects the table, `name` selects the slot. */
29
+ type Message = Command | Event | Query;
30
+ //#endregion
31
+ //#region src/application/handlers.d.ts
32
+ /**
33
+ * Handles one command with the dependency container and returns the caller's value.
34
+ * A handler that writes inside a transaction opens the unit of work itself. Failures throw.
35
+ */
36
+ type CommandHandler<TCommand extends Command, Deps, Value> = (command: TCommand, deps: Deps) => Promise<Value>;
37
+ /**
38
+ * Handles one query with the read-only view of the container.
39
+ * The view is a type over the same object, so a query cannot name a write capability.
40
+ */
41
+ type QueryHandler<TQuery extends Query, QueryDeps, Value> = (query: TQuery, deps: QueryDeps) => Promise<Value>;
42
+ /** Completes one subscriber's work for an event. Failures throw. */
43
+ type EventHandler<TEvent extends Event, Deps> = (event: TEvent, deps: Deps) => Promise<void>;
44
+ //#endregion
45
+ //#region src/application/message-store.d.ts
46
+ /**
47
+ * Stores processed messages and outgoing events in the same transaction as application writes.
48
+ * One adapter per database, built on the boundary like every other repository.
49
+ */
50
+ interface IMessageStore {
51
+ /** Record this message in the transaction; reject an existing record with `MessageAlreadyProcessedError`. */
52
+ claim(messageId: MessageId): Promise<void>;
53
+ /** Write the events to the outbox, inside the same boundary as the state change. */
54
+ persistEvents(events: ReadonlyArray<Event>): Promise<void>;
55
+ }
56
+ //#endregion
57
+ //#region src/application/registry.d.ts
58
+ /** One slot per command. `unknown` keeps the slot covariant; `MessageResult` recovers the value. */
59
+ type CommandRegistry<TCommand extends Command, Deps> = { readonly [M in TCommand as M['name']]: CommandHandler<M, Deps, unknown>; };
60
+ /** One slot per query, typed with the read-only view of the container. */
61
+ type QueryRegistry<TQuery extends Query, QueryDeps> = { readonly [M in TQuery as M['name']]: QueryHandler<M, QueryDeps, unknown>; };
62
+ /**
63
+ * Zero or more subscribers per event. Annotate the object with this type rather than
64
+ * `satisfies`, so an event with no subscribers is still a message the bus accepts.
65
+ */
66
+ type EventRegistry<TEvent extends Event, Deps> = { readonly [M in TEvent as M['name']]?: ReadonlyArray<EventHandler<M, Deps>>; };
67
+ /**
68
+ * The app's three tables. `satisfies MessageRegistry<AppMessage, AppDeps, AppQueryDeps>`
69
+ * makes a missing handler a compile error. `Deps extends QueryDeps` is checked here, the one
70
+ * place both types meet: the read view must be a subset of the container.
71
+ */
72
+ type MessageRegistry<TMessage extends Message, Deps extends QueryDeps, QueryDeps> = {
73
+ readonly commands: CommandRegistry<Extract<TMessage, Command>, Deps>;
74
+ readonly queries: QueryRegistry<Extract<TMessage, Query>, QueryDeps>;
75
+ readonly events: EventRegistry<Extract<TMessage, Event>, Deps>;
76
+ };
77
+ type Slot<Deps> = {
78
+ handle(message: Message, deps: Deps): Promise<unknown>;
79
+ }['handle'];
80
+ type Subscription<Deps> = {
81
+ handle(event: Event, deps: Deps): Promise<void>;
82
+ }['handle'];
83
+ /**
84
+ * The registry as the bus routes it: each table keyed by name. The app's names are unknown
85
+ * inside the package, so this is what a runtime string can index.
86
+ */
87
+ type Handlers<Deps> = {
88
+ readonly commands: Readonly<Record<string, Slot<Deps>>>;
89
+ readonly queries: Readonly<Record<string, Slot<Deps>>>;
90
+ readonly events: Readonly<Partial<Record<string, ReadonlyArray<Subscription<Deps>>>>>;
91
+ };
92
+ type SlotMessage<TSlots> = Parameters<Extract<TSlots[keyof TSlots], Slot<never>>>[0];
93
+ type SubscriptionMessage<TSlots> = NonNullable<TSlots[keyof TSlots]> extends ReadonlyArray<infer TSubscription> ? Parameters<Extract<TSubscription, Subscription<never>>>[0] : never;
94
+ /** The messages a registry has a slot for, read off each slot's first parameter. */
95
+ type RegistryMessage<TRegistry extends Handlers<never>> = SlotMessage<TRegistry['commands']> | SlotMessage<TRegistry['queries']> | SubscriptionMessage<TRegistry['events']>;
96
+ type SlotValue<TSlots, TName extends string> = TName extends keyof TSlots ? Awaited<ReturnType<Extract<TSlots[TName], Slot<never>>>> : never;
97
+ /** The value `bus.handle(message)` resolves to: the slot's own return type, or void for an event. */
98
+ type MessageResult<TMessage extends Message, TRegistry extends Handlers<never>> = TMessage extends Command ? SlotValue<TRegistry['commands'], TMessage['name']> : TMessage extends Query ? SlotValue<TRegistry['queries'], TMessage['name']> : void;
99
+ //#endregion
100
+ //#region src/application/message-bus.d.ts
101
+ /** The port an entrypoint dispatches through. `AppDeps` names this as `IMessageBus<typeof registry>`. */
102
+ interface IMessageBus<TRegistry extends Handlers<never>> {
103
+ handle<TMessage extends Message & RegistryMessage<TRegistry>>(message: TMessage): Promise<MessageResult<TMessage, TRegistry>>;
104
+ }
105
+ /**
106
+ * Routes one message to its slot. The bus opens no transaction and keeps no outbox:
107
+ * a handler that writes opens the unit of work itself.
108
+ */
109
+ declare class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMessageBus<TRegistry> {
110
+ private readonly deps;
111
+ private readonly registry;
112
+ constructor(deps: Deps, registry: TRegistry);
113
+ /** Resolves to the handler's own value for a command or query, and to void for an event. */
114
+ handle<TMessage extends Message & RegistryMessage<TRegistry>>(message: TMessage): Promise<MessageResult<TMessage, TRegistry>>;
115
+ private dispatch;
116
+ private handleCommand;
117
+ private handleQuery;
118
+ /**
119
+ * Every subscriber runs to completion. One failure is rethrown as is. Several are thrown as
120
+ * one `AggregateError`. A `MessageAlreadyProcessedError` means that subscriber already committed on
121
+ * an earlier delivery, so it counts as delivered.
122
+ */
123
+ private handleEvent;
124
+ }
125
+ //#endregion
126
+ //#region src/application/outbox-relay.d.ts
127
+ /**
128
+ * Delivers committed outbox rows. The unit of work only wakes it after a commit that wrote
129
+ * events; the app decides what a wake is: `waitUntil`, a Durable Object alarm, nothing.
130
+ * The sweep is the source of truth, so a lost wake loses no events.
131
+ */
132
+ interface IOutboxRelay {
133
+ wake(): void;
134
+ }
135
+ //#endregion
136
+ //#region src/application/transaction.d.ts
137
+ /**
138
+ * Supplies `Tx` to participating repositories and resolves after their writes commit. Failure rolls back or discards writes.
139
+ * Nested calls reject with `UnitOfWorkAlreadyActiveError` before opening another boundary.
140
+ * Independent concurrent calls are allowed.
141
+ */
142
+ interface ITransaction<Tx> {
143
+ transaction<T>(work: (tx: Tx) => Promise<T>): Promise<T>;
144
+ }
145
+ //#endregion
146
+ //#region src/application/event-collector.d.ts
147
+ /** Records events in the current unit of work; the unit of work collects a snapshot for outbox persistence. */
148
+ interface IEventCollector {
149
+ emit(event: Event): void;
150
+ collect(): ReadonlyArray<Event>;
151
+ }
152
+ //#endregion
153
+ //#region src/application/unit-of-work.d.ts
154
+ /**
155
+ * Commits the processed-message record, repository writes, and emitted events together before resolving.
156
+ * An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.
157
+ * Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.
158
+ */
159
+ interface IUnitOfWork<Repos> {
160
+ run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>;
161
+ }
162
+ type UnitOfWorkDeps<Tx, Repos> = {
163
+ readonly transaction: ITransaction<Tx>;
164
+ /** The processed-messages and outbox repository for this transaction. */
165
+ readonly messageStore: (tx: Tx) => IMessageStore;
166
+ /** Repositories participate in the current transaction. */
167
+ readonly createRepositories: (tx: Tx) => Repos;
168
+ readonly eventCollector: IEventCollector;
169
+ readonly relay: IOutboxRelay;
170
+ };
171
+ /** The unit of work: the boundary, the repositories, the events. Every database call is a port. */
172
+ declare class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {
173
+ private readonly deps;
174
+ constructor(deps: UnitOfWorkDeps<Tx, Repos>);
175
+ run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>;
176
+ }
177
+ //#endregion
178
+ //#region src/application/message-already-processed.error.d.ts
179
+ /** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */
180
+ declare class MessageAlreadyProcessedError extends AppError {
181
+ readonly messageId: MessageId;
182
+ readonly _tag = "MessageAlreadyProcessedError";
183
+ readonly classification = "terminal";
184
+ constructor(messageId: MessageId, options?: {
185
+ readonly cause: unknown;
186
+ });
187
+ /** Recognize only the processed-message primary key; preserve other failures unchanged at the call site. */
188
+ static from(cause: unknown, messageId: MessageId): MessageAlreadyProcessedError | undefined;
189
+ }
190
+ //#endregion
191
+ //#region src/application/unit-of-work.errors.d.ts
192
+ /** A boundary was opened inside another. One message, one atomic scope. */
193
+ declare class UnitOfWorkAlreadyActiveError extends AppError {
194
+ readonly _tag = "UnitOfWorkAlreadyActiveError";
195
+ readonly classification = "terminal";
196
+ constructor();
197
+ }
198
+ //#endregion
199
+ export { Event as C, Query as E, Command as S, MessageId as T, RegistryMessage as _, UnitOfWorkDeps as a, EventHandler as b, IOutboxRelay as c, CommandRegistry as d, EventRegistry as f, QueryRegistry as g, MessageResult as h, UnitOfWork as i, IMessageBus as l, MessageRegistry as m, MessageAlreadyProcessedError as n, IEventCollector as o, Handlers as p, IUnitOfWork as r, ITransaction as s, UnitOfWorkAlreadyActiveError as t, MessageBus as u, IMessageStore as v, Message as w, QueryHandler as x, CommandHandler as y };
200
+ //# sourceMappingURL=index-CkaFqbbV.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-CkaFqbbV.d.ts","names":[],"sources":["../src/application/messages.ts","../src/application/handlers.ts","../src/application/message-store.ts","../src/application/registry.ts","../src/application/message-bus.ts","../src/application/outbox-relay.ts","../src/application/transaction.ts","../src/application/event-collector.ts","../src/application/unit-of-work.ts","../src/application/message-already-processed.error.ts","../src/application/unit-of-work.errors.ts"],"mappings":";;;;;;;;;KAOY;;KAGA,QAAQ;WACT;WACA,MAAM;WACN,IAAI;;;KAIH,MAAM;WACP;WACA,MAAM;;;KAIL,MAAM;WACP;WACA,MAAM;WACN,IAAI;;;KAIH,UAAU,UAAU,QAAQ;;;;;;;KCxB5B,eAAe,iBAAiB,SAAS,MAAM,UACzD,SAAS,UACT,MAAM,SACH,QAAQ;;;;;KAMD,aAAa,eAAe,OAAO,WAAW,UACxD,OAAO,QACP,MAAM,cACH,QAAQ;;KAGD,aAAa,eAAe,OAAO,SAAS,OAAO,QAAQ,MAAM,SAAS;;;;;;;UCfrE;;EAEf,MAAM,WAAW,YAAY;;EAE7B,cAAc,QAAQ,cAAc,SAAS;;;;;KCNnC,gBAAgB,iBAAiB,SAAS,oBAC1C,KAAK,YAAY,YAAY,eAAe,GAAG;;KAI/C,cAAc,eAAe,OAAO,yBACpC,KAAK,UAAU,YAAY,aAAa,GAAG;;;;;KAO3C,cAAc,eAAe,OAAO,oBACpC,KAAK,UAAU,aAAa,cAAc,aAAa,GAAG;;;;;;KAQ1D,gBAAgB,iBAAiB,SAAS,aAAa,WAAW;WACnE,UAAU,gBAAgB,QAAQ,UAAU,UAAU;WACtD,SAAS,cAAc,QAAQ,UAAU,QAAQ;WACjD,QAAQ,cAAc,QAAQ,UAAU,QAAQ;;KAKtD,KAAK;EAAU,OAAO,SAAS,SAAS,MAAM,OAAO;;KACrD,aAAa;EAAU,OAAO,OAAO,OAAO,MAAM,OAAO;;;;;;KAMlD,SAAS;WACV,UAAU,SAAS,eAAe,KAAK;WACvC,SAAS,SAAS,eAAe,KAAK;WACtC,QAAQ,SAAS,QAAQ,eAAe,cAAc,aAAa;;KAGzE,YAAY,UAAU,WAAW,QAAQ,aAAa,SAAS;KAE/D,oBAAoB,UACvB,YAAY,aAAa,iBAAiB,oBAAoB,iBAC1D,WAAW,QAAQ,eAAe;;KAI5B,gBAAgB,kBAAkB,mBAC1C,YAAY,yBACZ,YAAY,wBACZ,oBAAoB;KAEnB,UAAU,QAAQ,wBAAwB,oBAAoB,SAC/D,QAAQ,WAAW,QAAQ,OAAO,QAAQ;;KAIlC,cACV,iBAAiB,SACjB,kBAAkB,mBAChB,iBAAiB,UACjB,UAAU,uBAAuB,oBACjC,iBAAiB,QACf,UAAU,sBAAsB;;;;UC5DrB,YAAY,kBAAkB;EAC7C,OAAO,iBAAiB,UAAU,gBAAgB,YAChD,SAAS,WACR,QAAQ,cAAc,UAAU;;;;;;cAOxB,WAAW,MAAM,kBAAkB,SAAS,kBAAkB,YAAY;mBAElE;mBACA;EAFnB,YACmB,MAAM,MACN,UAAU;;EAIvB,OAAO,iBAAiB,UAAU,gBAAgB,YACtD,SAAS,WACR,QAAQ,cAAc,UAAU;UAM3B;UASA;UASA;;;;;;UAWM;;;;;;;;;UC5DC;EACf;;;;;;;;;UCDe,aAAa;EAC5B,YAAY,GAAG,OAAO,IAAI,OAAO,QAAQ,KAAK,QAAQ;;;;;UCHvC;EACf,KAAK,OAAO;EACZ,WAAW,cAAc;;;;;;;;;UCOV,YAAY;EAC3B,IAAI,GAAG,WAAW,WAAW,OAAO,OAAO,UAAU,QAAQ,KAAK,QAAQ;;KAGhE,eAAe,IAAI;WACpB,aAAa,aAAa;;WAE1B,eAAe,IAAI,OAAO;;WAE1B,qBAAqB,IAAI,OAAO;WAChC,gBAAgB;WAChB,OAAO;;;cAIL,WAAW,IAAI,kBAAkB,YAAY;mBAC3B;EAA7B,YAA6B,MAAM,eAAe,IAAI;EAEhD,IAAI,GAAG,WAAW,WAAW,OAAO,OAAO,UAAU,QAAQ,KAAK,QAAQ;;;;;cCzBrE,qCAAqC;WAKrC,WAAW;WAJb;WACA;EAET,YACW,WAAW,WACpB;aAAqB;;;SAMhB,KAAK,gBAAgB,WAAW,YAAY;;;;;cCdxC,qCAAqC;WACvC;WACA;EAET"}