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.
Files changed (76) hide show
  1. package/dist/{application-CIxo_BpK.js → application-DnlgWcOZ.js} +118 -6
  2. package/dist/application-DnlgWcOZ.js.map +1 -0
  3. package/dist/application.d.ts +3 -3
  4. package/dist/application.js +2 -2
  5. package/dist/better-auth/apple-client-secret.d.ts +1 -1
  6. package/dist/better-auth/apple-client-secret.js +1 -1
  7. package/dist/better-auth/auth-client.js +1 -1
  8. package/dist/better-auth/rate-limit.d.ts +1 -1
  9. package/dist/better-auth/rate-limit.js +1 -1
  10. package/dist/better-auth.js +1 -1
  11. package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
  12. package/dist/cloudflare/auth-secondary-storage.js +1 -1
  13. package/dist/cloudflare/queue.d.ts +8 -3
  14. package/dist/cloudflare/queue.d.ts.map +1 -1
  15. package/dist/cloudflare/queue.js +26 -16
  16. package/dist/cloudflare/queue.js.map +1 -1
  17. package/dist/cloudflare/scheduled.d.ts +1 -1
  18. package/dist/cloudflare/scheduled.js +1 -1
  19. package/dist/cloudflare.d.ts +30 -4
  20. package/dist/cloudflare.d.ts.map +1 -1
  21. package/dist/cloudflare.js +98 -5
  22. package/dist/cloudflare.js.map +1 -1
  23. package/dist/{core-DG_cC8YT.js → core-BiqdOMwU.js} +3 -2
  24. package/dist/{core-DG_cC8YT.js.map → core-BiqdOMwU.js.map} +1 -1
  25. package/dist/core.d.ts +2 -2
  26. package/dist/core.js +2 -2
  27. package/dist/http.d.ts +2 -2
  28. package/dist/http.js +2 -2
  29. package/dist/{index-Bl0K1RQW.d.ts → index-B5O6mNjZ.d.ts} +4 -2
  30. package/dist/{index-Bl0K1RQW.d.ts.map → index-B5O6mNjZ.d.ts.map} +1 -1
  31. package/dist/{index-DgDyeR5s.d.ts → index-DG4jkwad.d.ts} +95 -4
  32. package/dist/index-DG4jkwad.d.ts.map +1 -0
  33. package/dist/{unit-of-work-context-BPE1In2f.js → outbox-repository-options-BI75j7b7.js} +12 -3
  34. package/dist/outbox-repository-options-BI75j7b7.js.map +1 -0
  35. package/dist/outbox-repository-options-BiilQf4_.d.ts +26 -0
  36. package/dist/outbox-repository-options-BiilQf4_.d.ts.map +1 -0
  37. package/dist/postgres.d.ts +30 -4
  38. package/dist/postgres.d.ts.map +1 -1
  39. package/dist/postgres.js +92 -5
  40. package/dist/postgres.js.map +1 -1
  41. package/dist/resend.d.ts +3 -3
  42. package/dist/resend.js +3 -3
  43. package/dist/resend.js.map +1 -1
  44. package/dist/tanstack.d.ts +1 -1
  45. package/dist/tanstack.js +1 -1
  46. package/dist/testing/outbox-repository.d.ts +44 -0
  47. package/dist/testing/outbox-repository.d.ts.map +1 -0
  48. package/dist/testing/outbox-repository.js +206 -0
  49. package/dist/testing/outbox-repository.js.map +1 -0
  50. package/dist/{webhook-verification-BQ9qZnVK.js → webhook-verification-CeXUJXVP.js} +2 -2
  51. package/dist/{webhook-verification-BQ9qZnVK.js.map → webhook-verification-CeXUJXVP.js.map} +1 -1
  52. package/dist/{webhook-verification-sZtwTI5n.d.ts → webhook-verification-CyBoERFz.d.ts} +2 -2
  53. package/dist/{webhook-verification-sZtwTI5n.d.ts.map → webhook-verification-CyBoERFz.d.ts.map} +1 -1
  54. package/package.json +35 -1
  55. package/src/application/email-service.ts +3 -1
  56. package/src/application/index.ts +18 -1
  57. package/src/application/outbox-relay.ts +117 -0
  58. package/src/application/outbox-repository.ts +46 -0
  59. package/src/application/outbox-retention.ts +61 -0
  60. package/src/core/backoff.ts +6 -1
  61. package/src/core/index.ts +1 -1
  62. package/src/infrastructure/cloudflare/d1-outbox-repository.ts +224 -0
  63. package/src/infrastructure/cloudflare/index.ts +2 -0
  64. package/src/infrastructure/cloudflare/queue/index.ts +1 -0
  65. package/src/infrastructure/cloudflare/queue/queue-client.ts +6 -2
  66. package/src/infrastructure/cloudflare/queue/wait-until-wake.ts +19 -0
  67. package/src/infrastructure/outbox-repository-options.ts +9 -0
  68. package/src/infrastructure/postgres/index.ts +2 -0
  69. package/src/infrastructure/postgres/postgres-outbox-repository.ts +216 -0
  70. package/src/infrastructure/resend/resend-email-service.ts +1 -1
  71. package/src/infrastructure/testing/outbox-repository-contract.ts +228 -0
  72. package/dist/application-CIxo_BpK.js.map +0 -1
  73. package/dist/index-DgDyeR5s.d.ts.map +0 -1
  74. package/dist/unit-of-work-context-BPE1In2f.js.map +0 -1
  75. package/dist/unit-of-work-context-BzmnjlDa.d.ts +0 -16
  76. package/dist/unit-of-work-context-BzmnjlDa.d.ts.map +0 -1
