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 { $ as AppError, D as DatabaseError, E as unreachable, a as createLogger, f as classifyFailure } from "./core-BiqdOMwU.js";
|
|
2
2
|
import { panic } from "better-result";
|
|
3
3
|
|
|
4
4
|
//#region src/application/email-service.ts
|
|
@@ -52,7 +52,7 @@ var MessageAlreadyProcessedError = class MessageAlreadyProcessedError extends Ap
|
|
|
52
52
|
|
|
53
53
|
//#endregion
|
|
54
54
|
//#region src/application/message-bus.ts
|
|
55
|
-
const logger = createLogger("message-bus");
|
|
55
|
+
const logger$1 = createLogger("message-bus");
|
|
56
56
|
/**
|
|
57
57
|
* Routes one message to its slot. The bus opens no transaction and keeps no outbox:
|
|
58
58
|
* a handler that writes opens the unit of work itself.
|
|
@@ -77,7 +77,7 @@ var MessageBus = class {
|
|
|
77
77
|
handleCommand(command) {
|
|
78
78
|
const handler = this.registry.commands[command.name];
|
|
79
79
|
if (handler === void 0) return panic(`No handler for command ${command.name}`);
|
|
80
|
-
logger.debug("handling_command", {
|
|
80
|
+
logger$1.debug("handling_command", {
|
|
81
81
|
name: command.name,
|
|
82
82
|
id: command.id
|
|
83
83
|
});
|
|
@@ -100,7 +100,7 @@ var MessageBus = class {
|
|
|
100
100
|
for (const [subscriber, outcome] of outcomes.entries()) {
|
|
101
101
|
if (outcome.status === "fulfilled") continue;
|
|
102
102
|
if (outcome.reason instanceof MessageAlreadyProcessedError) {
|
|
103
|
-
logger.info("duplicate_subscriber_ignored", {
|
|
103
|
+
logger$1.info("duplicate_subscriber_ignored", {
|
|
104
104
|
event: event.name,
|
|
105
105
|
id: event.id,
|
|
106
106
|
subscriber
|
|
@@ -114,6 +114,118 @@ var MessageBus = class {
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/application/outbox-relay.ts
|
|
119
|
+
const logger = createLogger("outbox-relay");
|
|
120
|
+
/**
|
|
121
|
+
* Publishes committed events with bounded durable retries. Ported from Typist's relay with per-row exhaustion.
|
|
122
|
+
* Send failures are recorded; repository failures propagate to the wake or scheduled boundary.
|
|
123
|
+
*/
|
|
124
|
+
var OutboxRelay = class {
|
|
125
|
+
publisher;
|
|
126
|
+
repository;
|
|
127
|
+
options;
|
|
128
|
+
constructor(publisher, repository, options = {}) {
|
|
129
|
+
this.publisher = publisher;
|
|
130
|
+
this.repository = repository;
|
|
131
|
+
this.options = {
|
|
132
|
+
batchSize: options.batchSize ?? 50,
|
|
133
|
+
maxBatches: options.maxBatches ?? 10,
|
|
134
|
+
maxAttempts: options.maxAttempts ?? 10
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Return the claimed count, including released and dead-lettered rows. Zero means no due rows. */
|
|
138
|
+
async flush() {
|
|
139
|
+
const claim = await this.repository.claimPending(this.options.batchSize);
|
|
140
|
+
const exhausted = claim.messages.filter((row) => row.attempts > this.options.maxAttempts);
|
|
141
|
+
await this.deadLetter(claim, exhausted, "exhausted", "Claim attempt limit exceeded");
|
|
142
|
+
const pending = claim.messages.filter((row) => row.attempts <= this.options.maxAttempts);
|
|
143
|
+
if (pending.length === 0) return claim.messages.length;
|
|
144
|
+
let oversized;
|
|
145
|
+
try {
|
|
146
|
+
oversized = await this.publisher.sendBatchPartial(pending.map((row) => row.payload));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
await this.handleSendFailure(claim, pending, error);
|
|
149
|
+
return claim.messages.length;
|
|
150
|
+
}
|
|
151
|
+
const oversizedIds = new Set(oversized.map((event) => event.id));
|
|
152
|
+
await this.deadLetter(claim, pending.filter((row) => oversizedIds.has(row.payload.id)), "oversized", "Exceeds queue message size limit");
|
|
153
|
+
const sent = pending.filter((row) => !oversizedIds.has(row.payload.id));
|
|
154
|
+
if (sent.length > 0) await this.repository.markPublished(claim, sent);
|
|
155
|
+
return claim.messages.length;
|
|
156
|
+
}
|
|
157
|
+
/** Drain until an empty claim or the batch limit; return the total claimed count. */
|
|
158
|
+
async drain() {
|
|
159
|
+
let total = 0;
|
|
160
|
+
for (let batch = 0; batch < this.options.maxBatches; batch += 1) {
|
|
161
|
+
const claimed = await this.flush();
|
|
162
|
+
total += claimed;
|
|
163
|
+
if (claimed === 0) break;
|
|
164
|
+
}
|
|
165
|
+
return total;
|
|
166
|
+
}
|
|
167
|
+
async handleSendFailure(claim, pending, cause) {
|
|
168
|
+
const classification = classifyFailure(cause);
|
|
169
|
+
const message = cause instanceof Error ? cause.message : "Publisher rejected with a non-error value";
|
|
170
|
+
if (classification === "terminal") {
|
|
171
|
+
await this.deadLetter(claim, pending, "terminal", message, cause);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const exhausted = pending.filter((row) => row.attempts >= this.options.maxAttempts);
|
|
175
|
+
await this.deadLetter(claim, exhausted, "exhausted", message, cause);
|
|
176
|
+
const retry = pending.filter((row) => row.attempts < this.options.maxAttempts);
|
|
177
|
+
if (retry.length > 0) await this.repository.releaseClaim(claim, retry);
|
|
178
|
+
}
|
|
179
|
+
async deadLetter(claim, messages, reason, errorMessage, cause) {
|
|
180
|
+
if (messages.length === 0) return;
|
|
181
|
+
await this.repository.deadLetter(claim, messages, reason, errorMessage);
|
|
182
|
+
logger.error("outbox_messages_dead_lettered", {
|
|
183
|
+
error: cause,
|
|
184
|
+
details: {
|
|
185
|
+
claimToken: claim.token,
|
|
186
|
+
count: messages.length,
|
|
187
|
+
reason
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/application/outbox-retention.ts
|
|
195
|
+
/** Bounded cleanup ported from Typist's message retention handler. The application schedules each sweep. */
|
|
196
|
+
var OutboxRetention = class {
|
|
197
|
+
repository;
|
|
198
|
+
options;
|
|
199
|
+
constructor(repository, options = {}) {
|
|
200
|
+
this.repository = repository;
|
|
201
|
+
this.options = {
|
|
202
|
+
publishedRetentionMs: options.publishedRetentionMs ?? 864e5,
|
|
203
|
+
failedRetentionMs: options.failedRetentionMs ?? 2592e6,
|
|
204
|
+
receiptRetentionMs: options.receiptRetentionMs ?? 6048e5,
|
|
205
|
+
batchSize: options.batchSize ?? 1e3,
|
|
206
|
+
maxBatches: options.maxBatches ?? 10
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Calculate all cutoffs from one supplied time; propagate repository failures to the calling boundary. */
|
|
210
|
+
async sweep(at) {
|
|
211
|
+
const { publishedRetentionMs, failedRetentionMs, receiptRetentionMs } = this.options;
|
|
212
|
+
return {
|
|
213
|
+
published: await this.deleteBatches((size) => this.repository.deletePublishedBefore(new Date(at.getTime() - publishedRetentionMs), size)),
|
|
214
|
+
failed: await this.deleteBatches((size) => this.repository.deleteFailedBefore(new Date(at.getTime() - failedRetentionMs), size)),
|
|
215
|
+
receipts: await this.deleteBatches((size) => this.repository.deleteReceiptsBefore(new Date(at.getTime() - receiptRetentionMs), size))
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async deleteBatches(deleteBatch) {
|
|
219
|
+
let total = 0;
|
|
220
|
+
for (let batch = 0; batch < this.options.maxBatches; batch += 1) {
|
|
221
|
+
const deleted = await deleteBatch(this.options.batchSize);
|
|
222
|
+
total += deleted;
|
|
223
|
+
if (deleted < this.options.batchSize) break;
|
|
224
|
+
}
|
|
225
|
+
return total;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
117
229
|
//#endregion
|
|
118
230
|
//#region src/application/unit-of-work.ts
|
|
119
231
|
/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */
|
|
@@ -156,5 +268,5 @@ var UnitOfWorkAlreadyActiveError = class extends AppError {
|
|
|
156
268
|
};
|
|
157
269
|
|
|
158
270
|
//#endregion
|
|
159
|
-
export {
|
|
160
|
-
//# sourceMappingURL=application-
|
|
271
|
+
export { MessageBus as a, RecordingEmailService as c, OutboxRelay as i, UnitOfWork as n, MessageAlreadyProcessedError as o, OutboxRetention as r, recordEmailEvent as s, UnitOfWorkAlreadyActiveError as t };
|
|
272
|
+
//# sourceMappingURL=application-DnlgWcOZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"application-DnlgWcOZ.js","names":["logger"],"sources":["../src/application/email-service.ts","../src/application/record-email-event.ts","../src/application/message-already-processed.error.ts","../src/application/message-bus.ts","../src/application/outbox-relay.ts","../src/application/outbox-retention.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import type { EmailIdempotencyKey } from '@/core'\n\n/** A media type such as `application/pdf`. */\nexport type MimeType = `${string}/${string}`\n\n/** A file to attach. Binary content is bytes; the adapter encodes it for the provider. */\nexport type EmailAttachment = {\n readonly filename: string\n readonly content: string | Uint8Array\n /** Optional MIME metadata; Resend accepts filename and content without it. */\n readonly contentType?: MimeType\n}\n\n/**\n * One transactional email. Resend accepts 50 recipients, 75 tags with ASCII names and values,\n * and 40 MB of attachments after base64. Sources: https://resend.com/docs/api-reference/emails/send-email\n * and https://resend.com/docs/dashboard/emails/tags\n */\nexport type OutboundEmail = {\n /** Override the default for this email, for example \"Display Name <address@domain.com>\". */\n readonly from?: string\n readonly to: string | readonly string[]\n readonly subject: string\n readonly html: string\n /** The stable identity of this send. Resend deduplicates it for 24 hours. */\n readonly idempotencyKey: EmailIdempotencyKey\n /** Resend generates plain text from html when omitted. An empty string disables that generation. */\n readonly text?: string\n readonly replyTo?: string\n /** Labels the provider stores with the email and returns on every delivery event. */\n readonly tags?: Readonly<Record<string, string>>\n readonly attachments?: readonly EmailAttachment[]\n}\n\n/** The provider accepted the email and assigned its id. */\nexport type EmailAccepted = { readonly providerId: string }\n\n/**\n * The adapter supplies the default sender and maps provider errors. Each email can override the sender.\n * A provider rejection throws `EmailServiceError`; unexpected SDK rejections propagate unchanged.\n */\nexport interface IEmailService {\n /** Resolve after provider acceptance; this does not confirm delivery to the recipient. */\n sendEmail(email: OutboundEmail): Promise<EmailAccepted>\n}\n\n/** Records every send for a test. Provider ids count up as `recorded-1`, `recorded-2`. */\nexport class RecordingEmailService implements IEmailService {\n /** Sends in call order, including repeated idempotency keys. */\n readonly sent: OutboundEmail[] = []\n\n /** Record the send before returning its provider id. */\n async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {\n this.sent.push(email)\n return { providerId: `recorded-${this.sent.length}` }\n }\n}\n","import type { BounceType, DomainCommand, EmailAddress, RecordEmailEventPayload } from '@/core'\n\nimport type { IUnitOfWork } from './unit-of-work.ts'\n\n/** What a bounce records for one address. */\nexport type EmailBounceRecord = {\n readonly type: BounceType\n readonly subType: string | null\n}\n\n/**\n * The application's deliverability state per address. An implementation keeps the higher bounce\n * severity with `shouldUpdateBounce`, and treats an unknown address as nothing to record.\n */\nexport interface IEmailDeliverabilityStore {\n /** Keep the higher severity atomically. Equal severity permits replacement; the store owns timestamp ordering. */\n recordBounce(address: EmailAddress, bounce: EmailBounceRecord): Promise<void>\n /** Record a complaint without clearing existing suppression state. */\n recordComplaint(address: EmailAddress): Promise<void>\n}\n\n/** The repositories the handler needs from the unit of work. */\nexport type RecordEmailEventRepos = { readonly emailDeliverability: IEmailDeliverabilityStore }\n\n/** The container slice the handler reads. */\nexport type RecordEmailEventDeps = { readonly uow: IUnitOfWork<RecordEmailEventRepos> }\n\n/** The command, under whatever name the application registered the payload. */\nexport type RecordEmailEventCommand = DomainCommand & RecordEmailEventPayload\n\n/**\n * Record a bounce or a complaint for every recipient inside the command's unit of work.\n * The command id is the provider's event id, so a redelivered webhook is `MessageAlreadyProcessedError`.\n */\nexport async function recordEmailEvent(\n command: RecordEmailEventCommand,\n deps: RecordEmailEventDeps,\n): Promise<void> {\n await deps.uow.run(command.id, async ({ emailDeliverability }) => {\n for (const address of command.recipients) {\n if (command.event.type === 'bounced') {\n // oxlint-disable-next-line no-await-in-loop -- Finish each transaction write before starting another; a failure must leave no pending writes.\n await emailDeliverability.recordBounce(address, {\n type: command.event.bounceType,\n subType: command.event.bounceSubType,\n })\n } else {\n // oxlint-disable-next-line no-await-in-loop -- A rejected write must stop the transaction before another write starts.\n await emailDeliverability.recordComplaint(address)\n }\n }\n })\n}\n","import { AppError, DatabaseError, type MessageId } from '@/core'\n\n/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */\nexport class MessageAlreadyProcessedError extends AppError {\n readonly _tag = 'MessageAlreadyProcessedError'\n readonly classification = 'terminal'\n\n constructor(\n readonly messageId: MessageId,\n options?: { readonly cause: unknown },\n ) {\n super(`message ${messageId} already processed`, options)\n }\n\n /** Recognize only the processed-message primary key; preserve other failures unchanged at the call site. */\n static from(cause: unknown, messageId: MessageId): MessageAlreadyProcessedError | undefined {\n if (cause instanceof MessageAlreadyProcessedError) return cause\n if (!(cause instanceof DatabaseError)) return undefined\n const details = cause.details\n const duplicate =\n details.database === 'd1'\n ? details.kind === 'unique' &&\n details.table === 'processed_messages' &&\n details.column === 'message_id'\n : details.code === '23505' &&\n details.table_name === 'processed_messages' &&\n details.constraint_name === 'processed_messages_pkey'\n if (!duplicate) return undefined\n return new MessageAlreadyProcessedError(messageId, { cause })\n }\n}\n","import { panic } from 'better-result'\n\nimport {\n createLogger,\n unreachable,\n type DomainCommand,\n type DomainEvent,\n type DomainMessage,\n type DomainQuery,\n} from '@/core'\n\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { Handlers, MessageResult, RegistryMessage } from './registry.ts'\n\nconst logger = createLogger('message-bus')\n\n/** The port an entrypoint dispatches through. `AppDeps` names this as `IMessageBus<typeof registry>`. */\nexport interface IMessageBus<TRegistry extends Handlers<never>> {\n handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>>\n}\n\n/**\n * Routes one message to its slot. The bus opens no transaction and keeps no outbox:\n * a handler that writes opens the unit of work itself.\n */\nexport class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMessageBus<TRegistry> {\n constructor(\n private readonly deps: Deps,\n private readonly registry: TRegistry,\n ) {}\n\n /** Resolves to the handler's own value for a command or query, and to void for an event. */\n async handle<TMessage extends DomainMessage & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>> {\n // SAFETY: the slot for this name is what `MessageResult` names, and dispatch returns its value.\n return (await this.dispatch(message)) as MessageResult<TMessage, TRegistry>\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- one table holds every slot's value; `handle` names it.\n private dispatch(message: DomainMessage): Promise<unknown> {\n if (message.type === 'command') return this.handleCommand(message)\n if (message.type === 'query') return this.handleQuery(message)\n if (message.type === 'event') return this.handleEvent(message)\n // A message parsed at a boundary cannot reach here; one that does must not resolve silently.\n return unreachable(message)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleCommand(command: DomainCommand): Promise<unknown> {\n // `satisfies` proved the registry complete, so a missing slot is a broken registry object.\n const handler = this.registry.commands[command.name]\n if (handler === undefined) return panic(`No handler for command ${command.name}`)\n logger.debug('handling_command', { name: command.name, id: command.id })\n return handler(command, this.deps)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleQuery(query: DomainQuery): Promise<unknown> {\n const handler = this.registry.queries[query.name]\n if (handler === undefined) return panic(`No handler for query ${query.name}`)\n return handler(query, this.deps)\n }\n\n /**\n * Every subscriber runs to completion. One failure is rethrown as is. Several are thrown as\n * one `AggregateError`. A `MessageAlreadyProcessedError` means that subscriber already committed on\n * an earlier delivery, so it counts as delivered.\n */\n private async handleEvent(event: DomainEvent): Promise<void> {\n const subscriptions = this.registry.events[event.name] ?? []\n const outcomes = await Promise.allSettled(\n subscriptions.map((subscription) => subscription(event, this.deps)),\n )\n const failures: unknown[] = []\n for (const [subscriber, outcome] of outcomes.entries()) {\n if (outcome.status === 'fulfilled') continue\n if (outcome.reason instanceof MessageAlreadyProcessedError) {\n // The bus swallows this error, so this line is the only record that the skip happened.\n logger.info('duplicate_subscriber_ignored', { event: event.name, id: event.id, subscriber })\n continue\n }\n failures.push(outcome.reason)\n }\n if (failures.length === 1) throw failures[0]\n if (failures.length > 1) {\n throw new AggregateError(failures, `${event.name}: ${failures.length} subscribers failed`)\n }\n }\n}\n","import { classifyFailure, createLogger, type DomainEvent } from '@/core'\n\nimport type {\n DeadLetterReason,\n IOutboxRepository,\n OutboxClaim,\n OutboxMessage,\n} from './outbox-repository.ts'\n\n/**\n * Delivers committed outbox rows. The unit of work only wakes it after a commit that wrote\n * events; the app decides what a wake is: `waitUntil`, a Durable Object alarm, nothing.\n * The sweep is the source of truth, so a lost wake loses no events.\n */\nexport interface IOutboxRelay {\n wake(): void\n}\n\n/** The publisher sends valid events and returns oversized events. QueueClient satisfies this port directly. */\nexport interface IOutboxPublisher {\n /** Resolves after queue acceptance; a rejected batch may already have sent some events. */\n sendBatchPartial(messages: ReadonlyArray<DomainEvent>): Promise<ReadonlyArray<DomainEvent>>\n}\n\n/** Optional limits for one relay; durable attempts exclude the publisher's immediate retries. */\nexport type OutboxRelayOptions = {\n readonly batchSize?: number\n readonly maxBatches?: number\n readonly maxAttempts?: number\n}\n\nconst logger = createLogger('outbox-relay')\n\n/**\n * Publishes committed events with bounded durable retries. Ported from Typist's relay with per-row exhaustion.\n * Send failures are recorded; repository failures propagate to the wake or scheduled boundary.\n */\nexport class OutboxRelay {\n private readonly options: Required<OutboxRelayOptions>\n\n constructor(\n private readonly publisher: IOutboxPublisher,\n private readonly repository: IOutboxRepository,\n options: OutboxRelayOptions = {},\n ) {\n this.options = {\n batchSize: options.batchSize ?? 50,\n maxBatches: options.maxBatches ?? 10,\n maxAttempts: options.maxAttempts ?? 10,\n }\n }\n\n /** Return the claimed count, including released and dead-lettered rows. Zero means no due rows. */\n async flush(): Promise<number> {\n const claim = await this.repository.claimPending(this.options.batchSize)\n const exhausted = claim.messages.filter((row) => row.attempts > this.options.maxAttempts)\n await this.deadLetter(claim, exhausted, 'exhausted', 'Claim attempt limit exceeded')\n const pending = claim.messages.filter((row) => row.attempts <= this.options.maxAttempts)\n if (pending.length === 0) return claim.messages.length\n\n let oversized: ReadonlyArray<DomainEvent>\n try {\n oversized = await this.publisher.sendBatchPartial(pending.map((row) => row.payload))\n } catch (error) {\n await this.handleSendFailure(claim, pending, error)\n return claim.messages.length\n }\n\n const oversizedIds = new Set(oversized.map((event) => event.id))\n await this.deadLetter(\n claim,\n pending.filter((row) => oversizedIds.has(row.payload.id)),\n 'oversized',\n 'Exceeds queue message size limit',\n )\n const sent = pending.filter((row) => !oversizedIds.has(row.payload.id))\n if (sent.length > 0) await this.repository.markPublished(claim, sent)\n return claim.messages.length\n }\n\n /** Drain until an empty claim or the batch limit; return the total claimed count. */\n async drain(): Promise<number> {\n let total = 0\n for (let batch = 0; batch < this.options.maxBatches; batch += 1) {\n // oxlint-disable-next-line no-await-in-loop -- An empty claim must stop the drain before another claim starts.\n const claimed = await this.flush()\n total += claimed\n if (claimed === 0) break\n }\n return total\n }\n\n private async handleSendFailure(\n claim: OutboxClaim,\n pending: ReadonlyArray<OutboxMessage>,\n cause: unknown,\n ): Promise<void> {\n const classification = classifyFailure(cause)\n const message =\n cause instanceof Error ? cause.message : 'Publisher rejected with a non-error value'\n if (classification === 'terminal') {\n await this.deadLetter(claim, pending, 'terminal', message, cause)\n return\n }\n const exhausted = pending.filter((row) => row.attempts >= this.options.maxAttempts)\n await this.deadLetter(claim, exhausted, 'exhausted', message, cause)\n const retry = pending.filter((row) => row.attempts < this.options.maxAttempts)\n if (retry.length > 0) await this.repository.releaseClaim(claim, retry)\n }\n\n private async deadLetter(\n claim: OutboxClaim,\n messages: ReadonlyArray<OutboxMessage>,\n reason: DeadLetterReason,\n errorMessage: string,\n cause?: unknown,\n ): Promise<void> {\n if (messages.length === 0) return\n await this.repository.deadLetter(claim, messages, reason, errorMessage)\n logger.error('outbox_messages_dead_lettered', {\n error: cause,\n details: { claimToken: claim.token, count: messages.length, reason },\n })\n }\n}\n","import type { IOutboxRetentionRepository } from './outbox-repository.ts'\n\n/** Typist's retention defaults; receipt retention must cover the application's supported replay period. */\nexport type OutboxRetentionOptions = {\n readonly publishedRetentionMs?: number\n readonly failedRetentionMs?: number\n readonly receiptRetentionMs?: number\n readonly batchSize?: number\n readonly maxBatches?: number\n}\n\n/** Deleted records per category during one bounded sweep. */\nexport type OutboxRetentionResult = {\n readonly published: number\n readonly failed: number\n readonly receipts: number\n}\n\n/** Bounded cleanup ported from Typist's message retention handler. The application schedules each sweep. */\nexport class OutboxRetention {\n private readonly options: Required<OutboxRetentionOptions>\n\n constructor(\n private readonly repository: IOutboxRetentionRepository,\n options: OutboxRetentionOptions = {},\n ) {\n this.options = {\n publishedRetentionMs: options.publishedRetentionMs ?? 86_400_000,\n failedRetentionMs: options.failedRetentionMs ?? 30 * 86_400_000,\n receiptRetentionMs: options.receiptRetentionMs ?? 7 * 86_400_000,\n batchSize: options.batchSize ?? 1_000,\n maxBatches: options.maxBatches ?? 10,\n }\n }\n\n /** Calculate all cutoffs from one supplied time; propagate repository failures to the calling boundary. */\n async sweep(at: Date): Promise<OutboxRetentionResult> {\n const { publishedRetentionMs, failedRetentionMs, receiptRetentionMs } = this.options\n const published = await this.deleteBatches((size) =>\n this.repository.deletePublishedBefore(new Date(at.getTime() - publishedRetentionMs), size),\n )\n const failed = await this.deleteBatches((size) =>\n this.repository.deleteFailedBefore(new Date(at.getTime() - failedRetentionMs), size),\n )\n const receipts = await this.deleteBatches((size) =>\n this.repository.deleteReceiptsBefore(new Date(at.getTime() - receiptRetentionMs), size),\n )\n return { published, failed, receipts }\n }\n\n private async deleteBatches(deleteBatch: (size: number) => Promise<number>): Promise<number> {\n let total = 0\n for (let batch = 0; batch < this.options.maxBatches; batch += 1) {\n // oxlint-disable-next-line no-await-in-loop -- The delete count decides whether another batch is needed.\n const deleted = await deleteBatch(this.options.batchSize)\n total += deleted\n if (deleted < this.options.batchSize) break\n }\n return total\n }\n}\n","import type { MessageId } from '@/core'\n\nimport type { IEventCollector } from './event-collector.ts'\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { IMessageStore } from './message-store.ts'\nimport type { IOutboxRelay } from './outbox-relay.ts'\nimport type { ITransaction } from './transaction.ts'\n\n/**\n * Commits the processed-message record, repository writes, and emitted events together before resolving.\n * An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.\n * Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.\n */\nexport interface IUnitOfWork<Repos> {\n run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>\n}\n\nexport type UnitOfWorkDeps<Tx, Repos> = {\n readonly transaction: ITransaction<Tx>\n /** The processed-messages and outbox repository for this transaction. */\n readonly messageStore: (tx: Tx) => IMessageStore\n /** Repositories participate in the current transaction. */\n readonly createRepositories: (tx: Tx) => Repos\n readonly eventCollector: IEventCollector\n readonly relay: IOutboxRelay\n}\n\n/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */\nexport class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {\n constructor(private readonly deps: UnitOfWorkDeps<Tx, Repos>) {}\n\n async run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T> {\n const { transaction, messageStore, createRepositories, eventCollector, relay } = this.deps\n let written = 0\n\n let value: T\n try {\n value = await transaction.transaction(async (tx) => {\n const store = messageStore(tx)\n await store.claim(messageId)\n const repos = createRepositories(tx)\n const result = await work(repos)\n const pending = eventCollector.collect()\n await store.persistEvents(pending)\n written = pending.length\n return result\n })\n } catch (error) {\n throw MessageAlreadyProcessedError.from(error, messageId) ?? error\n }\n\n if (written > 0) relay.wake()\n return value\n }\n}\n","import { AppError } from '@/core'\n\n/** A boundary was opened inside another. One message, one atomic scope. */\nexport class UnitOfWorkAlreadyActiveError extends AppError {\n readonly _tag = 'UnitOfWorkAlreadyActiveError'\n readonly classification = 'terminal'\n\n constructor() {\n super('a unit of work is already active')\n }\n}\n"],"mappings":"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"}
|
package/dist/application.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { G as
|
|
2
|
-
import { A as
|
|
3
|
-
export { type CommandHandler, type CommandRegistry, type DomainCommand, type DomainEvent, type DomainMessage, type DomainQuery, type EmailAccepted, type EmailAttachment, type EmailBounceRecord, type EventHandler, type EventRegistry, type Handlers, type IEmailDeliverabilityStore, type IEmailService, type IEventCollector, type IMessageBus, type IMessageStore, type IOutboxRelay, type ITransaction, type IUnitOfWork, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type MimeType, type OutboundEmail, type QueryHandler, type QueryRegistry, type RecordEmailEventCommand, type RecordEmailEventDeps, type RecordEmailEventRepos, RecordingEmailService, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps, recordEmailEvent };
|
|
1
|
+
import { G as DomainEvent, J as MessageId, K as DomainMessage, W as DomainCommand, q as DomainQuery } from "./index-B5O6mNjZ.js";
|
|
2
|
+
import { A as IOutboxRepository, B as OutboundEmail, C as UnitOfWorkDeps, D as OutboxRelay, E as IOutboxRelay, F as IEventCollector, H as CommandHandler, I as EmailAccepted, L as EmailAttachment, M as OutboxClaim, N as OutboxMessage, O as OutboxRelayOptions, P as IMessageStore, R as IEmailService, S as UnitOfWork, T as IOutboxPublisher, U as EventHandler, V as RecordingEmailService, W as QueryHandler, _ as RecordEmailEventCommand, a as OutboxRetentionResult, b as recordEmailEvent, c as CommandRegistry, d as MessageRegistry, f as MessageResult, g as IEmailDeliverabilityStore, h as EmailBounceRecord, i as OutboxRetentionOptions, j as IOutboxRetentionRepository, k as DeadLetterReason, l as EventRegistry, m as RegistryMessage, n as MessageAlreadyProcessedError, o as IMessageBus, p as QueryRegistry, r as OutboxRetention, s as MessageBus, t as UnitOfWorkAlreadyActiveError, u as Handlers, v as RecordEmailEventDeps, w as ITransaction, x as IUnitOfWork, y as RecordEmailEventRepos, z as MimeType } from "./index-DG4jkwad.js";
|
|
3
|
+
export { type CommandHandler, type CommandRegistry, type DeadLetterReason, type DomainCommand, type DomainEvent, type DomainMessage, type DomainQuery, type EmailAccepted, type EmailAttachment, type EmailBounceRecord, type EventHandler, type EventRegistry, type Handlers, type IEmailDeliverabilityStore, type IEmailService, type IEventCollector, type IMessageBus, type IMessageStore, type IOutboxPublisher, type IOutboxRelay, type IOutboxRepository, type IOutboxRetentionRepository, type ITransaction, type IUnitOfWork, MessageAlreadyProcessedError, MessageBus, type MessageId, type MessageRegistry, type MessageResult, type MimeType, type OutboundEmail, type OutboxClaim, type OutboxMessage, OutboxRelay, type OutboxRelayOptions, OutboxRetention, type OutboxRetentionOptions, type OutboxRetentionResult, type QueryHandler, type QueryRegistry, type RecordEmailEventCommand, type RecordEmailEventDeps, type RecordEmailEventRepos, RecordingEmailService, type RegistryMessage, UnitOfWork, UnitOfWorkAlreadyActiveError, type UnitOfWorkDeps, recordEmailEvent };
|
package/dist/application.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as MessageBus, c as RecordingEmailService, i as OutboxRelay, n as UnitOfWork, o as MessageAlreadyProcessedError, r as OutboxRetention, s as recordEmailEvent, t as UnitOfWorkAlreadyActiveError } from "./application-DnlgWcOZ.js";
|
|
2
2
|
|
|
3
|
-
export { MessageAlreadyProcessedError, MessageBus, RecordingEmailService, UnitOfWork, UnitOfWorkAlreadyActiveError, recordEmailEvent };
|
|
3
|
+
export { MessageAlreadyProcessedError, MessageBus, OutboxRelay, OutboxRetention, RecordingEmailService, UnitOfWork, UnitOfWorkAlreadyActiveError, recordEmailEvent };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Wt as AppError } from "../index-B5O6mNjZ.js";
|
|
2
2
|
//#region src/infrastructure/better-auth/apple-client-secret.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Apple's issuer and OAuth callback origin, and the `aud` claim of the client secret. Include it in
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Wt as AppError } from "../index-B5O6mNjZ.js";
|
|
2
2
|
import { BetterAuthOptions, BetterAuthRateLimitStorage } from "better-auth";
|
|
3
3
|
//#region src/infrastructure/better-auth/rate-limit.d.ts
|
|
4
4
|
/** A rule Better Auth passes to storage: a window in seconds and the requests it allows. */
|
package/dist/better-auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as createLogger } from "./core-
|
|
1
|
+
import { a as createLogger } from "./core-BiqdOMwU.js";
|
|
2
2
|
import { v7 } from "uuid";
|
|
3
3
|
import { isAPIError } from "better-auth/api";
|
|
4
4
|
import { anonymous, bearer, captcha, deviceAuthorization, emailOTP, lastLoginMethod, magicLink, openAPI } from "better-auth/plugins";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Wt as AppError } from "../index-B5O6mNjZ.js";
|
|
2
2
|
import { DurableObject } from "cloudflare:workers";
|
|
3
3
|
//#region src/infrastructure/cloudflare/auth-secondary-storage.d.ts
|
|
4
4
|
/** The supplied TTL cannot produce a valid expiry timestamp. Retrying the same input cannot fix it. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { G as DomainEvent, Gt as FailureClassification, W as DomainCommand, Wt as AppError } from "../index-B5O6mNjZ.js";
|
|
2
|
+
import { D as OutboxRelay, E as IOutboxRelay } from "../index-DG4jkwad.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region src/infrastructure/cloudflare/queue/queue-message.d.ts
|
|
4
5
|
/** A message a queue carries: a command from outside the request, or an event from the outbox. */
|
|
@@ -26,7 +27,7 @@ interface IQueueClient<TMessage extends QueueMessage> {
|
|
|
26
27
|
send(message: TMessage, options?: QueueSendOptions): Promise<void>;
|
|
27
28
|
sendBatch(messages: ReadonlyArray<TMessage>): Promise<void>;
|
|
28
29
|
/** Sends every message within the limits and returns the oversized ones. */
|
|
29
|
-
sendBatchPartial(messages: ReadonlyArray<
|
|
30
|
+
sendBatchPartial<Sent extends TMessage>(messages: ReadonlyArray<Sent>): Promise<ReadonlyArray<Sent>>;
|
|
30
31
|
/** Parks a delivered message's body as it arrived. */
|
|
31
32
|
sendToDlq(message: Message): Promise<void>;
|
|
32
33
|
}
|
|
@@ -41,7 +42,7 @@ export declare class QueueClient<TMessage extends QueueMessage, Lane extends str
|
|
|
41
42
|
send(message: TMessage, options?: QueueSendOptions): Promise<void>;
|
|
42
43
|
/** Rejects the whole batch before any send when one message is oversized. */
|
|
43
44
|
sendBatch(messages: ReadonlyArray<TMessage>): Promise<void>;
|
|
44
|
-
sendBatchPartial(messages: ReadonlyArray<
|
|
45
|
+
sendBatchPartial<Sent extends TMessage>(messages: ReadonlyArray<Sent>): Promise<ReadonlyArray<Sent>>;
|
|
45
46
|
sendToDlq(message: Message): Promise<void>;
|
|
46
47
|
/** Each lane's chunks retry on their own, so one failed chunk does not resend the others. */
|
|
47
48
|
private sendSizedBatch;
|
|
@@ -171,5 +172,9 @@ type RedeliveryDecision = {
|
|
|
171
172
|
*/
|
|
172
173
|
export declare function decideRedelivery(cause: unknown, attempt: number): RedeliveryDecision;
|
|
173
174
|
//#endregion
|
|
175
|
+
//#region src/infrastructure/cloudflare/queue/wait-until-wake.d.ts
|
|
176
|
+
/** Register delivery after commit; log a failed drain once and leave recovery to the scheduled sweep. */
|
|
177
|
+
export declare function waitUntilWake(context: Pick<ExecutionContext, 'waitUntil'>, relay: Pick<OutboxRelay, 'drain'>): IOutboxRelay;
|
|
178
|
+
//#endregion
|
|
174
179
|
export type { DispatchBus, DispatchWrapper, IQueueClient, QueueBinding, QueueClientOptions, QueueMessage, QueueOperation, QueueSendOptions, RedeliveryDecision, SizedMessage };
|
|
175
180
|
//# sourceMappingURL=queue.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.d.ts","names":[],"sources":["../../src/infrastructure/cloudflare/queue/queue-message.ts","../../src/infrastructure/cloudflare/queue/queue-client.ts","../../src/infrastructure/cloudflare/queue/queue-consumer.ts","../../src/infrastructure/cloudflare/queue/queue-limits.ts","../../src/infrastructure/cloudflare/queue/queue.errors.ts","../../src/infrastructure/cloudflare/queue/redelivery-policy.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"queue.d.ts","names":[],"sources":["../../src/infrastructure/cloudflare/queue/queue-message.ts","../../src/infrastructure/cloudflare/queue/queue-client.ts","../../src/infrastructure/cloudflare/queue/queue-consumer.ts","../../src/infrastructure/cloudflare/queue/queue-limits.ts","../../src/infrastructure/cloudflare/queue/queue.errors.ts","../../src/infrastructure/cloudflare/queue/redelivery-policy.ts","../../src/infrastructure/cloudflare/queue/wait-until-wake.ts"],"mappings":";;;;;KAGY,eAAe,gBAAgB;;;;KCa/B;WAA8B;;;KAG9B,aAAa,QAAQ,KAAK,MAAM;;;;;KAMhC,mBAAmB,iBAAiB,cAAc;WACnD,QAAQ,SAAS,OAAO,MAAM,aAAa;;WAE3C,YAAY,KAAK;WACjB,SAAS,SAAS,aAAa;;;UAIzB,aAAa,iBAAiB;EAC7C,KAAK,SAAS,UAAU,UAAU,mBAAmB;EACrD,UAAU,UAAU,cAAc,YAAY;;EAE9C,iBAAiB,aAAa,UAC5B,UAAU,cAAc,QACvB,QAAQ,cAAc;;EAEzB,UAAU,SAAS,UAAU;;;;;;qBA8BlB,YACX,iBAAiB,cACjB,gCACW,aAAa;mBACK;EAA7B,YAA6B,SAAS,mBAAmB,UAAU;;EAG7D,KAAK,SAAS,UAAU,UAAU,mBAAmB;;EA4BrD,UAAU,UAAU,cAAc,YAAY;EAO9C,iBAAiB,aAAa,UAClC,UAAU,cAAc,QACvB,QAAQ,cAAc;EAMnB,UAAU,SAAS,UAAU;;UAQrB;UA4BN;UAIM;;;;;KCpJJ,gBAAgB,iBAAiB,iBAC3C,SAAS,UACT,iBACA,WAAW,kBACR;;KAGO,YAAY,iBAAiB;EAEvC,OAAO,SAAS,WAAW;;;;;;;qBAQhB,cAAc,iBAAiB;mBAQvB;mBACA;mBACA;mBACA;;;;;;;EAJnB,YACmB,KAAK,YAAY,WACjB,aAAa,KAAK,aAAa,yBAC/B,QAAQ,EAAE,QAAQ,WAClB,eAAc,gBAAgB;;EAI3C,aAAa,OAAO,eAAe;UAc3B;;UA0BA;;UAMA;;;;;UAaN;;;;;;;;;qBCjGG;WACX;WACA;WACA;;;wBAIc,kBAAkB,SAAS;;KAK/B,aAAa;WAAuB,SAAS;WAAmB;;;wBAG5D,aAAa,iBAAiB,cAC5C,UAAU,cAAc,YACvB,aAAa;;wBAKA,YAAY,OAAO,aAAa;;wBAKhC,cAAc,UAC5B,OAAO,cAAc,aAAa,aACjC;;;;KCjCS;;wBA4CI,mBAAmB,iBAAiB;;qBASvC,uBAAuB;WACzB;WACA,gBAAgB;WAChB,WAAW;EAEpB,YAAY;aAAiB,WAAW;aAAyB;;;;;;;qBAWtD,8BAA8B;WAChC;WACA;WACA;WACA;WACA;EAET,YAAY;aACD;aACA;aACA;;;;;;;qBAeA,mCAAmC;WACrC;WACA;EAET,YAAY;;;;;KCrFF;WACG;WAAwB;;WAExB;WACA,gBAAgB,QAAQ;WACxB;;;;;;;;wBASC,iBAAiB,gBAAgB,kBAAkB;;;;wBCxBnD,cACd,SAAS,KAAK,gCACd,OAAO,KAAK,wBACX"}
|
package/dist/cloudflare/queue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { $ as AppError, a as createLogger, d as classifyBoundaryError, f as classifyFailure, l as exponentialEqualJitterMs, p as retryAfterSecondsOf, u as spreadJitterMs, x as shouldRetryFailure } from "../core-BiqdOMwU.js";
|
|
2
|
+
import { o as MessageAlreadyProcessedError } from "../application-DnlgWcOZ.js";
|
|
3
3
|
import { Result } from "better-result";
|
|
4
4
|
|
|
5
5
|
//#region src/infrastructure/cloudflare/queue/queue-limits.ts
|
|
@@ -136,7 +136,7 @@ var DeadLetterUnavailableError = class extends AppError {
|
|
|
136
136
|
|
|
137
137
|
//#endregion
|
|
138
138
|
//#region src/infrastructure/cloudflare/queue/queue-client.ts
|
|
139
|
-
const logger$
|
|
139
|
+
const logger$2 = createLogger("queue-client");
|
|
140
140
|
/** Immediate retries of one binding call: three attempts, 250 ms then 500 ms, with jitter. */
|
|
141
141
|
const RETRY_TIMES = 2;
|
|
142
142
|
const RETRY_BASE_DELAY_MS = 250;
|
|
@@ -171,7 +171,7 @@ var QueueClient = class {
|
|
|
171
171
|
bytes: messageByteLength(message)
|
|
172
172
|
};
|
|
173
173
|
if (isOversized(sized)) {
|
|
174
|
-
logger$
|
|
174
|
+
logger$2.warn("queue_message_oversized", {
|
|
175
175
|
name: message.name,
|
|
176
176
|
type: message.type,
|
|
177
177
|
bytes: sized.bytes
|
|
@@ -179,7 +179,7 @@ var QueueClient = class {
|
|
|
179
179
|
throw oversizedError(sized);
|
|
180
180
|
}
|
|
181
181
|
const queue = this.queueFor(message);
|
|
182
|
-
logger$
|
|
182
|
+
logger$2.debug("sending_message", {
|
|
183
183
|
name: message.name,
|
|
184
184
|
type: message.type,
|
|
185
185
|
delaySeconds: options?.delaySeconds
|
|
@@ -202,7 +202,7 @@ var QueueClient = class {
|
|
|
202
202
|
return sized.filter(isOversized).map((item) => item.message);
|
|
203
203
|
}
|
|
204
204
|
async sendToDlq(message) {
|
|
205
|
-
logger$
|
|
205
|
+
logger$2.debug("sending_to_dlq", { queueMessageId: message.id });
|
|
206
206
|
await this.callQueue("send_dlq", async () => {
|
|
207
207
|
await this.options.deadLetter.send(message.body);
|
|
208
208
|
});
|
|
@@ -216,7 +216,7 @@ var QueueClient = class {
|
|
|
216
216
|
items.push(item);
|
|
217
217
|
byLane.set(lane, items);
|
|
218
218
|
}
|
|
219
|
-
logger$
|
|
219
|
+
logger$2.debug("sending_batch", {
|
|
220
220
|
count: sized.length,
|
|
221
221
|
lanes: [...byLane].map(([lane, items]) => ({
|
|
222
222
|
lane,
|
|
@@ -304,7 +304,7 @@ function decideRedelivery(cause, attempt) {
|
|
|
304
304
|
|
|
305
305
|
//#endregion
|
|
306
306
|
//#region src/infrastructure/cloudflare/queue/queue-consumer.ts
|
|
307
|
-
const logger = createLogger("queue-consumer");
|
|
307
|
+
const logger$1 = createLogger("queue-consumer");
|
|
308
308
|
/**
|
|
309
309
|
* Parses and dispatches each queue message, then selects acknowledgement or retry.
|
|
310
310
|
* Cloudflare owns the retry limit and sends exhausted messages to the configured dead letter.
|
|
@@ -329,7 +329,7 @@ var QueueConsumer = class {
|
|
|
329
329
|
}
|
|
330
330
|
/** Every message settles on its own. A settlement that throws rejects the batch after its siblings settle. */
|
|
331
331
|
async processBatch(batch) {
|
|
332
|
-
logger.debug("queue_batch_received", { count: batch.messages.length });
|
|
332
|
+
logger$1.debug("queue_batch_received", { count: batch.messages.length });
|
|
333
333
|
const rejected = (await Promise.allSettled(batch.messages.map((message) => this.processMessage(message)))).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
334
334
|
if (rejected.length > 0) throw new AggregateError(rejected, "Queue message settlement failed");
|
|
335
335
|
}
|
|
@@ -338,7 +338,7 @@ var QueueConsumer = class {
|
|
|
338
338
|
try {
|
|
339
339
|
const body = await this.parseMessage(message);
|
|
340
340
|
if (body === null) {
|
|
341
|
-
logger.error("invalid_message_format", { details: {
|
|
341
|
+
logger$1.error("invalid_message_format", { details: {
|
|
342
342
|
attempt: message.attempts,
|
|
343
343
|
queueMessageId: message.id
|
|
344
344
|
} });
|
|
@@ -350,7 +350,7 @@ var QueueConsumer = class {
|
|
|
350
350
|
await this.bus.handle(body);
|
|
351
351
|
});
|
|
352
352
|
message.ack();
|
|
353
|
-
logger.debug(`queue_message_processed:${body.name}`, { name: body.name });
|
|
353
|
+
logger$1.debug(`queue_message_processed:${body.name}`, { name: body.name });
|
|
354
354
|
} catch (cause) {
|
|
355
355
|
this.handleFailure(message, parsed, cause);
|
|
356
356
|
}
|
|
@@ -375,7 +375,7 @@ var QueueConsumer = class {
|
|
|
375
375
|
*/
|
|
376
376
|
handleFailure(message, parsed, cause) {
|
|
377
377
|
if (cause instanceof MessageAlreadyProcessedError) {
|
|
378
|
-
logger.debug("duplicate_skipped", {
|
|
378
|
+
logger$1.debug("duplicate_skipped", {
|
|
379
379
|
name: parsed?.name,
|
|
380
380
|
messageId: cause.messageId,
|
|
381
381
|
attempt: message.attempts
|
|
@@ -385,7 +385,7 @@ var QueueConsumer = class {
|
|
|
385
385
|
}
|
|
386
386
|
const decision = decideRedelivery(cause, message.attempts);
|
|
387
387
|
if (decision.action === "ack") {
|
|
388
|
-
logger.error("message_discarded", {
|
|
388
|
+
logger$1.error("message_discarded", {
|
|
389
389
|
error: cause,
|
|
390
390
|
details: {
|
|
391
391
|
name: parsed?.name,
|
|
@@ -405,15 +405,25 @@ var QueueConsumer = class {
|
|
|
405
405
|
classification: decision.classification,
|
|
406
406
|
delaySeconds: decision.delaySeconds
|
|
407
407
|
};
|
|
408
|
-
if (decision.classification === "unknown" && message.attempts === 1) logger.error("queue_unknown_failure", {
|
|
408
|
+
if (decision.classification === "unknown" && message.attempts === 1) logger$1.error("queue_unknown_failure", {
|
|
409
409
|
error: cause,
|
|
410
410
|
details: context
|
|
411
411
|
});
|
|
412
|
-
else logger.debug("redelivery_requested", context);
|
|
412
|
+
else logger$1.debug("redelivery_requested", context);
|
|
413
413
|
message.retry({ delaySeconds: decision.delaySeconds });
|
|
414
414
|
}
|
|
415
415
|
};
|
|
416
416
|
|
|
417
417
|
//#endregion
|
|
418
|
-
|
|
418
|
+
//#region src/infrastructure/cloudflare/queue/wait-until-wake.ts
|
|
419
|
+
const logger = createLogger("outbox-wake");
|
|
420
|
+
/** Register delivery after commit; log a failed drain once and leave recovery to the scheduled sweep. */
|
|
421
|
+
function waitUntilWake(context, relay) {
|
|
422
|
+
return { wake: () => context.waitUntil(relay.drain().catch((cause) => {
|
|
423
|
+
logger.error("outbox_wake_failed", { error: cause });
|
|
424
|
+
})) };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
//#endregion
|
|
428
|
+
export { DeadLetterUnavailableError, OversizedMessageError, QUEUE_LIMITS, QueueClient, QueueConsumer, QueueSendError, chunkByLimits, classifyQueueError, decideRedelivery, isOversized, messageByteLength, sizeMessages, waitUntilWake };
|
|
419
429
|
//# sourceMappingURL=queue.js.map
|