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.
- package/dist/application-rpZoTfzU.js +131 -0
- package/dist/application-rpZoTfzU.js.map +1 -0
- package/dist/application.d.ts +2 -127
- package/dist/application.js +2 -84
- package/dist/cloudflare.d.ts +301 -1
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +169 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/{core-D0asUCvq.js → core-MjwyZJ4i.js} +26 -6
- package/dist/core-MjwyZJ4i.js.map +1 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/index-BN_tjxME.d.ts +200 -0
- package/dist/index-BN_tjxME.d.ts.map +1 -0
- package/dist/{index-eL4vb02m.d.ts → index-ToddX0m3.d.ts} +33 -3
- package/dist/index-ToddX0m3.d.ts.map +1 -0
- package/dist/postgres.d.ts +289 -1
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +169 -0
- package/dist/postgres.js.map +1 -0
- package/dist/unit-of-work-context-BLNj72v0.js +48 -0
- package/dist/unit-of-work-context-BLNj72v0.js.map +1 -0
- package/dist/unit-of-work-context-CbUJGHlL.d.ts +15 -0
- package/dist/unit-of-work-context-CbUJGHlL.d.ts.map +1 -0
- package/package.json +17 -2
- package/src/application/event-collector.ts +7 -0
- package/src/application/index.ts +7 -1
- package/src/application/message-already-processed.error.ts +33 -0
- package/src/application/message-bus.ts +3 -3
- package/src/application/message-store.ts +12 -0
- package/src/application/outbox-relay.ts +8 -0
- package/src/application/transaction.ts +8 -0
- package/src/application/unit-of-work.errors.ts +11 -0
- package/src/application/unit-of-work.ts +54 -0
- package/src/core/database.error.ts +37 -0
- package/src/core/index.ts +1 -0
- package/src/core/logger.ts +13 -4
- package/src/infrastructure/cloudflare/base-d1-repository.ts +40 -0
- package/src/infrastructure/cloudflare/classify-d1-error.ts +82 -0
- package/src/infrastructure/cloudflare/d1-batch-transaction.ts +27 -0
- package/src/infrastructure/cloudflare/d1-database-execution.ts +13 -0
- package/src/infrastructure/cloudflare/d1-database-types.ts +20 -0
- package/src/infrastructure/cloudflare/d1-message-store.ts +41 -0
- package/src/infrastructure/cloudflare/d1-schema.ts +43 -0
- package/src/infrastructure/cloudflare/index.ts +9 -0
- package/src/infrastructure/drizzle-schema.ts +6 -0
- package/src/infrastructure/postgres/base-postgres-repository.ts +30 -0
- package/src/infrastructure/postgres/classify-postgres-error.ts +90 -0
- package/src/infrastructure/postgres/index.ts +15 -0
- package/src/infrastructure/postgres/postgres-database-execution.ts +13 -0
- package/src/infrastructure/postgres/postgres-database-types.ts +27 -0
- package/src/infrastructure/postgres/postgres-message-store.ts +32 -0
- package/src/infrastructure/postgres/postgres-schema.ts +33 -0
- package/src/infrastructure/postgres/postgres-transaction.ts +22 -0
- package/src/infrastructure/unit-of-work-context.ts +55 -0
- package/dist/application.d.ts.map +0 -1
- package/dist/application.js.map +0 -1
- package/dist/core-D0asUCvq.js.map +0 -1
- package/dist/index-eL4vb02m.d.ts.map +0 -1
- 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"}
|
package/dist/cloudflare.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 { i as stageD1Statement, n as eventCollector, r as runWithUnitOfWorkContext, t as collectD1Statements } from "./unit-of-work-context-BLNj72v0.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":";;;;;;;;AAIA,MAAM,+BAAsD,IAAI,IAAI;CAClE,CAAC,gCAAgC,QAAQ;CACzC,CAAC,4BAA4B,QAAQ;CACrC,CAAC,6BAA6B,UAAU;CACxC,CAAC,gCAAgC,aAAa;CAC9C,CAAC,2BAA2B,OAAO;AACrC,CAAC;AAED,MAAM,gBAAoE;CACxE,CAAC,4BAA4B,QAAQ;CACrC,CAAC,8BAA8B,UAAU;CACzC,CAAC,iCAAiC,aAAa;CAC/C,CAAC,2BAA2B,OAAO;AACrC;;;;;AAMA,SAAgB,gBAAgB,OAAgB,YAAY,YAAuC;CACjG,IAAI,iBAAiB,YAAa,iBAAiB,SAAS,MAAM,SAAS,cACzE,OAAO;CACT,MAAM,UAAU,WAAW,KAAK;CAChC,MAAM,OAAO,OAAO,OAAO;CAC3B,MAAM,iBAAiB,SAAS,SAAY,iBAAiB,OAAO,IAAI;CACxE,MAAM,OAAO,QAAQ,MAAM,mCAAmC,CAAC,GAAG;CAClE,IAAI,SAAS,UAAa,mBAAmB,aAAa,EAAE,iBAAiB,oBAC3E,OAAO;CAET,MAAM,WAAW,QAAQ,MAAM,4DAA4D;CAC3F,MAAM,QAAQ,SAAS,UAAU,QAAQ,MAAM,yCAAyC,IAAI;CAC5F,OAAO,IAAI,cACT,WACA,gBACA;EACE,UAAU;EACV;EACA;EACA,OAAO,WAAW;EAClB,QAAQ,WAAW;EACnB,gBAAgB,QAAQ;CAC1B,GACA,EAAE,MAAM,CACV;AACF;AAEA,SAAS,OAAO,SAA+C;CAC7D,MAAM,OAAO,QAAQ,MAAM,uBAAuB,CAAC,GAAG;CACtD,IAAI,SAAS,QAAW,OAAO,aAAa,IAAI,IAAI;CACpD,OAAO,cAAc,MAAM,CAAC,WAAW,QAAQ,SAAS,KAAK,CAAC,CAAC,GAAG;AACpE;AAGA,SAAS,WAAW,OAAwB;CAC1C,MAAM,WAAqB,CAAC;CAC5B,MAAM,uBAAO,IAAI,IAAa;CAC9B,IAAI,UAAU;CACd,OAAO,mBAAmB,SAAS,CAAC,KAAK,IAAI,OAAO,GAAG;EACrD,KAAK,IAAI,OAAO;EAChB,IAAI,EAAE,mBAAmB,oBAAoB,SAAS,KAAK,QAAQ,OAAO;EAC1E,UAAU,QAAQ;CACpB;CAEA,IAAI,OAAO,YAAY,UAAU,SAAS,KAAK,OAAO;CACtD,OAAO,SAAS,KAAK,IAAI;AAC3B;AAGA,MAAM,mBACJ;AACF,MAAM,oBACJ;AAEF,SAAS,iBAAiB,SAAwC;CAChE,IAAI,iBAAiB,KAAK,OAAO,GAAG,OAAO;CAC3C,IAAI,kBAAkB,KAAK,OAAO,GAAG,OAAO;CAC5C,OAAO;AACT;;;;;AC9EA,eAAsB,gBACpB,WACA,MACY;CACZ,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,OAAO;EACd,MAAM,gBAAgB,OAAO,SAAS,KAAK;CAC7C;AACF;;;;;;;;ACOA,IAAsB,mBAAtB,MAAqE;CACpC;CAA/B,YAAY,AAAmB,IAAkB;EAAlB;CAAmB;;CAGlD,AAAU,MAAM,WAAsC;EACpD,iBAAiB,SAAS;CAC5B;;CAGA,AAAU,KAAQ,WAAmB,OAAyC;EAC5E,OAAO,gBAAgB,WAAW,KAAK;CACzC;;CAGA,AAAU,eAAe,WAAkC;EACzD,MAAM,SAAS,UAAU,cAAc;EACvC,IAAI,OAAO,WAAW,GAAG;EACzB,KAAK,MAAM,SAAS,QAAQ,eAAe,KAAK,KAAK;EACrD,UAAU,YAAY;CACxB;AACF;;;;;ACjCA,MAAa,oBAAoB,YAC/B,sBACA;CACE,WAAW,KAAK,YAAY,CAAC,CAAC,WAAW;CACzC,aAAa,QAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC,CAAC,CACxD,QAAQ,CAAC,CACT,iCAAiB,IAAI,KAAK,CAAC;AAChC,IACC,UAAU,CAAC,MAAM,qCAAqC,CAAC,CAAC,GAAG,MAAM,WAAW,CAAC,CAChF;;AAGA,MAAa,gBAAgB,YAC3B,kBACA;CACE,IAAI,KAAK,IAAI,CAAC,CAAC,WAAW;CAC1B,SAAS,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,MAAa,CAAC,CAAC,QAAQ;CAClE,WAAW,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,CAAC,CACpD,QAAQ,CAAC,CACT,iCAAiB,IAAI,KAAK,CAAC;CAC9B,aAAa,QAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC,CAAC,CACxD,QAAQ,CAAC,CACT,iCAAiB,IAAI,KAAK,CAAC;CAC9B,UAAU,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CACjD,YAAY,KAAK,aAAa;CAC9B,gBAAgB,QAAQ,oBAAoB,EAAE,MAAM,YAAY,CAAC;CACjE,aAAa,QAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC;CAC1D,UAAU,QAAQ,aAAa,EAAE,MAAM,YAAY,CAAC;CACpD,WAAW,KAAK,YAAY;AAC9B,IACC,UAAU,CACT,MAAM,wBAAwB,CAAC,CAC5B,GAAG,MAAM,aAAa,MAAM,SAAS,CAAC,CACtC,MAAM,GAAG,GAAG,MAAM,YAAY,SAAS,GAC1C,MAAM,gCAAgC,CAAC,CAAC,GAAG,MAAM,UAAU,CAC7D,CACF;;;;;AC5BA,IAAa,iBAAb,cACU,iBAEV;CACE,MAAM,MAAM,WAAqC;EAQ/C,IAAI,MAPoB,KAAK,KAAK,uBAChC,KAAK,GACF,OAAO,EAAE,WAAW,kBAAkB,UAAU,CAAC,CAAC,CAClD,KAAK,iBAAiB,CAAC,CACvB,MAAM,GAAG,kBAAkB,WAAW,SAAS,CAAC,CAAC,CACjD,IAAI,CACT,MACkB,QAAW,MAAM,IAAI,6BAA6B,SAAS;EAG7E,KAAK,MAAM,KAAK,GAAG,OAAO,iBAAiB,CAAC,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;CACpE;CAEA,MAAM,cAAc,QAA6C;EAC/D,IAAI,OAAO,WAAW,GAAG;EACzB,KAAK,MACH,KAAK,GACF,OAAO,aAAa,CAAC,CACrB,OAAO,OAAO,KAAK,WAAW;GAAE,IAAI,MAAM;GAAI,SAAS;EAAM,EAAE,CAAC,CACrE;CACF;AACF;;;;;;;;AC7BA,IAAa,qBAAb,MAEE;CAC6B;CAA7B,YAAY,AAAiB,IAAkB;EAAlB;CAAmB;CAEhD,MAAM,YAAe,MAAoD;EACvE,OAAO,yBAAyB,MAAM,YAAY;GAChD,MAAM,QAAQ,MAAM,KAAK,EAAE,IAAI,KAAK,GAAG,CAAC;GACxC,MAAM,CAAC,OAAO,GAAG,QAAQ,oBAAoB;GAC7C,IAAI,UAAU,QACZ,MAAM,gBAAgB,6BAA6B,KAAK,GAAG,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;GAEpF,OAAO;EACT,CAAC;CACH;AACF"}
|
|
@@ -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
|
/**
|
|
@@ -163,7 +179,7 @@ function setLogSink(fn) {
|
|
|
163
179
|
logSink = fn;
|
|
164
180
|
}
|
|
165
181
|
const LOG_LEVELS = Object.values(LogLevel);
|
|
166
|
-
const
|
|
182
|
+
const MAX_ERROR_DEPTH = 3;
|
|
167
183
|
/**
|
|
168
184
|
* Diagnostics worth keeping off an error, named one by one.
|
|
169
185
|
*
|
|
@@ -185,9 +201,9 @@ const KEPT_ERROR_FIELDS = [
|
|
|
185
201
|
"durableObjectReset"
|
|
186
202
|
];
|
|
187
203
|
const serializeError = (error, depth = 0) => {
|
|
188
|
-
if (depth >
|
|
204
|
+
if (depth > MAX_ERROR_DEPTH) return {
|
|
189
205
|
name: "Error",
|
|
190
|
-
message: "[
|
|
206
|
+
message: "[error nesting truncated]"
|
|
191
207
|
};
|
|
192
208
|
const carrier = error;
|
|
193
209
|
const kept = KEPT_ERROR_FIELDS.filter((key) => carrier[key] !== void 0);
|
|
@@ -195,13 +211,17 @@ const serializeError = (error, depth = 0) => {
|
|
|
195
211
|
name: error.name,
|
|
196
212
|
message: error.message
|
|
197
213
|
};
|
|
198
|
-
if (
|
|
214
|
+
if (error.stack !== void 0) serialized.stack = error.stack;
|
|
199
215
|
if (kept.length > 0) serialized.fields = Object.fromEntries(kept.map((key) => [key, carrier[key]]));
|
|
200
216
|
if (error.cause instanceof Error) serialized.cause = serializeError(error.cause, depth + 1);
|
|
201
217
|
else if (error.cause !== void 0) serialized.cause = {
|
|
202
218
|
name: "Error",
|
|
203
219
|
message: "[non-error cause]"
|
|
204
220
|
};
|
|
221
|
+
if (error instanceof AggregateError) serialized.errors = error.errors.map((failure) => failure instanceof Error ? serializeError(failure, depth + 1) : {
|
|
222
|
+
name: "NonError",
|
|
223
|
+
message: "[non-error aggregate member]"
|
|
224
|
+
});
|
|
205
225
|
return serialized;
|
|
206
226
|
};
|
|
207
227
|
/** Write module-scoped logs with application defaults and optional overrides. */
|
|
@@ -278,5 +298,5 @@ function createLogger(module, config) {
|
|
|
278
298
|
}
|
|
279
299
|
|
|
280
300
|
//#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,
|
|
282
|
-
//# sourceMappingURL=core-
|
|
301
|
+
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 };
|
|
302
|
+
//# sourceMappingURL=core-MjwyZJ4i.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-MjwyZJ4i.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, including diagnostics, causes, and aggregate members. */\nexport type SerializedError = {\n readonly name: string\n readonly message: string\n readonly stack?: string\n readonly fields?: LogFields\n readonly cause?: SerializedError\n readonly errors?: ReadonlyArray<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_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_DEPTH) return { name: 'Error', message: '[error nesting 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 (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 if (error instanceof AggregateError) {\n // oxlint-disable-next-line anti-slop/no-unknown-parameters -- AggregateError accepts arbitrary values; instanceof Error validates each member.\n serialized.errors = error.errors.map((failure: unknown) =>\n failure instanceof Error\n ? serializeError(failure, depth + 1)\n : { name: 'NonError', message: '[non-error aggregate member]' },\n )\n }\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
|
|
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-ToddX0m3.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
|
|
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-MjwyZJ4i.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 };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { D as AppError } from "./index-ToddX0m3.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-BN_tjxME.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-BN_tjxME.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"}
|
|
@@ -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.
|
|
@@ -81,13 +110,14 @@ declare const LogLevel: {
|
|
|
81
110
|
readonly ERROR: 'ERROR';
|
|
82
111
|
};
|
|
83
112
|
type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
|
|
84
|
-
/** An error as plain data
|
|
113
|
+
/** An error as plain data, including diagnostics, causes, and aggregate members. */
|
|
85
114
|
type SerializedError = {
|
|
86
115
|
readonly name: string;
|
|
87
116
|
readonly message: string;
|
|
88
117
|
readonly stack?: string;
|
|
89
118
|
readonly fields?: LogFields;
|
|
90
119
|
readonly cause?: SerializedError;
|
|
120
|
+
readonly errors?: ReadonlyArray<SerializedError>;
|
|
91
121
|
};
|
|
92
122
|
/** One log, as the sink receives it. Plain data, safe to stringify. */
|
|
93
123
|
type LogEntry = {
|
|
@@ -179,5 +209,5 @@ declare class Logger {
|
|
|
179
209
|
/** Create a logger for one module. */
|
|
180
210
|
declare function createLogger(module: string, config?: LoggerConfig): Logger;
|
|
181
211
|
//#endregion
|
|
182
|
-
export { unreachable as C,
|
|
183
|
-
//# sourceMappingURL=index-
|
|
212
|
+
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 };
|
|
213
|
+
//# sourceMappingURL=index-ToddX0m3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-ToddX0m3.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;WACR,SAAS,cAAc;;;KAItB;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;;cAyDlB;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"}
|