better-ship 0.11.0 → 0.13.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.
- package/dist/{application-CIxo_BpK.js → application-DnlgWcOZ.js} +118 -6
- package/dist/application-DnlgWcOZ.js.map +1 -0
- package/dist/application.d.ts +3 -3
- package/dist/application.js +2 -2
- package/dist/better-auth/apple-client-secret.d.ts +1 -1
- package/dist/better-auth/apple-client-secret.js +1 -1
- package/dist/better-auth/auth-client.js +1 -1
- package/dist/better-auth/rate-limit.d.ts +1 -1
- package/dist/better-auth/rate-limit.js +1 -1
- package/dist/better-auth.js +1 -1
- package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
- package/dist/cloudflare/auth-secondary-storage.js +1 -1
- package/dist/cloudflare/queue.d.ts +8 -3
- package/dist/cloudflare/queue.d.ts.map +1 -1
- package/dist/cloudflare/queue.js +26 -16
- package/dist/cloudflare/queue.js.map +1 -1
- package/dist/cloudflare/scheduled.d.ts +1 -1
- package/dist/cloudflare/scheduled.js +1 -1
- package/dist/cloudflare.d.ts +30 -4
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +98 -5
- package/dist/cloudflare.js.map +1 -1
- package/dist/{core-DG_cC8YT.js → core-BiqdOMwU.js} +3 -2
- package/dist/{core-DG_cC8YT.js.map → core-BiqdOMwU.js.map} +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/http.d.ts +2 -2
- package/dist/http.js +2 -2
- package/dist/{index-Bl0K1RQW.d.ts → index-B5O6mNjZ.d.ts} +4 -2
- package/dist/{index-Bl0K1RQW.d.ts.map → index-B5O6mNjZ.d.ts.map} +1 -1
- package/dist/{index-DgDyeR5s.d.ts → index-DG4jkwad.d.ts} +95 -4
- package/dist/index-DG4jkwad.d.ts.map +1 -0
- package/dist/{unit-of-work-context-BPE1In2f.js → outbox-repository-options-BI75j7b7.js} +12 -3
- package/dist/outbox-repository-options-BI75j7b7.js.map +1 -0
- package/dist/outbox-repository-options-BiilQf4_.d.ts +26 -0
- package/dist/outbox-repository-options-BiilQf4_.d.ts.map +1 -0
- package/dist/postgres.d.ts +30 -4
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +92 -5
- package/dist/postgres.js.map +1 -1
- package/dist/resend.d.ts +3 -3
- package/dist/resend.js +3 -3
- package/dist/resend.js.map +1 -1
- package/dist/tanstack.d.ts +1 -1
- package/dist/tanstack.js +1 -1
- package/dist/testing/outbox-repository.d.ts +44 -0
- package/dist/testing/outbox-repository.d.ts.map +1 -0
- package/dist/testing/outbox-repository.js +206 -0
- package/dist/testing/outbox-repository.js.map +1 -0
- package/dist/{webhook-verification-BQ9qZnVK.js → webhook-verification-CeXUJXVP.js} +2 -2
- package/dist/{webhook-verification-BQ9qZnVK.js.map → webhook-verification-CeXUJXVP.js.map} +1 -1
- package/dist/{webhook-verification-sZtwTI5n.d.ts → webhook-verification-CyBoERFz.d.ts} +2 -2
- package/dist/{webhook-verification-sZtwTI5n.d.ts.map → webhook-verification-CyBoERFz.d.ts.map} +1 -1
- package/package.json +35 -1
- package/src/application/email-service.ts +3 -1
- package/src/application/index.ts +18 -1
- package/src/application/outbox-relay.ts +117 -0
- package/src/application/outbox-repository.ts +46 -0
- package/src/application/outbox-retention.ts +61 -0
- package/src/core/backoff.ts +6 -1
- package/src/core/index.ts +1 -1
- package/src/infrastructure/cloudflare/d1-outbox-repository.ts +224 -0
- package/src/infrastructure/cloudflare/index.ts +2 -0
- package/src/infrastructure/cloudflare/queue/index.ts +1 -0
- package/src/infrastructure/cloudflare/queue/queue-client.ts +6 -2
- package/src/infrastructure/cloudflare/queue/wait-until-wake.ts +19 -0
- package/src/infrastructure/outbox-repository-options.ts +9 -0
- package/src/infrastructure/postgres/index.ts +2 -0
- package/src/infrastructure/postgres/postgres-outbox-repository.ts +216 -0
- package/src/infrastructure/resend/resend-email-service.ts +1 -1
- package/src/infrastructure/testing/outbox-repository-contract.ts +228 -0
- package/dist/application-CIxo_BpK.js.map +0 -1
- package/dist/index-DgDyeR5s.d.ts.map +0 -1
- package/dist/unit-of-work-context-BPE1In2f.js.map +0 -1
- package/dist/unit-of-work-context-BzmnjlDa.d.ts +0 -16
- package/dist/unit-of-work-context-BzmnjlDa.d.ts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { At as RecordEmailEventPayload, G as DomainEvent, Ht as EmailAddress, J as MessageId, K as DomainMessage, Mt as BounceType, W as DomainCommand, Wt as AppError, q as DomainQuery, zt as EmailIdempotencyKey } from "./index-B5O6mNjZ.js";
|
|
2
2
|
//#region src/application/handlers.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Handles one command with the dependency container and returns the caller's value.
|
|
@@ -29,6 +29,8 @@ type EmailAttachment = {
|
|
|
29
29
|
* and https://resend.com/docs/dashboard/emails/tags
|
|
30
30
|
*/
|
|
31
31
|
type OutboundEmail = {
|
|
32
|
+
/** Override the default for this email, for example "Display Name <address@domain.com>". */
|
|
33
|
+
readonly from?: string;
|
|
32
34
|
readonly to: string | readonly string[];
|
|
33
35
|
readonly subject: string;
|
|
34
36
|
readonly html: string;
|
|
@@ -46,7 +48,7 @@ type EmailAccepted = {
|
|
|
46
48
|
readonly providerId: string;
|
|
47
49
|
};
|
|
48
50
|
/**
|
|
49
|
-
* The
|
|
51
|
+
* The adapter supplies the default sender and maps provider errors. Each email can override the sender.
|
|
50
52
|
* A provider rejection throws `EmailServiceError`; unexpected SDK rejections propagate unchanged.
|
|
51
53
|
*/
|
|
52
54
|
interface IEmailService {
|
|
@@ -80,6 +82,43 @@ interface IMessageStore {
|
|
|
80
82
|
persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void>;
|
|
81
83
|
}
|
|
82
84
|
//#endregion
|
|
85
|
+
//#region src/application/outbox-repository.d.ts
|
|
86
|
+
/** A claimed event and its durable attempt count. The payload id identifies the outbox row. */
|
|
87
|
+
type OutboxMessage = {
|
|
88
|
+
readonly payload: DomainEvent;
|
|
89
|
+
readonly attempts: number;
|
|
90
|
+
};
|
|
91
|
+
/** One lease shared by the rows returned from a single atomic claim. */
|
|
92
|
+
type OutboxClaim = {
|
|
93
|
+
readonly token: string;
|
|
94
|
+
readonly messages: ReadonlyArray<OutboxMessage>;
|
|
95
|
+
};
|
|
96
|
+
/** Why an event remains in the outbox without further automatic delivery. */
|
|
97
|
+
type DeadLetterReason = 'oversized' | 'terminal' | 'exhausted';
|
|
98
|
+
/** Bounded retention operations on committed records, outside the unit of work. */
|
|
99
|
+
interface IOutboxRetentionRepository {
|
|
100
|
+
/** Delete published events strictly older than the cutoff. */
|
|
101
|
+
deletePublishedBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
102
|
+
/** Delete failed events strictly older than the cutoff. */
|
|
103
|
+
deleteFailedBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
104
|
+
/** Delete duplicate-protection receipts strictly older than the cutoff. */
|
|
105
|
+
deleteReceiptsBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Claims and settles committed events on the plain database connection. Never retry these calls locally.
|
|
109
|
+
* Each settlement affects only the supplied rows that still hold the claim token.
|
|
110
|
+
*/
|
|
111
|
+
interface IOutboxRepository extends IOutboxRetentionRepository {
|
|
112
|
+
/** Atomically lease due rows and increment their attempts, including claims after an expired lease. */
|
|
113
|
+
claimPending(batchSize: number): Promise<OutboxClaim>;
|
|
114
|
+
/** Record queue acceptance and clear the lease. */
|
|
115
|
+
markPublished(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void>;
|
|
116
|
+
/** Clear the lease and delay each row according to its own attempt count. */
|
|
117
|
+
releaseClaim(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void>;
|
|
118
|
+
/** Stop automatic delivery while retaining the payload and a readable failure reason. */
|
|
119
|
+
deadLetter(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>, reason: DeadLetterReason, errorMessage: string): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
83
122
|
//#region src/application/outbox-relay.d.ts
|
|
84
123
|
/**
|
|
85
124
|
* Delivers committed outbox rows. The unit of work only wakes it after a commit that wrote
|
|
@@ -89,6 +128,33 @@ interface IMessageStore {
|
|
|
89
128
|
interface IOutboxRelay {
|
|
90
129
|
wake(): void;
|
|
91
130
|
}
|
|
131
|
+
/** The publisher sends valid events and returns oversized events. QueueClient satisfies this port directly. */
|
|
132
|
+
interface IOutboxPublisher {
|
|
133
|
+
/** Resolves after queue acceptance; a rejected batch may already have sent some events. */
|
|
134
|
+
sendBatchPartial(messages: ReadonlyArray<DomainEvent>): Promise<ReadonlyArray<DomainEvent>>;
|
|
135
|
+
}
|
|
136
|
+
/** Optional limits for one relay; durable attempts exclude the publisher's immediate retries. */
|
|
137
|
+
type OutboxRelayOptions = {
|
|
138
|
+
readonly batchSize?: number;
|
|
139
|
+
readonly maxBatches?: number;
|
|
140
|
+
readonly maxAttempts?: number;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Publishes committed events with bounded durable retries. Ported from Typist's relay with per-row exhaustion.
|
|
144
|
+
* Send failures are recorded; repository failures propagate to the wake or scheduled boundary.
|
|
145
|
+
*/
|
|
146
|
+
declare class OutboxRelay {
|
|
147
|
+
private readonly publisher;
|
|
148
|
+
private readonly repository;
|
|
149
|
+
private readonly options;
|
|
150
|
+
constructor(publisher: IOutboxPublisher, repository: IOutboxRepository, options?: OutboxRelayOptions);
|
|
151
|
+
/** Return the claimed count, including released and dead-lettered rows. Zero means no due rows. */
|
|
152
|
+
flush(): Promise<number>;
|
|
153
|
+
/** Drain until an empty claim or the batch limit; return the total claimed count. */
|
|
154
|
+
drain(): Promise<number>;
|
|
155
|
+
private handleSendFailure;
|
|
156
|
+
private deadLetter;
|
|
157
|
+
}
|
|
92
158
|
//#endregion
|
|
93
159
|
//#region src/application/transaction.d.ts
|
|
94
160
|
/**
|
|
@@ -226,6 +292,31 @@ declare class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMes
|
|
|
226
292
|
private handleEvent;
|
|
227
293
|
}
|
|
228
294
|
//#endregion
|
|
295
|
+
//#region src/application/outbox-retention.d.ts
|
|
296
|
+
/** Typist's retention defaults; receipt retention must cover the application's supported replay period. */
|
|
297
|
+
type OutboxRetentionOptions = {
|
|
298
|
+
readonly publishedRetentionMs?: number;
|
|
299
|
+
readonly failedRetentionMs?: number;
|
|
300
|
+
readonly receiptRetentionMs?: number;
|
|
301
|
+
readonly batchSize?: number;
|
|
302
|
+
readonly maxBatches?: number;
|
|
303
|
+
};
|
|
304
|
+
/** Deleted records per category during one bounded sweep. */
|
|
305
|
+
type OutboxRetentionResult = {
|
|
306
|
+
readonly published: number;
|
|
307
|
+
readonly failed: number;
|
|
308
|
+
readonly receipts: number;
|
|
309
|
+
};
|
|
310
|
+
/** Bounded cleanup ported from Typist's message retention handler. The application schedules each sweep. */
|
|
311
|
+
declare class OutboxRetention {
|
|
312
|
+
private readonly repository;
|
|
313
|
+
private readonly options;
|
|
314
|
+
constructor(repository: IOutboxRetentionRepository, options?: OutboxRetentionOptions);
|
|
315
|
+
/** Calculate all cutoffs from one supplied time; propagate repository failures to the calling boundary. */
|
|
316
|
+
sweep(at: Date): Promise<OutboxRetentionResult>;
|
|
317
|
+
private deleteBatches;
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
229
320
|
//#region src/application/message-already-processed.error.d.ts
|
|
230
321
|
/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */
|
|
231
322
|
declare class MessageAlreadyProcessedError extends AppError {
|
|
@@ -247,5 +338,5 @@ declare class UnitOfWorkAlreadyActiveError extends AppError {
|
|
|
247
338
|
constructor();
|
|
248
339
|
}
|
|
249
340
|
//#endregion
|
|
250
|
-
export {
|
|
251
|
-
//# sourceMappingURL=index-
|
|
341
|
+
export { IOutboxRepository as A, OutboundEmail as B, UnitOfWorkDeps as C, OutboxRelay as D, IOutboxRelay as E, IEventCollector as F, CommandHandler as H, EmailAccepted as I, EmailAttachment as L, OutboxClaim as M, OutboxMessage as N, OutboxRelayOptions as O, IMessageStore as P, IEmailService as R, UnitOfWork as S, IOutboxPublisher as T, EventHandler as U, RecordingEmailService as V, QueryHandler as W, RecordEmailEventCommand as _, OutboxRetentionResult as a, recordEmailEvent as b, CommandRegistry as c, MessageRegistry as d, MessageResult as f, IEmailDeliverabilityStore as g, EmailBounceRecord as h, OutboxRetentionOptions as i, IOutboxRetentionRepository as j, DeadLetterReason as k, EventRegistry as l, RegistryMessage as m, MessageAlreadyProcessedError as n, IMessageBus as o, QueryRegistry as p, OutboxRetention as r, MessageBus as s, UnitOfWorkAlreadyActiveError as t, Handlers as u, RecordEmailEventDeps as v, ITransaction as w, IUnitOfWork as x, RecordEmailEventRepos as y, MimeType as z };
|
|
342
|
+
//# sourceMappingURL=index-DG4jkwad.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-DG4jkwad.d.ts","names":[],"sources":["../src/application/handlers.ts","../src/application/email-service.ts","../src/application/event-collector.ts","../src/application/message-store.ts","../src/application/outbox-repository.ts","../src/application/outbox-relay.ts","../src/application/transaction.ts","../src/application/unit-of-work.ts","../src/application/record-email-event.ts","../src/application/registry.ts","../src/application/message-bus.ts","../src/application/outbox-retention.ts","../src/application/message-already-processed.error.ts","../src/application/unit-of-work.errors.ts"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as UnitOfWorkAlreadyActiveError } from "./application-
|
|
1
|
+
import { t as UnitOfWorkAlreadyActiveError } from "./application-DnlgWcOZ.js";
|
|
2
2
|
import { panic } from "better-result";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
4
|
|
|
@@ -44,5 +44,14 @@ function currentContext() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
//#endregion
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
//#region src/infrastructure/outbox-repository-options.ts
|
|
48
|
+
/** Shared lease and delay defaults for both database adapters. */
|
|
49
|
+
const OUTBOX_REPOSITORY_DEFAULTS = {
|
|
50
|
+
leaseMs: 6e4,
|
|
51
|
+
retryBaseDelayMs: 1e3,
|
|
52
|
+
retryMaxDelayMs: 6e4
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { stageD1Statement as a, runWithUnitOfWorkContext as i, collectD1Statements as n, eventCollector as r, OUTBOX_REPOSITORY_DEFAULTS as t };
|
|
57
|
+
//# sourceMappingURL=outbox-repository-options-BI75j7b7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox-repository-options-BI75j7b7.js","names":[],"sources":["../src/infrastructure/unit-of-work-context.ts","../src/infrastructure/outbox-repository-options.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\n\nimport { panic } from 'better-result'\nimport type { BatchItem } from 'drizzle-orm/batch'\n\nimport { UnitOfWorkAlreadyActiveError, type IEventCollector } from '@/application'\nimport type { DomainEvent } from '@/core'\n\ntype UnitOfWorkContext = { readonly events: DomainEvent[] } & (\n | { readonly database: 'd1'; readonly statements: BatchItem<'sqlite'>[] }\n | { readonly database: 'postgres' }\n)\n\n// One async scope isolates events and D1 statements and rejects nesting across both adapters.\nconst storage = new AsyncLocalStorage<UnitOfWorkContext>()\n\n/** Reject nested transactions before database work starts; preserve independent concurrent execution. */\nexport async function runWithUnitOfWorkContext<T>(\n database: UnitOfWorkContext['database'],\n work: () => Promise<T>,\n): Promise<T> {\n if (storage.getStore() !== undefined) throw new UnitOfWorkAlreadyActiveError()\n const context: UnitOfWorkContext =\n database === 'd1' ? { database, events: [], statements: [] } : { database, events: [] }\n return storage.run(context, work)\n}\n\n/** Inject this shared scope into the unit of work and expose its emitter to application handlers. */\nexport const eventCollector: IEventCollector = {\n emit(event) {\n currentContext().events.push(event)\n },\n collect() {\n return [...currentContext().events]\n },\n}\n\n/** Queue one native D1 statement for the active batch; other contexts cannot accept D1 writes. */\nexport function stageD1Statement(statement: BatchItem<'sqlite'>): void {\n d1Context().statements.push(statement)\n}\n\n/** Read the active D1 batch in staging order for commit. */\nexport function collectD1Statements(): ReadonlyArray<BatchItem<'sqlite'>> {\n return [...d1Context().statements]\n}\n\nfunction d1Context(): Extract<UnitOfWorkContext, { database: 'd1' }> {\n const context = currentContext()\n if (context.database !== 'd1') return panic('D1 statements require a D1 unit of work')\n return context\n}\n\nfunction currentContext(): UnitOfWorkContext {\n return storage.getStore() ?? panic('operation requires an active unit of work')\n}\n","/** Shared lease and delay defaults for both database adapters. */\nexport const OUTBOX_REPOSITORY_DEFAULTS = {\n leaseMs: 60_000,\n retryBaseDelayMs: 1_000,\n retryMaxDelayMs: 60_000,\n}\n\n/** Override lease and delay limits at the composition root. */\nexport type OutboxRepositoryOptions = Readonly<Partial<typeof OUTBOX_REPOSITORY_DEFAULTS>>\n"],"mappings":";;;;;AAcA,MAAM,UAAU,IAAI,kBAAqC;;AAGzD,eAAsB,yBACpB,UACA,MACY;CACZ,IAAI,QAAQ,SAAS,MAAM,QAAW,MAAM,IAAI,6BAA6B;CAC7E,MAAM,UACJ,aAAa,OAAO;EAAE;EAAU,QAAQ,CAAC;EAAG,YAAY,CAAC;CAAE,IAAI;EAAE;EAAU,QAAQ,CAAC;CAAE;CACxF,OAAO,QAAQ,IAAI,SAAS,IAAI;AAClC;;AAGA,MAAa,iBAAkC;CAC7C,KAAK,OAAO;EACV,eAAe,CAAC,CAAC,OAAO,KAAK,KAAK;CACpC;CACA,UAAU;EACR,OAAO,CAAC,GAAG,eAAe,CAAC,CAAC,MAAM;CACpC;AACF;;AAGA,SAAgB,iBAAiB,WAAsC;CACrE,UAAU,CAAC,CAAC,WAAW,KAAK,SAAS;AACvC;;AAGA,SAAgB,sBAA0D;CACxE,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,UAAU;AACnC;AAEA,SAAS,YAA4D;CACnE,MAAM,UAAU,eAAe;CAC/B,IAAI,QAAQ,aAAa,MAAM,OAAO,MAAM,yCAAyC;CACrF,OAAO;AACT;AAEA,SAAS,iBAAoC;CAC3C,OAAO,QAAQ,SAAS,KAAK,MAAM,2CAA2C;AAChF;;;;;ACtDA,MAAa,6BAA6B;CACxC,SAAS;CACT,kBAAkB;CAClB,iBAAiB;AACnB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "./index-B5O6mNjZ.js";
|
|
2
|
+
import { F as IEventCollector } from "./index-DG4jkwad.js";
|
|
3
|
+
import { BatchItem } from "drizzle-orm/batch";
|
|
4
|
+
//#region src/infrastructure/drizzle-schema.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The app's drizzle schema: the tables it declared, keyed by name. This is drizzle's own
|
|
7
|
+
* constraint on every database and transaction type, restated once so the adapters name it.
|
|
8
|
+
*/
|
|
9
|
+
type DrizzleSchema = Record<string, unknown>;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/infrastructure/unit-of-work-context.d.ts
|
|
12
|
+
/** Inject this shared scope into the unit of work and expose its emitter to application handlers. */
|
|
13
|
+
declare const eventCollector: IEventCollector;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/infrastructure/outbox-repository-options.d.ts
|
|
16
|
+
/** Shared lease and delay defaults for both database adapters. */
|
|
17
|
+
declare const OUTBOX_REPOSITORY_DEFAULTS: {
|
|
18
|
+
leaseMs: number;
|
|
19
|
+
retryBaseDelayMs: number;
|
|
20
|
+
retryMaxDelayMs: number;
|
|
21
|
+
};
|
|
22
|
+
/** Override lease and delay limits at the composition root. */
|
|
23
|
+
type OutboxRepositoryOptions = Readonly<Partial<typeof OUTBOX_REPOSITORY_DEFAULTS>>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { eventCollector as n, DrizzleSchema as r, OutboxRepositoryOptions as t };
|
|
26
|
+
//# sourceMappingURL=outbox-repository-options-BiilQf4_.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox-repository-options-BiilQf4_.d.ts","names":[],"sources":["../src/infrastructure/drizzle-schema.ts","../src/infrastructure/unit-of-work-context.ts","../src/infrastructure/outbox-repository-options.ts"],"mappings":";;;;;;;;KAKY,gBAAgB;;;;cCuBf,gBAAgB;;;;cC3BhB;EACX;EACA;EACA;;;KAIU,0BAA0B,SAAS,eAAe"}
|
package/dist/postgres.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as DrizzleSchema, t as
|
|
1
|
+
import { G as DomainEvent, J as MessageId, nt as DatabaseError, rt as DatabaseFailure } from "./index-B5O6mNjZ.js";
|
|
2
|
+
import { A as IOutboxRepository, M as OutboxClaim, N as OutboxMessage, P as IMessageStore, k as DeadLetterReason, w as ITransaction } from "./index-DG4jkwad.js";
|
|
3
|
+
import { n as eventCollector, r as DrizzleSchema, t as OutboxRepositoryOptions } from "./outbox-repository-options-BiilQf4_.js";
|
|
4
4
|
import { ExtractTablesWithRelations } from "drizzle-orm";
|
|
5
5
|
import { PostgresJsDatabase, PostgresJsTransaction, drizzle } from "drizzle-orm/postgres-js";
|
|
6
6
|
//#region src/infrastructure/postgres/postgres-database-types.d.ts
|
|
@@ -285,5 +285,31 @@ export declare function executeDatabase<T>(operation: string, work: () => Promis
|
|
|
285
285
|
/** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */
|
|
286
286
|
export declare function classifyPostgresError(cause: unknown, operation: string): DatabaseError | undefined;
|
|
287
287
|
//#endregion
|
|
288
|
-
|
|
288
|
+
//#region src/infrastructure/postgres/postgres-outbox-repository.d.ts
|
|
289
|
+
/**
|
|
290
|
+
* Typist's lease protocol with riposte's SKIP LOCKED selection on a plain connection, outside a unit of work.
|
|
291
|
+
* Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.
|
|
292
|
+
*/
|
|
293
|
+
export declare class PostgresOutboxRepository implements IOutboxRepository {
|
|
294
|
+
private readonly db;
|
|
295
|
+
private readonly now;
|
|
296
|
+
private readonly options;
|
|
297
|
+
constructor(db: PostgresDb<DrizzleSchema>, options?: OutboxRepositoryOptions, now?: () => Date);
|
|
298
|
+
/** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */
|
|
299
|
+
claimPending(batchSize: number): Promise<OutboxClaim>;
|
|
300
|
+
/** Mark only supplied rows that still hold this token as published. */
|
|
301
|
+
markPublished(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void>;
|
|
302
|
+
/** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */
|
|
303
|
+
releaseClaim(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void>;
|
|
304
|
+
/** Retain the full payload and record a closed reason with a readable error message. */
|
|
305
|
+
deadLetter(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>, reason: DeadLetterReason, errorMessage: string): Promise<void>;
|
|
306
|
+
/** Delete at most batchSize published rows, strictly before the cutoff. */
|
|
307
|
+
deletePublishedBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
308
|
+
/** Delete at most batchSize failed rows; pending and published rows remain untouched. */
|
|
309
|
+
deleteFailedBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
310
|
+
/** Delete at most batchSize receipts, strictly before the cutoff. */
|
|
311
|
+
deleteReceiptsBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
export { type AggregateEvents, DatabaseError, type DatabaseFailure, type OutboxRepositoryOptions, type PostgresConnection, type PostgresDb, type PostgresDbTransaction, type PostgresReadDb, type PostgresTx, eventCollector };
|
|
289
315
|
//# sourceMappingURL=postgres.d.ts.map
|
package/dist/postgres.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.d.ts","names":[],"sources":["../src/infrastructure/postgres/postgres-database-types.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-transaction.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/classify-postgres-error.ts"],"mappings":";;;;;;;KAMY,qBAAqB,kBAAkB;;KAGvC,WAAW,eAAe,iBAAiB,mBAAmB;;KAG9D,eAAe,eAAe,iBAAiB,KACzD,WAAW;;KAKD,sBAAsB,eAAe,iBAAiB,sBAChE,QACA,2BAA2B;;KAIjB,WAAW,eAAe;WAC3B,IAAI,sBAAsB;;;;;UCjBpB;EACf,iBAAiB,cAAc;EAC/B;;;8BAIoB,uBAAuB,eAAe;qBAC3B,IAAI,sBAAsB;EAAzD,YAA+B,IAAI,sBAAsB;;YAG/C,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;qBCfzB,qBAAqB,eAAe,0BAA0B;mBAC5C;EAA7B,YAA6B,IAAI,sBAAsB;EAEjD,MAAM,WAAW,YAAY;EAU7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCf9C,iDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAMA,6CAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCDb,oBAAoB,eAAe,0BAA0B,aACxE,WAAW;mBAEkB;EAA7B,YAA6B,IAAI,WAAW;EAE5C,YAAY,GAAG,OAAO,IAAI,WAAW,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ;;;;wBC2BK,sBACd,gBACA,oBACC"}
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","names":[],"sources":["../src/infrastructure/postgres/postgres-database-types.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-transaction.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/classify-postgres-error.ts","../src/infrastructure/postgres/postgres-outbox-repository.ts"],"mappings":";;;;;;;KAMY,qBAAqB,kBAAkB;;KAGvC,WAAW,eAAe,iBAAiB,mBAAmB;;KAG9D,eAAe,eAAe,iBAAiB,KACzD,WAAW;;KAKD,sBAAsB,eAAe,iBAAiB,sBAChE,QACA,2BAA2B;;KAIjB,WAAW,eAAe;WAC3B,IAAI,sBAAsB;;;;;UCjBpB;EACf,iBAAiB,cAAc;EAC/B;;;8BAIoB,uBAAuB,eAAe;qBAC3B,IAAI,sBAAsB;EAAzD,YAA+B,IAAI,sBAAsB;;YAG/C,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;qBCfzB,qBAAqB,eAAe,0BAA0B;mBAC5C;EAA7B,YAA6B,IAAI,sBAAsB;EAEjD,MAAM,WAAW,YAAY;EAU7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCf9C,iDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAMA,6CAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCDb,oBAAoB,eAAe,0BAA0B,aACxE,WAAW;mBAEkB;EAA7B,YAA6B,IAAI,WAAW;EAE5C,YAAY,GAAG,OAAO,IAAI,WAAW,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ;;;;wBC2BK,sBACd,gBACA,oBACC;;;;;;;qBClBU,oCAAoC;mBAI5B;mBAEA;mBALF;EAEjB,YACmB,IAAI,WAAW,gBAChC,UAAS,yBACQ,YAAW;;EAMxB,aAAa,oBAAoB,QAAQ;;EAoDzC,cAAc,OAAO,aAAa,UAAU,cAAc,iBAAiB;;EAmB3E,aAAa,OAAO,aAAa,UAAU,cAAc,iBAAiB;;EAwB1E,WACJ,OAAO,aACP,UAAU,cAAc,gBACxB,QAAQ,kBACR,uBACC;;EAwBG,sBAAsB,QAAQ,MAAM,oBAAoB;;EAqBxD,mBAAmB,QAAQ,MAAM,oBAAoB;;EAqBrD,qBAAqB,QAAQ,MAAM,oBAAoB"}
|
package/dist/postgres.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { $ as AppError, D as DatabaseError, c as exponentialDelayCapMs } from "./core-BiqdOMwU.js";
|
|
2
|
+
import { o as MessageAlreadyProcessedError } from "./application-DnlgWcOZ.js";
|
|
3
|
+
import { i as runWithUnitOfWorkContext, r as eventCollector, t as OUTBOX_REPOSITORY_DEFAULTS } from "./outbox-repository-options-BI75j7b7.js";
|
|
4
4
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
5
|
-
import { isNull } from "drizzle-orm";
|
|
5
|
+
import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
6
6
|
import { index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
7
7
|
|
|
8
8
|
//#region src/infrastructure/postgres/classify-postgres-error.ts
|
|
@@ -165,5 +165,92 @@ var PostgresTransaction = class {
|
|
|
165
165
|
};
|
|
166
166
|
|
|
167
167
|
//#endregion
|
|
168
|
-
|
|
168
|
+
//#region src/infrastructure/postgres/postgres-outbox-repository.ts
|
|
169
|
+
/**
|
|
170
|
+
* Typist's lease protocol with riposte's SKIP LOCKED selection on a plain connection, outside a unit of work.
|
|
171
|
+
* Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.
|
|
172
|
+
*/
|
|
173
|
+
var PostgresOutboxRepository = class {
|
|
174
|
+
db;
|
|
175
|
+
now;
|
|
176
|
+
options;
|
|
177
|
+
constructor(db, options = {}, now = () => /* @__PURE__ */ new Date()) {
|
|
178
|
+
this.db = db;
|
|
179
|
+
this.now = now;
|
|
180
|
+
this.options = {
|
|
181
|
+
...OUTBOX_REPOSITORY_DEFAULTS,
|
|
182
|
+
...options
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */
|
|
186
|
+
async claimPending(batchSize) {
|
|
187
|
+
const token = crypto.randomUUID();
|
|
188
|
+
const now = this.now();
|
|
189
|
+
const rows = await executeDatabase("claim pending outbox messages", () => this.db.update(messageOutbox).set({
|
|
190
|
+
claimToken: token,
|
|
191
|
+
claimExpiresAt: new Date(now.getTime() + this.options.leaseMs),
|
|
192
|
+
attempts: sql`${messageOutbox.attempts} + 1`
|
|
193
|
+
}).where(inArray(messageOutbox.id, this.db.select({ id: messageOutbox.id }).from(messageOutbox).where(and(isNull(messageOutbox.publishedAt), isNull(messageOutbox.failedAt), lte(messageOutbox.availableAt, now), or(isNull(messageOutbox.claimExpiresAt), lte(messageOutbox.claimExpiresAt, now)))).orderBy(asc(messageOutbox.availableAt), asc(messageOutbox.createdAt), asc(messageOutbox.id)).limit(batchSize).for("update", { skipLocked: true }))).returning({
|
|
194
|
+
payload: messageOutbox.payload,
|
|
195
|
+
attempts: messageOutbox.attempts,
|
|
196
|
+
availableAt: messageOutbox.availableAt,
|
|
197
|
+
createdAt: messageOutbox.createdAt
|
|
198
|
+
}));
|
|
199
|
+
rows.sort((a, b) => a.availableAt.getTime() - b.availableAt.getTime() || a.createdAt.getTime() - b.createdAt.getTime() || a.payload.id.localeCompare(b.payload.id));
|
|
200
|
+
return {
|
|
201
|
+
token,
|
|
202
|
+
messages: rows.map(({ payload, attempts }) => ({
|
|
203
|
+
payload,
|
|
204
|
+
attempts
|
|
205
|
+
}))
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Mark only supplied rows that still hold this token as published. */
|
|
209
|
+
async markPublished(claim, messages) {
|
|
210
|
+
if (messages.length === 0) return;
|
|
211
|
+
await executeDatabase("mark outbox messages published", () => this.db.update(messageOutbox).set({
|
|
212
|
+
publishedAt: this.now(),
|
|
213
|
+
claimToken: null,
|
|
214
|
+
claimExpiresAt: null
|
|
215
|
+
}).where(and(inArray(messageOutbox.id, messages.map((row) => row.payload.id)), eq(messageOutbox.claimToken, claim.token))));
|
|
216
|
+
}
|
|
217
|
+
/** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */
|
|
218
|
+
async releaseClaim(claim, messages) {
|
|
219
|
+
const now = this.now();
|
|
220
|
+
for (const attempts of new Set(messages.map((row) => row.attempts))) {
|
|
221
|
+
const ids = messages.filter((row) => row.attempts === attempts).map((row) => row.payload.id);
|
|
222
|
+
const delay = exponentialDelayCapMs(this.options.retryBaseDelayMs, this.options.retryMaxDelayMs, attempts);
|
|
223
|
+
await executeDatabase("release outbox claim", () => this.db.update(messageOutbox).set({
|
|
224
|
+
availableAt: new Date(now.getTime() + delay),
|
|
225
|
+
claimToken: null,
|
|
226
|
+
claimExpiresAt: null
|
|
227
|
+
}).where(and(inArray(messageOutbox.id, ids), eq(messageOutbox.claimToken, claim.token))));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Retain the full payload and record a closed reason with a readable error message. */
|
|
231
|
+
async deadLetter(claim, messages, reason, errorMessage) {
|
|
232
|
+
if (messages.length === 0) return;
|
|
233
|
+
await executeDatabase("dead-letter outbox messages", () => this.db.update(messageOutbox).set({
|
|
234
|
+
failedAt: this.now(),
|
|
235
|
+
lastError: `${reason}: ${errorMessage}`,
|
|
236
|
+
claimToken: null,
|
|
237
|
+
claimExpiresAt: null
|
|
238
|
+
}).where(and(inArray(messageOutbox.id, messages.map((row) => row.payload.id)), eq(messageOutbox.claimToken, claim.token))));
|
|
239
|
+
}
|
|
240
|
+
/** Delete at most batchSize published rows, strictly before the cutoff. */
|
|
241
|
+
async deletePublishedBefore(cutoff, batchSize) {
|
|
242
|
+
return (await executeDatabase("delete published outbox messages", () => this.db.delete(messageOutbox).where(inArray(messageOutbox.id, this.db.select({ id: messageOutbox.id }).from(messageOutbox).where(lt(messageOutbox.publishedAt, cutoff)).orderBy(asc(messageOutbox.publishedAt), asc(messageOutbox.id)).limit(batchSize))).returning({ id: messageOutbox.id }))).length;
|
|
243
|
+
}
|
|
244
|
+
/** Delete at most batchSize failed rows; pending and published rows remain untouched. */
|
|
245
|
+
async deleteFailedBefore(cutoff, batchSize) {
|
|
246
|
+
return (await executeDatabase("delete failed outbox messages", () => this.db.delete(messageOutbox).where(inArray(messageOutbox.id, this.db.select({ id: messageOutbox.id }).from(messageOutbox).where(and(isNull(messageOutbox.publishedAt), lt(messageOutbox.failedAt, cutoff))).orderBy(asc(messageOutbox.failedAt), asc(messageOutbox.id)).limit(batchSize))).returning({ id: messageOutbox.id }))).length;
|
|
247
|
+
}
|
|
248
|
+
/** Delete at most batchSize receipts, strictly before the cutoff. */
|
|
249
|
+
async deleteReceiptsBefore(cutoff, batchSize) {
|
|
250
|
+
return (await executeDatabase("delete message receipts", () => this.db.delete(processedMessages).where(inArray(processedMessages.messageId, this.db.select({ messageId: processedMessages.messageId }).from(processedMessages).where(lt(processedMessages.processedAt, cutoff)).orderBy(asc(processedMessages.processedAt), asc(processedMessages.messageId)).limit(batchSize))).returning({ messageId: processedMessages.messageId }))).length;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
export { BasePostgresRepository, DatabaseError, PostgresMessageStore, PostgresOutboxRepository, PostgresTransaction, classifyPostgresError, eventCollector, executeDatabase, messageOutbox, processedMessages };
|
|
169
256
|
//# sourceMappingURL=postgres.js.map
|
package/dist/postgres.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.js","names":[],"sources":["../src/infrastructure/postgres/classify-postgres-error.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/postgres-transaction.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type FailureClassification } from '@/core'\n\nconst TRANSIENT_CODES = new Set([\n '40001',\n '40P01',\n '08000',\n '08001',\n '08003',\n '08006',\n '57P01',\n '57P02',\n '57P03',\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'EAI_AGAIN',\n])\nconst TERMINAL_CODES = new Set(['57014', 'CONNECTION_ENDED', 'CONNECTION_DESTROYED', 'ECANCELED'])\nconst DRIVER_CODES = new Set([\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'CONNECTION_ENDED',\n 'CONNECTION_DESTROYED',\n])\nconst TERMINAL_CLASSES = new Set(['0A', '22', '23', '25', '28', '3D', '3F', '42', '54'])\n\n/* oxlint-disable anti-slop/no-runtime-typeof -- Raw driver exceptions have no shared schema; this boundary validates the fields it exposes. */\n/** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */\nexport function classifyPostgresError(\n cause: unknown,\n operation: string,\n): DatabaseError | undefined {\n let error = cause\n const seen = new Set<unknown>()\n while (error instanceof DrizzleQueryError && !seen.has(error)) {\n seen.add(error)\n error = error.cause\n }\n if (error instanceof AppError || (error instanceof Error && error.name === 'AbortError'))\n return undefined\n const code =\n error instanceof Error && 'code' in error && typeof error.code === 'string'\n ? error.code\n : undefined\n const serverError =\n error instanceof Error &&\n error.name === 'PostgresError' &&\n code !== undefined &&\n /^[0-9A-Z]{5}$/.test(code)\n const driverError = code !== undefined && DRIVER_CODES.has(code)\n if (!(cause instanceof DrizzleQueryError) && !serverError && !driverError) return undefined\n\n return new DatabaseError(\n operation,\n classificationOf(code),\n {\n database: 'postgres',\n code,\n table_name:\n error instanceof Error && 'table_name' in error && typeof error.table_name === 'string'\n ? error.table_name\n : undefined,\n column_name:\n error instanceof Error && 'column_name' in error && typeof error.column_name === 'string'\n ? error.column_name\n : undefined,\n constraint_name:\n error instanceof Error &&\n 'constraint_name' in error &&\n typeof error.constraint_name === 'string'\n ? error.constraint_name\n : undefined,\n },\n { cause },\n )\n}\n\n/* oxlint-enable anti-slop/no-runtime-typeof */\n\nfunction classificationOf(code: string | undefined): FailureClassification {\n if (code === undefined) return 'unknown'\n if (TRANSIENT_CODES.has(code)) return 'transient'\n if (TERMINAL_CODES.has(code) || TERMINAL_CLASSES.has(code.slice(0, 2))) return 'terminal'\n return 'unknown'\n}\n","import { classifyPostgresError } from './classify-postgres-error.ts'\n\n/** Execute once without retries; convert recognized Postgres failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyPostgresError(cause, operation) ?? cause\n }\n}\n","import type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/** Shared plumbing for a repository that writes inside a Postgres unit of work. */\nexport abstract class BasePostgresRepository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: PostgresDbTransaction<Schema>) {}\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { isNull } from 'drizzle-orm'\nimport { index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = pgTable('processed_messages', {\n messageId: text('message_id').primaryKey(),\n processedAt: timestamp('processed_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = pgTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: jsonb('payload').$type<DomainEvent>().notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n availableAt: timestamp('available_at', { withTimezone: true }).notNull().defaultNow(),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: timestamp('claim_expires_at', { withTimezone: true }),\n publishedAt: timestamp('published_at', { withTimezone: true }),\n failedAt: timestamp('failed_at', { withTimezone: true }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(isNull(table.publishedAt)),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\nimport { messageOutbox, processedMessages } from './postgres-schema.ts'\n\n/** Processed messages and outbox events share the active Postgres transaction; the insert result establishes the claim. */\nexport class PostgresMessageStore<Schema extends DrizzleSchema> implements IMessageStore {\n constructor(private readonly db: PostgresDbTransaction<Schema>) {}\n\n async claim(messageId: MessageId): Promise<void> {\n const claimed = await this.db\n .insert(processedMessages)\n .values({ messageId })\n .onConflictDoNothing()\n .returning({ messageId: processedMessages.messageId })\n // Thrown inside the transaction, so drizzle rolls back everything written before the claim.\n if (claimed.length === 0) throw new MessageAlreadyProcessedError(messageId)\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n await this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event })))\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDb, PostgresTx } from './postgres-database-types.ts'\n\n/**\n * The Postgres transaction boundary uses native Drizzle transactions through postgres.js.\n * Drizzle commits when `work` returns and rolls back when it throws.\n */\nexport class PostgresTransaction<Schema extends DrizzleSchema> implements ITransaction<\n PostgresTx<Schema>\n> {\n constructor(private readonly db: PostgresDb<Schema>) {}\n\n transaction<T>(work: (tx: PostgresTx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('postgres', () =>\n executeDatabase('unit of work transaction', () => this.db.transaction((db) => work({ db }))),\n )\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"postgres.js","names":[],"sources":["../src/infrastructure/postgres/classify-postgres-error.ts","../src/infrastructure/postgres/postgres-database-execution.ts","../src/infrastructure/postgres/base-postgres-repository.ts","../src/infrastructure/postgres/postgres-schema.ts","../src/infrastructure/postgres/postgres-message-store.ts","../src/infrastructure/postgres/postgres-transaction.ts","../src/infrastructure/postgres/postgres-outbox-repository.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type FailureClassification } from '@/core'\n\nconst TRANSIENT_CODES = new Set([\n '40001',\n '40P01',\n '08000',\n '08001',\n '08003',\n '08006',\n '57P01',\n '57P02',\n '57P03',\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'EAI_AGAIN',\n])\nconst TERMINAL_CODES = new Set(['57014', 'CONNECTION_ENDED', 'CONNECTION_DESTROYED', 'ECANCELED'])\nconst DRIVER_CODES = new Set([\n 'CONNECT_TIMEOUT',\n 'CONNECTION_CLOSED',\n 'CONNECTION_ENDED',\n 'CONNECTION_DESTROYED',\n])\nconst TERMINAL_CLASSES = new Set(['0A', '22', '23', '25', '28', '3D', '3F', '42', '54'])\n\n/* oxlint-disable anti-slop/no-runtime-typeof -- Raw driver exceptions have no shared schema; this boundary validates the fields it exposes. */\n/** Read SQLSTATE or driver codes; unknown callback exceptions remain unchanged. */\nexport function classifyPostgresError(\n cause: unknown,\n operation: string,\n): DatabaseError | undefined {\n let error = cause\n const seen = new Set<unknown>()\n while (error instanceof DrizzleQueryError && !seen.has(error)) {\n seen.add(error)\n error = error.cause\n }\n if (error instanceof AppError || (error instanceof Error && error.name === 'AbortError'))\n return undefined\n const code =\n error instanceof Error && 'code' in error && typeof error.code === 'string'\n ? error.code\n : undefined\n const serverError =\n error instanceof Error &&\n error.name === 'PostgresError' &&\n code !== undefined &&\n /^[0-9A-Z]{5}$/.test(code)\n const driverError = code !== undefined && DRIVER_CODES.has(code)\n if (!(cause instanceof DrizzleQueryError) && !serverError && !driverError) return undefined\n\n return new DatabaseError(\n operation,\n classificationOf(code),\n {\n database: 'postgres',\n code,\n table_name:\n error instanceof Error && 'table_name' in error && typeof error.table_name === 'string'\n ? error.table_name\n : undefined,\n column_name:\n error instanceof Error && 'column_name' in error && typeof error.column_name === 'string'\n ? error.column_name\n : undefined,\n constraint_name:\n error instanceof Error &&\n 'constraint_name' in error &&\n typeof error.constraint_name === 'string'\n ? error.constraint_name\n : undefined,\n },\n { cause },\n )\n}\n\n/* oxlint-enable anti-slop/no-runtime-typeof */\n\nfunction classificationOf(code: string | undefined): FailureClassification {\n if (code === undefined) return 'unknown'\n if (TRANSIENT_CODES.has(code)) return 'transient'\n if (TERMINAL_CODES.has(code) || TERMINAL_CLASSES.has(code.slice(0, 2))) return 'terminal'\n return 'unknown'\n}\n","import { classifyPostgresError } from './classify-postgres-error.ts'\n\n/** Execute once without retries; convert recognized Postgres failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyPostgresError(cause, operation) ?? cause\n }\n}\n","import type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/** Shared plumbing for a repository that writes inside a Postgres unit of work. */\nexport abstract class BasePostgresRepository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: PostgresDbTransaction<Schema>) {}\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { isNull } from 'drizzle-orm'\nimport { index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = pgTable('processed_messages', {\n messageId: text('message_id').primaryKey(),\n processedAt: timestamp('processed_at', { withTimezone: true }).notNull().defaultNow(),\n})\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = pgTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: jsonb('payload').$type<DomainEvent>().notNull(),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n availableAt: timestamp('available_at', { withTimezone: true }).notNull().defaultNow(),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: timestamp('claim_expires_at', { withTimezone: true }),\n publishedAt: timestamp('published_at', { withTimezone: true }),\n failedAt: timestamp('failed_at', { withTimezone: true }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(isNull(table.publishedAt)),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport type { PostgresDbTransaction } from './postgres-database-types.ts'\nimport { messageOutbox, processedMessages } from './postgres-schema.ts'\n\n/** Processed messages and outbox events share the active Postgres transaction; the insert result establishes the claim. */\nexport class PostgresMessageStore<Schema extends DrizzleSchema> implements IMessageStore {\n constructor(private readonly db: PostgresDbTransaction<Schema>) {}\n\n async claim(messageId: MessageId): Promise<void> {\n const claimed = await this.db\n .insert(processedMessages)\n .values({ messageId })\n .onConflictDoNothing()\n .returning({ messageId: processedMessages.messageId })\n // Thrown inside the transaction, so drizzle rolls back everything written before the claim.\n if (claimed.length === 0) throw new MessageAlreadyProcessedError(messageId)\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n await this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event })))\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDb, PostgresTx } from './postgres-database-types.ts'\n\n/**\n * The Postgres transaction boundary uses native Drizzle transactions through postgres.js.\n * Drizzle commits when `work` returns and rolls back when it throws.\n */\nexport class PostgresTransaction<Schema extends DrizzleSchema> implements ITransaction<\n PostgresTx<Schema>\n> {\n constructor(private readonly db: PostgresDb<Schema>) {}\n\n transaction<T>(work: (tx: PostgresTx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('postgres', () =>\n executeDatabase('unit of work transaction', () => this.db.transaction((db) => work({ db }))),\n )\n }\n}\n","import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from 'drizzle-orm'\n\nimport type { DeadLetterReason, IOutboxRepository, OutboxClaim, OutboxMessage } from '@/application'\nimport { exponentialDelayCapMs } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport {\n OUTBOX_REPOSITORY_DEFAULTS,\n type OutboxRepositoryOptions,\n} from '../outbox-repository-options.ts'\nimport { executeDatabase } from './postgres-database-execution.ts'\nimport type { PostgresDb } from './postgres-database-types.ts'\nimport { messageOutbox, processedMessages } from './postgres-schema.ts'\n\n/**\n * Typist's lease protocol with riposte's SKIP LOCKED selection on a plain connection, outside a unit of work.\n * Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.\n */\nexport class PostgresOutboxRepository implements IOutboxRepository {\n private readonly options: Required<OutboxRepositoryOptions>\n\n constructor(\n private readonly db: PostgresDb<DrizzleSchema>,\n options: OutboxRepositoryOptions = {},\n private readonly now: () => Date = () => new Date(),\n ) {\n this.options = { ...OUTBOX_REPOSITORY_DEFAULTS, ...options }\n }\n\n /** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */\n async claimPending(batchSize: number): Promise<OutboxClaim> {\n const token = crypto.randomUUID()\n const now = this.now()\n const rows = await executeDatabase('claim pending outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n claimToken: token,\n claimExpiresAt: new Date(now.getTime() + this.options.leaseMs),\n attempts: sql`${messageOutbox.attempts} + 1`,\n })\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(\n and(\n isNull(messageOutbox.publishedAt),\n isNull(messageOutbox.failedAt),\n lte(messageOutbox.availableAt, now),\n or(isNull(messageOutbox.claimExpiresAt), lte(messageOutbox.claimExpiresAt, now)),\n ),\n )\n .orderBy(\n asc(messageOutbox.availableAt),\n asc(messageOutbox.createdAt),\n asc(messageOutbox.id),\n )\n .limit(batchSize)\n .for('update', { skipLocked: true }),\n ),\n )\n .returning({\n payload: messageOutbox.payload,\n attempts: messageOutbox.attempts,\n availableAt: messageOutbox.availableAt,\n createdAt: messageOutbox.createdAt,\n }),\n )\n // UPDATE RETURNING does not preserve the selection order.\n rows.sort(\n (a, b) =>\n a.availableAt.getTime() - b.availableAt.getTime() ||\n a.createdAt.getTime() - b.createdAt.getTime() ||\n a.payload.id.localeCompare(b.payload.id),\n )\n return { token, messages: rows.map(({ payload, attempts }) => ({ payload, attempts })) }\n }\n\n /** Mark only supplied rows that still hold this token as published. */\n async markPublished(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('mark outbox messages published', () =>\n this.db\n .update(messageOutbox)\n .set({ publishedAt: this.now(), claimToken: null, claimExpiresAt: null })\n .where(\n and(\n inArray(\n messageOutbox.id,\n messages.map((row) => row.payload.id),\n ),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */\n async releaseClaim(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n const now = this.now()\n for (const attempts of new Set(messages.map((row) => row.attempts))) {\n const ids = messages.filter((row) => row.attempts === attempts).map((row) => row.payload.id)\n const delay = exponentialDelayCapMs(\n this.options.retryBaseDelayMs,\n this.options.retryMaxDelayMs,\n attempts,\n )\n // oxlint-disable-next-line no-await-in-loop -- Stop releasing rows on the first database failure.\n await executeDatabase('release outbox claim', () =>\n this.db\n .update(messageOutbox)\n .set({\n availableAt: new Date(now.getTime() + delay),\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(and(inArray(messageOutbox.id, ids), eq(messageOutbox.claimToken, claim.token))),\n )\n }\n }\n\n /** Retain the full payload and record a closed reason with a readable error message. */\n async deadLetter(\n claim: OutboxClaim,\n messages: ReadonlyArray<OutboxMessage>,\n reason: DeadLetterReason,\n errorMessage: string,\n ): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('dead-letter outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n failedAt: this.now(),\n lastError: `${reason}: ${errorMessage}`,\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(\n and(\n inArray(\n messageOutbox.id,\n messages.map((row) => row.payload.id),\n ),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Delete at most batchSize published rows, strictly before the cutoff. */\n async deletePublishedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete published outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(lt(messageOutbox.publishedAt, cutoff))\n .orderBy(asc(messageOutbox.publishedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize failed rows; pending and published rows remain untouched. */\n async deleteFailedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete failed outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(and(isNull(messageOutbox.publishedAt), lt(messageOutbox.failedAt, cutoff)))\n .orderBy(asc(messageOutbox.failedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize receipts, strictly before the cutoff. */\n async deleteReceiptsBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete message receipts', () =>\n this.db\n .delete(processedMessages)\n .where(\n inArray(\n processedMessages.messageId,\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(lt(processedMessages.processedAt, cutoff))\n .orderBy(asc(processedMessages.processedAt), asc(processedMessages.messageId))\n .limit(batchSize),\n ),\n )\n .returning({ messageId: processedMessages.messageId }),\n )\n return rows.length\n }\n}\n"],"mappings":"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"}
|
package/dist/resend.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { r as WebhookVerifier, t as WebhookVerification } from "./webhook-verification-
|
|
1
|
+
import { Mt as BounceType } from "./index-B5O6mNjZ.js";
|
|
2
|
+
import { B as OutboundEmail, I as EmailAccepted, R as IEmailService } from "./index-DG4jkwad.js";
|
|
3
|
+
import { r as WebhookVerifier, t as WebhookVerification } from "./webhook-verification-CyBoERFz.js";
|
|
4
4
|
import { Resend } from "resend";
|
|
5
5
|
//#region src/infrastructure/resend/resend-email-service.d.ts
|
|
6
6
|
/** The SDK operations required by the adapter; a test factory can return a plain object. */
|
package/dist/resend.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as WebhookVerificationError } from "./webhook-verification-
|
|
1
|
+
import { Q as EmailAddressSchema, Y as EmailServiceError, q as parseBounceType } from "./core-BiqdOMwU.js";
|
|
2
|
+
import { t as WebhookVerificationError } from "./webhook-verification-CeXUJXVP.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
|
|
@@ -78,7 +78,7 @@ var ResendEmailService = class ResendEmailService {
|
|
|
78
78
|
if (this.client === void 0) this.client = Promise.resolve().then(this.createClient);
|
|
79
79
|
const client = await this.client;
|
|
80
80
|
const payload = {
|
|
81
|
-
from: this.fromAddress,
|
|
81
|
+
from: email.from ?? this.fromAddress,
|
|
82
82
|
to: [email.to].flat(),
|
|
83
83
|
subject: email.subject,
|
|
84
84
|
html: email.html
|
package/dist/resend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resend.js","names":[],"sources":["../src/infrastructure/resend/resend-email-service.ts","../src/infrastructure/resend/email-event.ts","../src/infrastructure/resend/resend-webhook.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nimport type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'\n\nimport type { EmailAccepted, EmailAttachment, IEmailService, OutboundEmail } from '@/application'\nimport { EmailServiceError, type EmailSendFailureReason } from '@/core'\n\n// SDK additions must receive an explicit classification before the package builds.\nconst SDK_REASONS = {\n invalid_idempotency_key: 'invalid_request',\n validation_error: 'invalid_request',\n missing_api_key: 'configuration',\n restricted_api_key: 'configuration',\n invalid_api_key: 'configuration',\n not_found: 'invalid_request',\n method_not_allowed: 'invalid_request',\n invalid_idempotent_request: 'invalid_request',\n concurrent_idempotent_requests: 'idempotency_conflict',\n invalid_attachment: 'invalid_request',\n invalid_from_address: 'invalid_request',\n invalid_access: 'configuration',\n invalid_parameter: 'invalid_request',\n invalid_region: 'invalid_request',\n missing_required_field: 'invalid_request',\n monthly_quota_exceeded: 'quota_exceeded',\n daily_quota_exceeded: 'quota_exceeded',\n rate_limit_exceeded: 'rate_limited',\n security_error: 'configuration',\n application_error: 'provider_error',\n internal_server_error: 'provider_error',\n} satisfies Record<ErrorResponse['name'], EmailSendFailureReason>\n\n// Resend documents these codes before its SDK includes them in ErrorResponse.\nconst DOCUMENTED_REASONS = new Map<string, EmailSendFailureReason>([\n ['email_above_quota', 'quota_exceeded'],\n ['invalid_permission', 'configuration'],\n ['suspended_api_key', 'configuration'],\n ['resource_locked', 'idempotency_conflict'],\n ['service_unavailable', 'provider_error'],\n ['missing_required_parameter', 'invalid_request'],\n])\n\nfunction reasonFromResend(error: ErrorResponse): EmailSendFailureReason {\n if (Object.hasOwn(SDK_REASONS, error.name)) return SDK_REASONS[error.name]\n const reason = DOCUMENTED_REASONS.get(error.name)\n if (reason !== undefined) return reason\n if (error.statusCode === 429) return 'rate_limited'\n if (error.statusCode !== null && error.statusCode >= 400 && error.statusCode < 500)\n return 'invalid_request'\n return 'provider_error'\n}\n\nfunction retryAfterSeconds(value: string | undefined): number | null {\n if (value === undefined || !/^\\d+$/.test(value)) return null\n const seconds = Number(value)\n return Number.isSafeInteger(seconds) ? seconds : null\n}\n\nfunction toAttachment(attachment: EmailAttachment): Attachment {\n const result: Attachment = {\n filename: attachment.filename,\n content: ArrayBuffer.isView(attachment.content)\n ? Buffer.from(attachment.content)\n : attachment.content,\n }\n if (attachment.contentType !== undefined) result.contentType = attachment.contentType\n return result\n}\n\n/** The SDK operations required by the adapter; a test factory can return a plain object. */\nexport type ResendEmailClient = { readonly emails: Pick<Resend['emails'], 'send'> }\n\n/**\n * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.\n * The Resend SDK itself logs API errors outside production.\n */\nexport class ResendEmailService implements IEmailService {\n /** Load the SDK on the first send. Constructing the service does not load it. */\n static create(apiKey: string, fromAddress: string): ResendEmailService {\n return new ResendEmailService(\n () => import('resend').then(({ Resend }) => new Resend(apiKey)),\n fromAddress,\n )\n }\n\n private client: Promise<ResendEmailClient> | undefined\n\n /** Supply an async factory. The service calls it once, on the first send, and keeps its promise. */\n constructor(\n private readonly createClient: () => Promise<ResendEmailClient>,\n private readonly fromAddress: string,\n ) {}\n\n /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */\n async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {\n if (this.client === undefined) {\n // Cache synchronous factory throws as rejections too, so later sends cannot initialize again.\n this.client = Promise.resolve().then(this.createClient)\n }\n const client = await this.client\n const payload: CreateEmailOptions = {\n from: this.fromAddress,\n to: [email.to].flat(),\n subject: email.subject,\n html: email.html,\n }\n if (email.text !== undefined) payload.text = email.text\n if (email.replyTo !== undefined) payload.replyTo = email.replyTo\n if (email.tags !== undefined)\n payload.tags = Object.entries(email.tags).map(([name, value]) => ({ name, value }))\n if (email.attachments !== undefined) payload.attachments = email.attachments.map(toAttachment)\n const result = await client.emails.send(payload, { idempotencyKey: email.idempotencyKey })\n if (result.error !== null) {\n throw new EmailServiceError({\n reason: reasonFromResend(result.error),\n provider: 'resend',\n providerCode: result.error.name,\n statusCode: result.error.statusCode,\n retryAfter: retryAfterSeconds(result.headers?.['retry-after']),\n cause: result.error,\n })\n }\n return { providerId: result.data.id }\n }\n}\n","import type { WebhookEventPayload } from 'resend'\nimport { z } from 'zod'\n\nimport { EmailAddressSchema, parseBounceType, type BounceType } from '@/core'\n\nconst SIMPLE_EMAIL_TYPES = [\n 'email.sent',\n 'email.scheduled',\n 'email.delivered',\n 'email.delivery_delayed',\n 'email.complained',\n 'email.opened',\n 'email.received',\n 'email.suppressed',\n] as const\n\n/** Resend email event names supported by this module. */\nexport type EmailEventType =\n | (typeof SIMPLE_EMAIL_TYPES)[number]\n | 'email.bounced'\n | 'email.failed'\n | 'email.clicked'\n\nconst EMAIL_TYPES = {\n 'email.sent': true,\n 'email.scheduled': true,\n 'email.delivered': true,\n 'email.delivery_delayed': true,\n 'email.complained': true,\n 'email.opened': true,\n 'email.received': true,\n 'email.suppressed': true,\n 'email.bounced': true,\n 'email.failed': true,\n 'email.clicked': true,\n} satisfies Record<Extract<WebhookEventPayload['type'], `email.${string}`>, true>\n\nconst BaseDataSchema = z.object({\n email_id: z.string().min(1),\n to: z.array(EmailAddressSchema).min(1),\n subject: z.string(),\n tags: z.record(z.string(), z.string()).default({}),\n})\n\nconst EmailPayloadSchema = z.discriminatedUnion('type', [\n z.object({ type: z.enum(SIMPLE_EMAIL_TYPES), data: BaseDataSchema }),\n z.object({\n type: z.literal('email.bounced'),\n data: BaseDataSchema.extend({\n bounce: z.object({\n type: z.string().optional(),\n subType: z.string().nullable().optional(),\n message: z.string(),\n }),\n }),\n }),\n z.object({\n type: z.literal('email.failed'),\n data: BaseDataSchema.extend({ failed: z.object({ reason: z.string() }) }),\n }),\n z.object({\n type: z.literal('email.clicked'),\n data: BaseDataSchema.extend({ click: z.object({ link: z.string() }) }),\n }),\n])\n\ntype EmailEventBase = {\n readonly id: string\n readonly emailId: string\n readonly recipients: readonly string[]\n readonly subject: string\n readonly tags: Readonly<Record<string, string>>\n readonly occurredAt: string\n}\n\n/** A verified event. `id` is the Svix identity; use `resend:event:${id}` as the command id. */\nexport type EmailEvent =\n | (EmailEventBase & {\n readonly type: 'email.bounced'\n readonly bounce: {\n readonly type: BounceType\n readonly subType: string | null\n readonly message: string\n }\n })\n | (EmailEventBase & { readonly type: 'email.failed'; readonly reason: string })\n | (EmailEventBase & { readonly type: 'email.clicked'; readonly link: string })\n | (EmailEventBase & { readonly type: (typeof SIMPLE_EMAIL_TYPES)[number] })\n\nconst EventHeaderSchema = z.object({\n type: z.string(),\n created_at: z.iso.datetime({ offset: true }),\n})\n\n/** Validate signed provider data before projecting it into the public event contract. */\nexport function parseEmailEvent(\n payload: WebhookEventPayload,\n id: string,\n):\n | { readonly outcome: 'event'; readonly event: EmailEvent }\n | { readonly outcome: 'ignored'; readonly type: string }\n | { readonly outcome: 'invalid' } {\n const header = EventHeaderSchema.safeParse(payload)\n if (!header.success) return { outcome: 'invalid' }\n if (!Object.hasOwn(EMAIL_TYPES, header.data.type))\n return { outcome: 'ignored', type: header.data.type }\n const parsed = EmailPayloadSchema.safeParse(payload)\n if (!parsed.success) return { outcome: 'invalid' }\n const event = parsed.data\n const base: EmailEventBase = {\n id,\n emailId: event.data.email_id,\n recipients: event.data.to,\n subject: event.data.subject,\n tags: event.data.tags,\n occurredAt: header.data.created_at,\n }\n switch (event.type) {\n case 'email.bounced':\n return {\n outcome: 'event',\n event: {\n ...base,\n type: event.type,\n bounce: {\n type: parseBounceType(event.data.bounce.type),\n subType: event.data.bounce.subType ?? null,\n message: event.data.bounce.message,\n },\n },\n }\n case 'email.failed':\n return {\n outcome: 'event',\n event: { ...base, type: event.type, reason: event.data.failed.reason },\n }\n case 'email.clicked':\n return { outcome: 'event', event: { ...base, type: event.type, link: event.data.click.link } }\n default:\n return { outcome: 'event', event: { ...base, type: event.type } }\n }\n}\n","import type { Resend } from 'resend'\nimport { z } from 'zod'\n\nimport {\n WebhookVerificationError,\n type WebhookVerification,\n type WebhookVerifier,\n} from '../http/webhook-verification.ts'\nimport { parseEmailEvent, type EmailEvent } from './email-event.ts'\n\n// Separate standardwebhooks versions have different prototypes; its error name remains the same.\nconst SignatureErrorSchema = z\n .instanceof(Error)\n .refine((error) => error.name === 'WebhookVerificationError')\n\n/** The SDK's local signature verifier. It makes no API request. */\nexport type ResendWebhookClient = Pick<Resend['webhooks'], 'verify'>\n\n/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */\nexport class ResendWebhook implements WebhookVerifier<EmailEvent> {\n /** Load the SDK on the first verification. Only the webhook secret is required. */\n static create(webhookSecret: string): ResendWebhook {\n return new ResendWebhook(webhookSecret, async () => {\n const { Resend } = await import('resend')\n // The SDK requires a nonempty key even though its local verifier never uses that key.\n return new Resend('re_webhook_verification_only').webhooks\n })\n }\n\n private client: Promise<ResendWebhookClient> | undefined\n\n /** Inject the local SDK verifier when testing. */\n constructor(\n private readonly webhookSecret: string,\n private readonly verifier: () => Promise<ResendWebhookClient>,\n ) {}\n\n /** Verify the raw body and Svix headers. Invalid requests return rejected; configuration and SDK defects propagate. */\n async verify(body: string, headers: Headers): Promise<WebhookVerification<EmailEvent>> {\n const id = headers.get('svix-id')\n const timestamp = headers.get('svix-timestamp')\n const signature = headers.get('svix-signature')\n if (!id || !timestamp || !signature)\n return { outcome: 'rejected', error: new WebhookVerificationError('missing_headers') }\n const client = await (this.client ??= this.verifier())\n let payload: ReturnType<ResendWebhookClient['verify']>\n try {\n payload = client.verify({\n payload: body,\n headers: { id, timestamp, signature },\n webhookSecret: this.webhookSecret,\n })\n } catch (cause) {\n if (cause instanceof SyntaxError)\n return {\n outcome: 'rejected',\n error: new WebhookVerificationError('invalid_payload', cause),\n }\n if (!SignatureErrorSchema.safeParse(cause).success) throw cause\n return {\n outcome: 'rejected',\n error: new WebhookVerificationError('invalid_signature', cause),\n }\n }\n const result = parseEmailEvent(payload, id)\n if (result.outcome === 'invalid')\n return { outcome: 'rejected', error: new WebhookVerificationError('invalid_payload') }\n return result\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resend.js","names":[],"sources":["../src/infrastructure/resend/resend-email-service.ts","../src/infrastructure/resend/email-event.ts","../src/infrastructure/resend/resend-webhook.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nimport type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'\n\nimport type { EmailAccepted, EmailAttachment, IEmailService, OutboundEmail } from '@/application'\nimport { EmailServiceError, type EmailSendFailureReason } from '@/core'\n\n// SDK additions must receive an explicit classification before the package builds.\nconst SDK_REASONS = {\n invalid_idempotency_key: 'invalid_request',\n validation_error: 'invalid_request',\n missing_api_key: 'configuration',\n restricted_api_key: 'configuration',\n invalid_api_key: 'configuration',\n not_found: 'invalid_request',\n method_not_allowed: 'invalid_request',\n invalid_idempotent_request: 'invalid_request',\n concurrent_idempotent_requests: 'idempotency_conflict',\n invalid_attachment: 'invalid_request',\n invalid_from_address: 'invalid_request',\n invalid_access: 'configuration',\n invalid_parameter: 'invalid_request',\n invalid_region: 'invalid_request',\n missing_required_field: 'invalid_request',\n monthly_quota_exceeded: 'quota_exceeded',\n daily_quota_exceeded: 'quota_exceeded',\n rate_limit_exceeded: 'rate_limited',\n security_error: 'configuration',\n application_error: 'provider_error',\n internal_server_error: 'provider_error',\n} satisfies Record<ErrorResponse['name'], EmailSendFailureReason>\n\n// Resend documents these codes before its SDK includes them in ErrorResponse.\nconst DOCUMENTED_REASONS = new Map<string, EmailSendFailureReason>([\n ['email_above_quota', 'quota_exceeded'],\n ['invalid_permission', 'configuration'],\n ['suspended_api_key', 'configuration'],\n ['resource_locked', 'idempotency_conflict'],\n ['service_unavailable', 'provider_error'],\n ['missing_required_parameter', 'invalid_request'],\n])\n\nfunction reasonFromResend(error: ErrorResponse): EmailSendFailureReason {\n if (Object.hasOwn(SDK_REASONS, error.name)) return SDK_REASONS[error.name]\n const reason = DOCUMENTED_REASONS.get(error.name)\n if (reason !== undefined) return reason\n if (error.statusCode === 429) return 'rate_limited'\n if (error.statusCode !== null && error.statusCode >= 400 && error.statusCode < 500)\n return 'invalid_request'\n return 'provider_error'\n}\n\nfunction retryAfterSeconds(value: string | undefined): number | null {\n if (value === undefined || !/^\\d+$/.test(value)) return null\n const seconds = Number(value)\n return Number.isSafeInteger(seconds) ? seconds : null\n}\n\nfunction toAttachment(attachment: EmailAttachment): Attachment {\n const result: Attachment = {\n filename: attachment.filename,\n content: ArrayBuffer.isView(attachment.content)\n ? Buffer.from(attachment.content)\n : attachment.content,\n }\n if (attachment.contentType !== undefined) result.contentType = attachment.contentType\n return result\n}\n\n/** The SDK operations required by the adapter; a test factory can return a plain object. */\nexport type ResendEmailClient = { readonly emails: Pick<Resend['emails'], 'send'> }\n\n/**\n * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.\n * The Resend SDK itself logs API errors outside production.\n */\nexport class ResendEmailService implements IEmailService {\n /** Load the SDK on the first send. Constructing the service does not load it. */\n static create(apiKey: string, fromAddress: string): ResendEmailService {\n return new ResendEmailService(\n () => import('resend').then(({ Resend }) => new Resend(apiKey)),\n fromAddress,\n )\n }\n\n private client: Promise<ResendEmailClient> | undefined\n\n /** Supply an async factory. The service calls it once, on the first send, and keeps its promise. */\n constructor(\n private readonly createClient: () => Promise<ResendEmailClient>,\n private readonly fromAddress: string,\n ) {}\n\n /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */\n async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {\n if (this.client === undefined) {\n // Cache synchronous factory throws as rejections too, so later sends cannot initialize again.\n this.client = Promise.resolve().then(this.createClient)\n }\n const client = await this.client\n const payload: CreateEmailOptions = {\n from: email.from ?? this.fromAddress,\n to: [email.to].flat(),\n subject: email.subject,\n html: email.html,\n }\n if (email.text !== undefined) payload.text = email.text\n if (email.replyTo !== undefined) payload.replyTo = email.replyTo\n if (email.tags !== undefined)\n payload.tags = Object.entries(email.tags).map(([name, value]) => ({ name, value }))\n if (email.attachments !== undefined) payload.attachments = email.attachments.map(toAttachment)\n const result = await client.emails.send(payload, { idempotencyKey: email.idempotencyKey })\n if (result.error !== null) {\n throw new EmailServiceError({\n reason: reasonFromResend(result.error),\n provider: 'resend',\n providerCode: result.error.name,\n statusCode: result.error.statusCode,\n retryAfter: retryAfterSeconds(result.headers?.['retry-after']),\n cause: result.error,\n })\n }\n return { providerId: result.data.id }\n }\n}\n","import type { WebhookEventPayload } from 'resend'\nimport { z } from 'zod'\n\nimport { EmailAddressSchema, parseBounceType, type BounceType } from '@/core'\n\nconst SIMPLE_EMAIL_TYPES = [\n 'email.sent',\n 'email.scheduled',\n 'email.delivered',\n 'email.delivery_delayed',\n 'email.complained',\n 'email.opened',\n 'email.received',\n 'email.suppressed',\n] as const\n\n/** Resend email event names supported by this module. */\nexport type EmailEventType =\n | (typeof SIMPLE_EMAIL_TYPES)[number]\n | 'email.bounced'\n | 'email.failed'\n | 'email.clicked'\n\nconst EMAIL_TYPES = {\n 'email.sent': true,\n 'email.scheduled': true,\n 'email.delivered': true,\n 'email.delivery_delayed': true,\n 'email.complained': true,\n 'email.opened': true,\n 'email.received': true,\n 'email.suppressed': true,\n 'email.bounced': true,\n 'email.failed': true,\n 'email.clicked': true,\n} satisfies Record<Extract<WebhookEventPayload['type'], `email.${string}`>, true>\n\nconst BaseDataSchema = z.object({\n email_id: z.string().min(1),\n to: z.array(EmailAddressSchema).min(1),\n subject: z.string(),\n tags: z.record(z.string(), z.string()).default({}),\n})\n\nconst EmailPayloadSchema = z.discriminatedUnion('type', [\n z.object({ type: z.enum(SIMPLE_EMAIL_TYPES), data: BaseDataSchema }),\n z.object({\n type: z.literal('email.bounced'),\n data: BaseDataSchema.extend({\n bounce: z.object({\n type: z.string().optional(),\n subType: z.string().nullable().optional(),\n message: z.string(),\n }),\n }),\n }),\n z.object({\n type: z.literal('email.failed'),\n data: BaseDataSchema.extend({ failed: z.object({ reason: z.string() }) }),\n }),\n z.object({\n type: z.literal('email.clicked'),\n data: BaseDataSchema.extend({ click: z.object({ link: z.string() }) }),\n }),\n])\n\ntype EmailEventBase = {\n readonly id: string\n readonly emailId: string\n readonly recipients: readonly string[]\n readonly subject: string\n readonly tags: Readonly<Record<string, string>>\n readonly occurredAt: string\n}\n\n/** A verified event. `id` is the Svix identity; use `resend:event:${id}` as the command id. */\nexport type EmailEvent =\n | (EmailEventBase & {\n readonly type: 'email.bounced'\n readonly bounce: {\n readonly type: BounceType\n readonly subType: string | null\n readonly message: string\n }\n })\n | (EmailEventBase & { readonly type: 'email.failed'; readonly reason: string })\n | (EmailEventBase & { readonly type: 'email.clicked'; readonly link: string })\n | (EmailEventBase & { readonly type: (typeof SIMPLE_EMAIL_TYPES)[number] })\n\nconst EventHeaderSchema = z.object({\n type: z.string(),\n created_at: z.iso.datetime({ offset: true }),\n})\n\n/** Validate signed provider data before projecting it into the public event contract. */\nexport function parseEmailEvent(\n payload: WebhookEventPayload,\n id: string,\n):\n | { readonly outcome: 'event'; readonly event: EmailEvent }\n | { readonly outcome: 'ignored'; readonly type: string }\n | { readonly outcome: 'invalid' } {\n const header = EventHeaderSchema.safeParse(payload)\n if (!header.success) return { outcome: 'invalid' }\n if (!Object.hasOwn(EMAIL_TYPES, header.data.type))\n return { outcome: 'ignored', type: header.data.type }\n const parsed = EmailPayloadSchema.safeParse(payload)\n if (!parsed.success) return { outcome: 'invalid' }\n const event = parsed.data\n const base: EmailEventBase = {\n id,\n emailId: event.data.email_id,\n recipients: event.data.to,\n subject: event.data.subject,\n tags: event.data.tags,\n occurredAt: header.data.created_at,\n }\n switch (event.type) {\n case 'email.bounced':\n return {\n outcome: 'event',\n event: {\n ...base,\n type: event.type,\n bounce: {\n type: parseBounceType(event.data.bounce.type),\n subType: event.data.bounce.subType ?? null,\n message: event.data.bounce.message,\n },\n },\n }\n case 'email.failed':\n return {\n outcome: 'event',\n event: { ...base, type: event.type, reason: event.data.failed.reason },\n }\n case 'email.clicked':\n return { outcome: 'event', event: { ...base, type: event.type, link: event.data.click.link } }\n default:\n return { outcome: 'event', event: { ...base, type: event.type } }\n }\n}\n","import type { Resend } from 'resend'\nimport { z } from 'zod'\n\nimport {\n WebhookVerificationError,\n type WebhookVerification,\n type WebhookVerifier,\n} from '../http/webhook-verification.ts'\nimport { parseEmailEvent, type EmailEvent } from './email-event.ts'\n\n// Separate standardwebhooks versions have different prototypes; its error name remains the same.\nconst SignatureErrorSchema = z\n .instanceof(Error)\n .refine((error) => error.name === 'WebhookVerificationError')\n\n/** The SDK's local signature verifier. It makes no API request. */\nexport type ResendWebhookClient = Pick<Resend['webhooks'], 'verify'>\n\n/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */\nexport class ResendWebhook implements WebhookVerifier<EmailEvent> {\n /** Load the SDK on the first verification. Only the webhook secret is required. */\n static create(webhookSecret: string): ResendWebhook {\n return new ResendWebhook(webhookSecret, async () => {\n const { Resend } = await import('resend')\n // The SDK requires a nonempty key even though its local verifier never uses that key.\n return new Resend('re_webhook_verification_only').webhooks\n })\n }\n\n private client: Promise<ResendWebhookClient> | undefined\n\n /** Inject the local SDK verifier when testing. */\n constructor(\n private readonly webhookSecret: string,\n private readonly verifier: () => Promise<ResendWebhookClient>,\n ) {}\n\n /** Verify the raw body and Svix headers. Invalid requests return rejected; configuration and SDK defects propagate. */\n async verify(body: string, headers: Headers): Promise<WebhookVerification<EmailEvent>> {\n const id = headers.get('svix-id')\n const timestamp = headers.get('svix-timestamp')\n const signature = headers.get('svix-signature')\n if (!id || !timestamp || !signature)\n return { outcome: 'rejected', error: new WebhookVerificationError('missing_headers') }\n const client = await (this.client ??= this.verifier())\n let payload: ReturnType<ResendWebhookClient['verify']>\n try {\n payload = client.verify({\n payload: body,\n headers: { id, timestamp, signature },\n webhookSecret: this.webhookSecret,\n })\n } catch (cause) {\n if (cause instanceof SyntaxError)\n return {\n outcome: 'rejected',\n error: new WebhookVerificationError('invalid_payload', cause),\n }\n if (!SignatureErrorSchema.safeParse(cause).success) throw cause\n return {\n outcome: 'rejected',\n error: new WebhookVerificationError('invalid_signature', cause),\n }\n }\n const result = parseEmailEvent(payload, id)\n if (result.outcome === 'invalid')\n return { outcome: 'rejected', error: new WebhookVerificationError('invalid_payload') }\n return result\n }\n}\n"],"mappings":"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"}
|
package/dist/tanstack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { st as ErrorTable } from "./index-B5O6mNjZ.js";
|
|
2
2
|
import { Register } from "@tanstack/react-start";
|
|
3
3
|
import { BetterAuthCookies } from "better-auth";
|
|
4
4
|
//#region src/infrastructure/tanstack/request-context.d.ts
|
package/dist/tanstack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as AppError, L as ValidationError, U as AuthenticationError, V as StaleSessionError, a as createLogger } from "./core-BiqdOMwU.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getReasonPhrase } from "http-status-codes";
|
|
4
4
|
import { createMiddleware } from "@tanstack/react-start";
|