@@ -1,4 +1,4 @@
1
- import { E as DatabaseError, Q as AppError, T as unreachable, a as createLogger } from "./core-DG_cC8YT.js";
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 { recordEmailEvent as a, MessageAlreadyProcessedError as i, UnitOfWork as n, RecordingEmailService as o, MessageBus as r, UnitOfWorkAlreadyActiveError as t };
160
- //# sourceMappingURL=application-CIxo_BpK.js.map
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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"}
@@ -1,3 +1,3 @@
1
- import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, q as MessageId } from "./index-Bl0K1RQW.js";
2
- import { A as RecordingEmailService, C as IMessageStore, D as IEmailService, E as EmailAttachment, M as EventHandler, N as QueryHandler, O as MimeType, S as IOutboxRelay, T as EmailAccepted, _ as recordEmailEvent, a as CommandRegistry, b as UnitOfWorkDeps, c as MessageRegistry, d as RegistryMessage, f as EmailBounceRecord, g as RecordEmailEventRepos, h as RecordEmailEventDeps, i as MessageBus, j as CommandHandler, k as OutboundEmail, l as MessageResult, m as RecordEmailEventCommand, n as MessageAlreadyProcessedError, o as EventRegistry, p as IEmailDeliverabilityStore, r as IMessageBus, s as Handlers, t as UnitOfWorkAlreadyActiveError, u as QueryRegistry, v as IUnitOfWork, w as IEventCollector, x as ITransaction, y as UnitOfWork } from "./index-DgDyeR5s.js";
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 };
@@ -1,3 +1,3 @@
1
- import { a as recordEmailEvent, i as MessageAlreadyProcessedError, n as UnitOfWork, o as RecordingEmailService, r as MessageBus, t as UnitOfWorkAlreadyActiveError } from "./application-CIxo_BpK.js";
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 { Ut as AppError } from "../index-Bl0K1RQW.js";
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 { Q as AppError, a as createLogger } from "../core-DG_cC8YT.js";
1
+ import { $ as AppError, a as createLogger } from "../core-BiqdOMwU.js";
2
2
  import { Buffer } from "node:buffer";
3
3
  import { createPrivateKey, sign } from "node:crypto";
4
4
 
@@ -1,4 +1,4 @@
1
- import { V as RateLimitError } from "../core-DG_cC8YT.js";
1
+ import { H as RateLimitError } from "../core-BiqdOMwU.js";
2
2
  import { createAuthClient } from "better-auth/client";
3
3
 
4
4
  //#region src/infrastructure/better-auth/auth-client.ts
@@ -1,4 +1,4 @@
1
- import { Ut as AppError } from "../index-Bl0K1RQW.js";
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. */
@@ -1,4 +1,4 @@
1
- import { Q as AppError, a as createLogger } from "../core-DG_cC8YT.js";
1
+ import { $ as AppError, a as createLogger } from "../core-BiqdOMwU.js";
2
2
 
3
3
  //#region src/infrastructure/better-auth/rate-limit.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as createLogger } from "./core-DG_cC8YT.js";
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 { Ut as AppError } from "../index-Bl0K1RQW.js";
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,4 @@
1
- import { Q as AppError } from "../core-DG_cC8YT.js";
1
+ import { $ as AppError } from "../core-BiqdOMwU.js";
2
2
  import { DurableObject } from "cloudflare:workers";
3
3
 
4
4
  //#region src/infrastructure/cloudflare/auth-secondary-storage.ts
@@ -1,4 +1,5 @@
1
- import { U as DomainCommand, Ut as AppError, W as DomainEvent, Wt as FailureClassification } from "../index-Bl0K1RQW.js";
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<TMessage>): Promise<ReadonlyArray<TMessage>>;
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<TMessage>): Promise<ReadonlyArray<TMessage>>;
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":";;;;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,UAAU,cAAc,YAAY,QAAQ,cAAc;;EAE3E,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,UAAU,cAAc,YAAY,QAAQ,cAAc;EAM3E,UAAU,SAAS,UAAU;;UAQrB;UA4BN;UAIM;;;;;KChJJ,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"}
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"}
@@ -1,5 +1,5 @@
1
- import { Q as AppError, a as createLogger, b as shouldRetryFailure, c as exponentialEqualJitterMs, d as classifyFailure, f as retryAfterSecondsOf, l as spreadJitterMs, u as classifyBoundaryError } from "../core-DG_cC8YT.js";
2
- import { i as MessageAlreadyProcessedError } from "../application-CIxo_BpK.js";
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$1 = createLogger("queue-client");
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$1.warn("queue_message_oversized", {
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$1.debug("sending_message", {
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$1.debug("sending_to_dlq", { queueMessageId: message.id });
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$1.debug("sending_batch", {
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
- export { DeadLetterUnavailableError, OversizedMessageError, QUEUE_LIMITS, QueueClient, QueueConsumer, QueueSendError, chunkByLimits, classifyQueueError, decideRedelivery, isOversized, messageByteLength, sizeMessages };
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