better-ship 0.12.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-DwWxIdQP.d.ts → index-DG4jkwad.d.ts} +92 -3
- 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 +2 -2
- 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/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/testing/outbox-repository-contract.ts +228 -0
- package/dist/application-CIxo_BpK.js.map +0 -1
- package/dist/index-DwWxIdQP.d.ts.map +0 -1
- package/dist/unit-of-work-context-BPE1In2f.js.map +0 -1
- package/dist/unit-of-work-context-D5ytJIa-.d.ts +0 -16
- package/dist/unit-of-work-context-D5ytJIa-.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.js","names":["logger"],"sources":["../../src/infrastructure/cloudflare/queue/queue-limits.ts","../../src/infrastructure/cloudflare/queue/queue.errors.ts","../../src/infrastructure/cloudflare/queue/queue-client.ts","../../src/infrastructure/cloudflare/queue/redelivery-policy.ts","../../src/infrastructure/cloudflare/queue/queue-consumer.ts"],"sourcesContent":["import type { QueueMessage } from './queue-message.ts'\n\n/**\n * The Cloudflare Queues producer limits with metadata headroom, checked before each send so a\n * violation never reaches the binding. The dead letter has the same per-message limit.\n * https://developers.cloudflare.com/queues/platform/limits/\n */\nexport const QUEUE_LIMITS = {\n messageBytes: 127_000,\n batchBytes: 250_000,\n batchCount: 100,\n} as const\n\n/** The size the binding measures: the JSON encoding of the body, in UTF-8 bytes. */\nexport function messageByteLength(message: QueueMessage): number {\n return new TextEncoder().encode(JSON.stringify(message)).length\n}\n\n/** A message with its measured size. */\nexport type SizedMessage<TMessage> = { readonly message: TMessage; readonly bytes: number }\n\n/** Measure every message once. */\nexport function sizeMessages<TMessage extends QueueMessage>(\n messages: ReadonlyArray<TMessage>,\n): SizedMessage<TMessage>[] {\n return messages.map((message) => ({ message, bytes: messageByteLength(message) }))\n}\n\n/** Whether one message is over the per-message limit. */\nexport function isOversized(sized: SizedMessage<QueueMessage>): boolean {\n return sized.bytes > QUEUE_LIMITS.messageBytes\n}\n\n/** Split a batch at the message-count and byte limits, keeping the order. */\nexport function chunkByLimits<TMessage>(\n sized: ReadonlyArray<SizedMessage<TMessage>>,\n): TMessage[][] {\n const chunks: TMessage[][] = []\n let current: TMessage[] = []\n let bytes = 0\n for (const item of sized) {\n const full =\n current.length >= QUEUE_LIMITS.batchCount || bytes + item.bytes > QUEUE_LIMITS.batchBytes\n if (current.length > 0 && full) {\n chunks.push(current)\n current = []\n bytes = 0\n }\n current.push(item.message)\n bytes += item.bytes\n }\n if (current.length > 0) chunks.push(current)\n return chunks\n}\n","import { AppError, classifyBoundaryError, type FailureClassification } from '@/core'\n\n/** The Queue binding calls the client makes. Each is safe to repeat. */\nexport type QueueOperation = 'send' | 'send_batch' | 'send_dlq'\n\nconst QUEUE_OPERATION_MESSAGES = {\n send: 'Failed to send queue message',\n send_batch: 'Failed to send queue batch',\n send_dlq: 'Failed to send DLQ message',\n} satisfies Readonly<Record<QueueOperation, string>>\n\n/**\n * Cloudflare Queues error codes, by whether a repeat could change the answer. The reference\n * marks only 10201 and 10250 for retry; 10251 waits for processing, 15000 is nobody's call.\n * https://developers.cloudflare.com/queues/reference/error-codes/\n */\nconst QUEUE_ERROR_CLASSIFICATIONS = new Map<number, FailureClassification>([\n [10104, 'terminal'], // QueueNotFound\n [10106, 'terminal'], // Unauthorized\n [10107, 'terminal'], // QueueIDMalformed\n [10201, 'transient'], // ClientDisconnected\n [10202, 'terminal'], // BatchDelayInvalid\n [10203, 'terminal'], // MessageMetadataInvalid\n [10204, 'terminal'], // MessageSizeOutOfBounds\n [10205, 'terminal'], // BatchSizeOutOfBounds\n [10206, 'terminal'], // BatchCountOutOfBounds\n [10207, 'terminal'], // JSONRequestBodyInvalid\n [10208, 'terminal'], // JSONRequestBodyMalformed\n [10250, 'transient'], // QueueOverloaded\n [10251, 'transient'], // QueueStorageLimitExceeded\n [10252, 'terminal'], // QueueDisabled\n [10253, 'terminal'], // FreeTierLimitExceeded\n [15000, 'unknown'], // UnknownInternalError\n])\n\nfunction extractQueueErrorCode(cause: unknown): number | undefined {\n if (!(cause instanceof Error)) return undefined\n const match = /(\\d{5})\\)?\\s*$/.exec(cause.message)\n return match?.[1] === undefined ? undefined : Number(match[1])\n}\n\nfunction classifyQueueBoundaryError(cause: unknown): FailureClassification | undefined {\n const code = extractQueueErrorCode(cause)\n return code === undefined ? undefined : QUEUE_ERROR_CLASSIFICATIONS.get(code)\n}\n\n/** Classify a raw Queue binding failure: the code table first, then Cloudflare's signals, then transport. */\nexport function classifyQueueError(cause: unknown): FailureClassification {\n return classifyBoundaryError({\n cause,\n classifyBoundary: classifyQueueBoundaryError,\n includeCloudflare: true,\n })\n}\n\n/** A send through a Queue binding failed. The classification says whether an immediate retry may help. */\nexport class QueueSendError extends AppError {\n readonly _tag = 'QueueSendError'\n readonly classification: FailureClassification\n readonly operation: QueueOperation\n\n constructor(args: { readonly operation: QueueOperation; readonly cause: unknown }) {\n super(QUEUE_OPERATION_MESSAGES[args.operation], { cause: args.cause })\n this.classification = classifyQueueError(args.cause)\n this.operation = args.operation\n }\n}\n\n/**\n * A message over the per-message cap can never be sent on any queue; the dead letter has the\n * same cap. The client detects it before the send, so it never comes from a platform error.\n */\nexport class OversizedMessageError extends AppError {\n readonly _tag = 'OversizedMessageError'\n readonly classification = 'terminal'\n readonly messageName: string\n readonly bytes: number\n readonly limit: number\n\n constructor(args: {\n readonly messageName: string\n readonly bytes: number\n readonly limit: number\n }) {\n super(\n `Queue message \"${args.messageName}\" is ${String(args.bytes)} bytes, over the ${String(args.limit)} limit`,\n )\n this.messageName = args.messageName\n this.bytes = args.bytes\n this.limit = args.limit\n }\n}\n\n/**\n * The dead letter did not accept an unparseable message. Always transient, whatever the send\n * failure was, so the source delivery retries instead of acknowledging and losing the body.\n */\nexport class DeadLetterUnavailableError extends AppError {\n readonly _tag = 'DeadLetterUnavailableError'\n readonly classification = 'transient'\n\n constructor(cause: unknown) {\n super('The dead letter did not accept the message', { cause })\n }\n}\n","import { Result } from 'better-result'\n\nimport { createLogger, shouldRetryFailure } from '@/core'\n\nimport {\n chunkByLimits,\n isOversized,\n messageByteLength,\n QUEUE_LIMITS,\n sizeMessages,\n type SizedMessage,\n} from './queue-limits.ts'\nimport type { QueueMessage } from './queue-message.ts'\nimport { OversizedMessageError, QueueSendError, type QueueOperation } from './queue.errors.ts'\n\n/** Cloudflare's delivery delay, in seconds. */\nexport type QueueSendOptions = { readonly delaySeconds: number }\n\n/** The two calls the client makes on a queue binding. */\nexport type QueueBinding<Body> = Pick<Queue<Body>, 'send' | 'sendBatch'>\n\n/**\n * The lanes and the dead letter of one application. A lane is a queue with its own consumer\n * settings; `laneOf` is the product's routing rule, written once at the composition root.\n */\nexport type QueueClientOptions<TMessage extends QueueMessage, Lane extends string> = {\n readonly queues: Readonly<Record<Lane, QueueBinding<TMessage>>>\n /** Where the consumer parks poison bodies. Cloudflare also routes exhausted retries here. */\n readonly deadLetter: Pick<Queue, 'send'>\n readonly laneOf: (message: TMessage) => Lane\n}\n\n/** The port an entrypoint, a relay, or a consumer sends through. */\nexport interface IQueueClient<TMessage extends QueueMessage> {\n send(message: TMessage, options?: QueueSendOptions): Promise<void>\n sendBatch(messages: ReadonlyArray<TMessage>): Promise<void>\n /** Sends every message within the limits and returns the oversized ones. */\n sendBatchPartial(messages: ReadonlyArray<TMessage>): Promise<ReadonlyArray<TMessage>>\n /** Parks a delivered message's body as it arrived. */\n sendToDlq(message: Message): Promise<void>\n}\n\nconst logger = createLogger('queue-client')\n\n/** Immediate retries of one binding call: three attempts, 250 ms then 500 ms, with jitter. */\nconst RETRY_TIMES = 2\nconst RETRY_BASE_DELAY_MS = 250\n\n/** Every send is safe to repeat, so a transient failure gets an immediate retry; terminal and unknown stop. */\nfunction shouldRetrySend(error: QueueSendError): boolean {\n return shouldRetryFailure({\n classification: error.classification,\n repeatSafe: true,\n owner: 'immediate',\n })\n}\n\nfunction oversizedError(sized: SizedMessage<QueueMessage>): OversizedMessageError {\n return new OversizedMessageError({\n messageName: sized.message.name,\n bytes: sized.bytes,\n limit: QUEUE_LIMITS.messageBytes,\n })\n}\n\n/**\n * Sends messages to the application's queues. Checks the size limits before a send, retries a\n * transient binding failure a bounded number of times, and converts the final failure to `QueueSendError`.\n */\nexport class QueueClient<\n TMessage extends QueueMessage,\n Lane extends string,\n> implements IQueueClient<TMessage> {\n constructor(private readonly options: QueueClientOptions<TMessage, Lane>) {}\n\n /** @throws OversizedMessageError before the send. @throws QueueSendError after the retries. */\n async send(message: TMessage, options?: QueueSendOptions): Promise<void> {\n const sized = { message, bytes: messageByteLength(message) }\n if (isOversized(sized)) {\n logger.warn('queue_message_oversized', {\n name: message.name,\n type: message.type,\n bytes: sized.bytes,\n })\n throw oversizedError(sized)\n }\n\n const queue = this.queueFor(message)\n logger.debug('sending_message', {\n name: message.name,\n type: message.type,\n delaySeconds: options?.delaySeconds,\n })\n\n await this.callQueue('send', async () => {\n if (options) {\n await queue.send(message, options)\n } else {\n await queue.send(message)\n }\n })\n }\n\n /** Rejects the whole batch before any send when one message is oversized. */\n async sendBatch(messages: ReadonlyArray<TMessage>): Promise<void> {\n const sized = sizeMessages(messages)\n const oversized = sized.find(isOversized)\n if (oversized) throw oversizedError(oversized)\n await this.sendSizedBatch(sized)\n }\n\n async sendBatchPartial(messages: ReadonlyArray<TMessage>): Promise<ReadonlyArray<TMessage>> {\n const sized = sizeMessages(messages)\n await this.sendSizedBatch(sized.filter((item) => !isOversized(item)))\n return sized.filter(isOversized).map((item) => item.message)\n }\n\n async sendToDlq(message: Message): Promise<void> {\n logger.debug('sending_to_dlq', { queueMessageId: message.id })\n await this.callQueue('send_dlq', async () => {\n await this.options.deadLetter.send(message.body)\n })\n }\n\n /** Each lane's chunks retry on their own, so one failed chunk does not resend the others. */\n private async sendSizedBatch(sized: ReadonlyArray<SizedMessage<TMessage>>): Promise<void> {\n const byLane = new Map<Lane, SizedMessage<TMessage>[]>()\n for (const item of sized) {\n const lane = this.options.laneOf(item.message)\n const items = byLane.get(lane) ?? []\n items.push(item)\n byLane.set(lane, items)\n }\n\n logger.debug('sending_batch', {\n count: sized.length,\n lanes: [...byLane].map(([lane, items]) => ({ lane, count: items.length })),\n })\n\n const sends: Promise<void>[] = []\n for (const [lane, items] of byLane) {\n const queue = this.options.queues[lane]\n for (const group of chunkByLimits(items)) {\n sends.push(\n this.callQueue('send_batch', async () => {\n await queue.sendBatch(group.map((body) => ({ body })))\n }),\n )\n }\n }\n await Promise.all(sends)\n }\n\n private queueFor(message: TMessage): QueueBinding<TMessage> {\n return this.options.queues[this.options.laneOf(message)]\n }\n\n private async callQueue<T>(operation: QueueOperation, call: () => Promise<T>): Promise<T> {\n const result = await Result.tryPromise(\n {\n try: call,\n catch: (cause) => new QueueSendError({ operation, cause }),\n },\n {\n retry: {\n times: RETRY_TIMES,\n delayMs: RETRY_BASE_DELAY_MS,\n backoff: 'exponential',\n jitter: true,\n shouldRetry: shouldRetrySend,\n },\n },\n )\n if (result.isErr()) throw result.error\n return result.value\n }\n}\n","import {\n classifyFailure,\n exponentialEqualJitterMs,\n retryAfterSecondsOf,\n shouldRetryFailure,\n spreadJitterMs,\n type FailureClassification,\n} from '@/core'\n\nconst BASE_RETRY_DELAY_MS = 5_000\nconst MAX_RETRY_DELAY_MS = 120_000\nconst RETRY_AFTER_SPREAD_MS = 5_000\n/** Cloudflare Queues accept a redelivery delay of at most one day. */\nconst MAX_QUEUE_DELAY_SECONDS = 24 * 60 * 60\n\n/** How the consumer settles one failed message: acknowledge it, or have Cloudflare redeliver it after a delay. */\nexport type RedeliveryDecision =\n | { readonly action: 'ack'; readonly classification: 'terminal' }\n | {\n readonly action: 'retry'\n readonly classification: Exclude<FailureClassification, 'terminal'>\n readonly delaySeconds: number\n }\n\n/**\n * Decide whether the consumer acknowledges a failed message or asks Cloudflare to redeliver it.\n * A terminal failure acknowledges. Transient and unknown failures redeliver: after the advertised\n * wait plus a spread when the error carries one, else with exponential equal-jitter backoff.\n * An `AggregateError` from event delivery redelivers when any member does, after the longest wait.\n */\nexport function decideRedelivery(cause: unknown, attempt: number): RedeliveryDecision {\n if (cause instanceof AggregateError && cause.errors.length > 0) {\n const decisions = cause.errors.map((member) => decideRedelivery(member, attempt))\n const retries = decisions.filter((decision) => decision.action === 'retry')\n if (retries.length === 0) return { action: 'ack', classification: 'terminal' }\n\n // Redelivery must respect every failed subscriber's required wait.\n return {\n action: 'retry',\n classification: retries.some((decision) => decision.classification === 'unknown')\n ? 'unknown'\n : 'transient',\n delaySeconds: Math.max(...retries.map((decision) => decision.delaySeconds)),\n }\n }\n\n const classification = classifyFailure(cause)\n if (classification === 'terminal') return { action: 'ack', classification }\n\n const canRetry = shouldRetryFailure({ classification, repeatSafe: true, owner: 'durable' })\n if (!canRetry) return { action: 'ack', classification: 'terminal' }\n\n const retryAfterSeconds = retryAfterSecondsOf(cause)\n const delayMs =\n retryAfterSeconds === null\n ? exponentialEqualJitterMs(BASE_RETRY_DELAY_MS, MAX_RETRY_DELAY_MS, attempt)\n : retryAfterSeconds * 1_000 + spreadJitterMs(RETRY_AFTER_SPREAD_MS)\n\n return {\n action: 'retry',\n classification,\n delaySeconds: Math.min(MAX_QUEUE_DELAY_SECONDS, Math.max(1, Math.ceil(delayMs / 1_000))),\n }\n}\n","import type { z } from 'zod'\n\nimport { MessageAlreadyProcessedError } from '@/application'\nimport { createLogger } from '@/core'\n\nimport type { IQueueClient } from './queue-client.ts'\nimport type { QueueMessage } from './queue-message.ts'\nimport { DeadLetterUnavailableError } from './queue.errors.ts'\nimport { decideRedelivery } from './redelivery-policy.ts'\n\nconst logger = createLogger('queue-consumer')\n\n/** Wraps one dispatch: an error-tracking scope, an analytics context. The default runs it as is. */\nexport type DispatchWrapper<TMessage extends QueueMessage> = (\n message: TMessage,\n attempt: number,\n run: () => Promise<void>,\n) => Promise<void>\n\n/** The one call the consumer makes on the bus. */\nexport type DispatchBus<TMessage extends QueueMessage> = {\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- the bus resolves to each handler's own value; the consumer discards it.\n handle(message: TMessage): Promise<unknown>\n}\n\n/**\n * Parses and dispatches each queue message, then selects acknowledgement or retry.\n * Cloudflare owns the retry limit and sends exhausted messages to the configured dead letter.\n * The consumer logs each failure once; handlers do not.\n */\nexport class QueueConsumer<TMessage extends QueueMessage> {\n /**\n * @param bus - Where a parsed message goes.\n * @param queueClient - Parks an unparseable body in the dead letter.\n * @param schema - What a body may be: the application's `messages.queueSchema`.\n * @param wrapDispatch - Wraps one dispatch in the application's context. Default: run it as is.\n */\n constructor(\n private readonly bus: DispatchBus<TMessage>,\n private readonly queueClient: Pick<IQueueClient<TMessage>, 'sendToDlq'>,\n private readonly schema: z.ZodType<TMessage>,\n private readonly wrapDispatch: DispatchWrapper<TMessage> = (_message, _attempt, run) => run(),\n ) {}\n\n /** Every message settles on its own. A settlement that throws rejects the batch after its siblings settle. */\n async processBatch(batch: MessageBatch): Promise<void> {\n logger.debug('queue_batch_received', { count: batch.messages.length })\n const results = await Promise.allSettled(\n batch.messages.map((message) => this.processMessage(message)),\n )\n const rejected = results\n .filter((result) => result.status === 'rejected')\n .map((result) => result.reason)\n\n if (rejected.length > 0) {\n throw new AggregateError(rejected, 'Queue message settlement failed')\n }\n }\n\n private async processMessage(message: Message): Promise<void> {\n let parsed: TMessage | null = null\n\n try {\n const body = await this.parseMessage(message)\n if (body === null) {\n // An unparseable body cannot succeed on retry, so park it for inspection.\n logger.error('invalid_message_format', {\n details: { attempt: message.attempts, queueMessageId: message.id },\n })\n await this.parkPoison(message)\n return\n }\n parsed = body\n\n await this.wrapDispatch(body, message.attempts, async () => {\n await this.bus.handle(body)\n })\n message.ack()\n logger.debug(`queue_message_processed:${body.name}`, { name: body.name })\n } catch (cause) {\n this.handleFailure(message, parsed, cause)\n }\n }\n\n /** Returns null on failure; the caller owns the single log point. */\n private async parseMessage(message: Message): Promise<TMessage | null> {\n const result = await this.schema.safeParseAsync(message.body)\n return result.success ? result.data : null\n }\n\n /** Park an unparseable message in the dead letter, then acknowledge its source delivery. */\n private async parkPoison(message: Message): Promise<void> {\n try {\n await this.queueClient.sendToDlq(message)\n message.ack()\n } catch (cause) {\n throw new DeadLetterUnavailableError(cause)\n }\n }\n\n /**\n * Retry declared transient and unknown failures so Cloudflare can enforce the attempt ceiling.\n * Acknowledge only declared terminal failures.\n */\n private handleFailure(message: Message, parsed: TMessage | null, cause: unknown): void {\n if (cause instanceof MessageAlreadyProcessedError) {\n logger.debug('duplicate_skipped', {\n name: parsed?.name,\n messageId: cause.messageId,\n attempt: message.attempts,\n })\n message.ack()\n return\n }\n\n const decision = decideRedelivery(cause, message.attempts)\n if (decision.action === 'ack') {\n logger.error('message_discarded', {\n error: cause,\n details: {\n name: parsed?.name,\n messageId: parsed?.id,\n queueMessageId: message.id,\n attempt: message.attempts,\n },\n })\n message.ack()\n return\n }\n\n const context = {\n name: parsed?.name,\n messageId: parsed?.id,\n queueMessageId: message.id,\n attempt: message.attempts,\n classification: decision.classification,\n delaySeconds: decision.delaySeconds,\n }\n if (decision.classification === 'unknown' && message.attempts === 1) {\n logger.error('queue_unknown_failure', { error: cause, details: context })\n } else {\n logger.debug('redelivery_requested', context)\n }\n message.retry({ delaySeconds: decision.delaySeconds })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"queue.js","names":["logger","logger"],"sources":["../../src/infrastructure/cloudflare/queue/queue-limits.ts","../../src/infrastructure/cloudflare/queue/queue.errors.ts","../../src/infrastructure/cloudflare/queue/queue-client.ts","../../src/infrastructure/cloudflare/queue/redelivery-policy.ts","../../src/infrastructure/cloudflare/queue/queue-consumer.ts","../../src/infrastructure/cloudflare/queue/wait-until-wake.ts"],"sourcesContent":["import type { QueueMessage } from './queue-message.ts'\n\n/**\n * The Cloudflare Queues producer limits with metadata headroom, checked before each send so a\n * violation never reaches the binding. The dead letter has the same per-message limit.\n * https://developers.cloudflare.com/queues/platform/limits/\n */\nexport const QUEUE_LIMITS = {\n messageBytes: 127_000,\n batchBytes: 250_000,\n batchCount: 100,\n} as const\n\n/** The size the binding measures: the JSON encoding of the body, in UTF-8 bytes. */\nexport function messageByteLength(message: QueueMessage): number {\n return new TextEncoder().encode(JSON.stringify(message)).length\n}\n\n/** A message with its measured size. */\nexport type SizedMessage<TMessage> = { readonly message: TMessage; readonly bytes: number }\n\n/** Measure every message once. */\nexport function sizeMessages<TMessage extends QueueMessage>(\n messages: ReadonlyArray<TMessage>,\n): SizedMessage<TMessage>[] {\n return messages.map((message) => ({ message, bytes: messageByteLength(message) }))\n}\n\n/** Whether one message is over the per-message limit. */\nexport function isOversized(sized: SizedMessage<QueueMessage>): boolean {\n return sized.bytes > QUEUE_LIMITS.messageBytes\n}\n\n/** Split a batch at the message-count and byte limits, keeping the order. */\nexport function chunkByLimits<TMessage>(\n sized: ReadonlyArray<SizedMessage<TMessage>>,\n): TMessage[][] {\n const chunks: TMessage[][] = []\n let current: TMessage[] = []\n let bytes = 0\n for (const item of sized) {\n const full =\n current.length >= QUEUE_LIMITS.batchCount || bytes + item.bytes > QUEUE_LIMITS.batchBytes\n if (current.length > 0 && full) {\n chunks.push(current)\n current = []\n bytes = 0\n }\n current.push(item.message)\n bytes += item.bytes\n }\n if (current.length > 0) chunks.push(current)\n return chunks\n}\n","import { AppError, classifyBoundaryError, type FailureClassification } from '@/core'\n\n/** The Queue binding calls the client makes. Each is safe to repeat. */\nexport type QueueOperation = 'send' | 'send_batch' | 'send_dlq'\n\nconst QUEUE_OPERATION_MESSAGES = {\n send: 'Failed to send queue message',\n send_batch: 'Failed to send queue batch',\n send_dlq: 'Failed to send DLQ message',\n} satisfies Readonly<Record<QueueOperation, string>>\n\n/**\n * Cloudflare Queues error codes, by whether a repeat could change the answer. The reference\n * marks only 10201 and 10250 for retry; 10251 waits for processing, 15000 is nobody's call.\n * https://developers.cloudflare.com/queues/reference/error-codes/\n */\nconst QUEUE_ERROR_CLASSIFICATIONS = new Map<number, FailureClassification>([\n [10104, 'terminal'], // QueueNotFound\n [10106, 'terminal'], // Unauthorized\n [10107, 'terminal'], // QueueIDMalformed\n [10201, 'transient'], // ClientDisconnected\n [10202, 'terminal'], // BatchDelayInvalid\n [10203, 'terminal'], // MessageMetadataInvalid\n [10204, 'terminal'], // MessageSizeOutOfBounds\n [10205, 'terminal'], // BatchSizeOutOfBounds\n [10206, 'terminal'], // BatchCountOutOfBounds\n [10207, 'terminal'], // JSONRequestBodyInvalid\n [10208, 'terminal'], // JSONRequestBodyMalformed\n [10250, 'transient'], // QueueOverloaded\n [10251, 'transient'], // QueueStorageLimitExceeded\n [10252, 'terminal'], // QueueDisabled\n [10253, 'terminal'], // FreeTierLimitExceeded\n [15000, 'unknown'], // UnknownInternalError\n])\n\nfunction extractQueueErrorCode(cause: unknown): number | undefined {\n if (!(cause instanceof Error)) return undefined\n const match = /(\\d{5})\\)?\\s*$/.exec(cause.message)\n return match?.[1] === undefined ? undefined : Number(match[1])\n}\n\nfunction classifyQueueBoundaryError(cause: unknown): FailureClassification | undefined {\n const code = extractQueueErrorCode(cause)\n return code === undefined ? undefined : QUEUE_ERROR_CLASSIFICATIONS.get(code)\n}\n\n/** Classify a raw Queue binding failure: the code table first, then Cloudflare's signals, then transport. */\nexport function classifyQueueError(cause: unknown): FailureClassification {\n return classifyBoundaryError({\n cause,\n classifyBoundary: classifyQueueBoundaryError,\n includeCloudflare: true,\n })\n}\n\n/** A send through a Queue binding failed. The classification says whether an immediate retry may help. */\nexport class QueueSendError extends AppError {\n readonly _tag = 'QueueSendError'\n readonly classification: FailureClassification\n readonly operation: QueueOperation\n\n constructor(args: { readonly operation: QueueOperation; readonly cause: unknown }) {\n super(QUEUE_OPERATION_MESSAGES[args.operation], { cause: args.cause })\n this.classification = classifyQueueError(args.cause)\n this.operation = args.operation\n }\n}\n\n/**\n * A message over the per-message cap can never be sent on any queue; the dead letter has the\n * same cap. The client detects it before the send, so it never comes from a platform error.\n */\nexport class OversizedMessageError extends AppError {\n readonly _tag = 'OversizedMessageError'\n readonly classification = 'terminal'\n readonly messageName: string\n readonly bytes: number\n readonly limit: number\n\n constructor(args: {\n readonly messageName: string\n readonly bytes: number\n readonly limit: number\n }) {\n super(\n `Queue message \"${args.messageName}\" is ${String(args.bytes)} bytes, over the ${String(args.limit)} limit`,\n )\n this.messageName = args.messageName\n this.bytes = args.bytes\n this.limit = args.limit\n }\n}\n\n/**\n * The dead letter did not accept an unparseable message. Always transient, whatever the send\n * failure was, so the source delivery retries instead of acknowledging and losing the body.\n */\nexport class DeadLetterUnavailableError extends AppError {\n readonly _tag = 'DeadLetterUnavailableError'\n readonly classification = 'transient'\n\n constructor(cause: unknown) {\n super('The dead letter did not accept the message', { cause })\n }\n}\n","import { Result } from 'better-result'\n\nimport { createLogger, shouldRetryFailure } from '@/core'\n\nimport {\n chunkByLimits,\n isOversized,\n messageByteLength,\n QUEUE_LIMITS,\n sizeMessages,\n type SizedMessage,\n} from './queue-limits.ts'\nimport type { QueueMessage } from './queue-message.ts'\nimport { OversizedMessageError, QueueSendError, type QueueOperation } from './queue.errors.ts'\n\n/** Cloudflare's delivery delay, in seconds. */\nexport type QueueSendOptions = { readonly delaySeconds: number }\n\n/** The two calls the client makes on a queue binding. */\nexport type QueueBinding<Body> = Pick<Queue<Body>, 'send' | 'sendBatch'>\n\n/**\n * The lanes and the dead letter of one application. A lane is a queue with its own consumer\n * settings; `laneOf` is the product's routing rule, written once at the composition root.\n */\nexport type QueueClientOptions<TMessage extends QueueMessage, Lane extends string> = {\n readonly queues: Readonly<Record<Lane, QueueBinding<TMessage>>>\n /** Where the consumer parks poison bodies. Cloudflare also routes exhausted retries here. */\n readonly deadLetter: Pick<Queue, 'send'>\n readonly laneOf: (message: TMessage) => Lane\n}\n\n/** The port an entrypoint, a relay, or a consumer sends through. */\nexport interface IQueueClient<TMessage extends QueueMessage> {\n send(message: TMessage, options?: QueueSendOptions): Promise<void>\n sendBatch(messages: ReadonlyArray<TMessage>): Promise<void>\n /** Sends every message within the limits and returns the oversized ones. */\n sendBatchPartial<Sent extends TMessage>(\n messages: ReadonlyArray<Sent>,\n ): Promise<ReadonlyArray<Sent>>\n /** Parks a delivered message's body as it arrived. */\n sendToDlq(message: Message): Promise<void>\n}\n\nconst logger = createLogger('queue-client')\n\n/** Immediate retries of one binding call: three attempts, 250 ms then 500 ms, with jitter. */\nconst RETRY_TIMES = 2\nconst RETRY_BASE_DELAY_MS = 250\n\n/** Every send is safe to repeat, so a transient failure gets an immediate retry; terminal and unknown stop. */\nfunction shouldRetrySend(error: QueueSendError): boolean {\n return shouldRetryFailure({\n classification: error.classification,\n repeatSafe: true,\n owner: 'immediate',\n })\n}\n\nfunction oversizedError(sized: SizedMessage<QueueMessage>): OversizedMessageError {\n return new OversizedMessageError({\n messageName: sized.message.name,\n bytes: sized.bytes,\n limit: QUEUE_LIMITS.messageBytes,\n })\n}\n\n/**\n * Sends messages to the application's queues. Checks the size limits before a send, retries a\n * transient binding failure a bounded number of times, and converts the final failure to `QueueSendError`.\n */\nexport class QueueClient<\n TMessage extends QueueMessage,\n Lane extends string,\n> implements IQueueClient<TMessage> {\n constructor(private readonly options: QueueClientOptions<TMessage, Lane>) {}\n\n /** @throws OversizedMessageError before the send. @throws QueueSendError after the retries. */\n async send(message: TMessage, options?: QueueSendOptions): Promise<void> {\n const sized = { message, bytes: messageByteLength(message) }\n if (isOversized(sized)) {\n logger.warn('queue_message_oversized', {\n name: message.name,\n type: message.type,\n bytes: sized.bytes,\n })\n throw oversizedError(sized)\n }\n\n const queue = this.queueFor(message)\n logger.debug('sending_message', {\n name: message.name,\n type: message.type,\n delaySeconds: options?.delaySeconds,\n })\n\n await this.callQueue('send', async () => {\n if (options) {\n await queue.send(message, options)\n } else {\n await queue.send(message)\n }\n })\n }\n\n /** Rejects the whole batch before any send when one message is oversized. */\n async sendBatch(messages: ReadonlyArray<TMessage>): Promise<void> {\n const sized = sizeMessages(messages)\n const oversized = sized.find(isOversized)\n if (oversized) throw oversizedError(oversized)\n await this.sendSizedBatch(sized)\n }\n\n async sendBatchPartial<Sent extends TMessage>(\n messages: ReadonlyArray<Sent>,\n ): Promise<ReadonlyArray<Sent>> {\n const sized = sizeMessages(messages)\n await this.sendSizedBatch(sized.filter((item) => !isOversized(item)))\n return sized.filter(isOversized).map((item) => item.message)\n }\n\n async sendToDlq(message: Message): Promise<void> {\n logger.debug('sending_to_dlq', { queueMessageId: message.id })\n await this.callQueue('send_dlq', async () => {\n await this.options.deadLetter.send(message.body)\n })\n }\n\n /** Each lane's chunks retry on their own, so one failed chunk does not resend the others. */\n private async sendSizedBatch(sized: ReadonlyArray<SizedMessage<TMessage>>): Promise<void> {\n const byLane = new Map<Lane, SizedMessage<TMessage>[]>()\n for (const item of sized) {\n const lane = this.options.laneOf(item.message)\n const items = byLane.get(lane) ?? []\n items.push(item)\n byLane.set(lane, items)\n }\n\n logger.debug('sending_batch', {\n count: sized.length,\n lanes: [...byLane].map(([lane, items]) => ({ lane, count: items.length })),\n })\n\n const sends: Promise<void>[] = []\n for (const [lane, items] of byLane) {\n const queue = this.options.queues[lane]\n for (const group of chunkByLimits(items)) {\n sends.push(\n this.callQueue('send_batch', async () => {\n await queue.sendBatch(group.map((body) => ({ body })))\n }),\n )\n }\n }\n await Promise.all(sends)\n }\n\n private queueFor(message: TMessage): QueueBinding<TMessage> {\n return this.options.queues[this.options.laneOf(message)]\n }\n\n private async callQueue<T>(operation: QueueOperation, call: () => Promise<T>): Promise<T> {\n const result = await Result.tryPromise(\n {\n try: call,\n catch: (cause) => new QueueSendError({ operation, cause }),\n },\n {\n retry: {\n times: RETRY_TIMES,\n delayMs: RETRY_BASE_DELAY_MS,\n backoff: 'exponential',\n jitter: true,\n shouldRetry: shouldRetrySend,\n },\n },\n )\n if (result.isErr()) throw result.error\n return result.value\n }\n}\n","import {\n classifyFailure,\n exponentialEqualJitterMs,\n retryAfterSecondsOf,\n shouldRetryFailure,\n spreadJitterMs,\n type FailureClassification,\n} from '@/core'\n\nconst BASE_RETRY_DELAY_MS = 5_000\nconst MAX_RETRY_DELAY_MS = 120_000\nconst RETRY_AFTER_SPREAD_MS = 5_000\n/** Cloudflare Queues accept a redelivery delay of at most one day. */\nconst MAX_QUEUE_DELAY_SECONDS = 24 * 60 * 60\n\n/** How the consumer settles one failed message: acknowledge it, or have Cloudflare redeliver it after a delay. */\nexport type RedeliveryDecision =\n | { readonly action: 'ack'; readonly classification: 'terminal' }\n | {\n readonly action: 'retry'\n readonly classification: Exclude<FailureClassification, 'terminal'>\n readonly delaySeconds: number\n }\n\n/**\n * Decide whether the consumer acknowledges a failed message or asks Cloudflare to redeliver it.\n * A terminal failure acknowledges. Transient and unknown failures redeliver: after the advertised\n * wait plus a spread when the error carries one, else with exponential equal-jitter backoff.\n * An `AggregateError` from event delivery redelivers when any member does, after the longest wait.\n */\nexport function decideRedelivery(cause: unknown, attempt: number): RedeliveryDecision {\n if (cause instanceof AggregateError && cause.errors.length > 0) {\n const decisions = cause.errors.map((member) => decideRedelivery(member, attempt))\n const retries = decisions.filter((decision) => decision.action === 'retry')\n if (retries.length === 0) return { action: 'ack', classification: 'terminal' }\n\n // Redelivery must respect every failed subscriber's required wait.\n return {\n action: 'retry',\n classification: retries.some((decision) => decision.classification === 'unknown')\n ? 'unknown'\n : 'transient',\n delaySeconds: Math.max(...retries.map((decision) => decision.delaySeconds)),\n }\n }\n\n const classification = classifyFailure(cause)\n if (classification === 'terminal') return { action: 'ack', classification }\n\n const canRetry = shouldRetryFailure({ classification, repeatSafe: true, owner: 'durable' })\n if (!canRetry) return { action: 'ack', classification: 'terminal' }\n\n const retryAfterSeconds = retryAfterSecondsOf(cause)\n const delayMs =\n retryAfterSeconds === null\n ? exponentialEqualJitterMs(BASE_RETRY_DELAY_MS, MAX_RETRY_DELAY_MS, attempt)\n : retryAfterSeconds * 1_000 + spreadJitterMs(RETRY_AFTER_SPREAD_MS)\n\n return {\n action: 'retry',\n classification,\n delaySeconds: Math.min(MAX_QUEUE_DELAY_SECONDS, Math.max(1, Math.ceil(delayMs / 1_000))),\n }\n}\n","import type { z } from 'zod'\n\nimport { MessageAlreadyProcessedError } from '@/application'\nimport { createLogger } from '@/core'\n\nimport type { IQueueClient } from './queue-client.ts'\nimport type { QueueMessage } from './queue-message.ts'\nimport { DeadLetterUnavailableError } from './queue.errors.ts'\nimport { decideRedelivery } from './redelivery-policy.ts'\n\nconst logger = createLogger('queue-consumer')\n\n/** Wraps one dispatch: an error-tracking scope, an analytics context. The default runs it as is. */\nexport type DispatchWrapper<TMessage extends QueueMessage> = (\n message: TMessage,\n attempt: number,\n run: () => Promise<void>,\n) => Promise<void>\n\n/** The one call the consumer makes on the bus. */\nexport type DispatchBus<TMessage extends QueueMessage> = {\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- the bus resolves to each handler's own value; the consumer discards it.\n handle(message: TMessage): Promise<unknown>\n}\n\n/**\n * Parses and dispatches each queue message, then selects acknowledgement or retry.\n * Cloudflare owns the retry limit and sends exhausted messages to the configured dead letter.\n * The consumer logs each failure once; handlers do not.\n */\nexport class QueueConsumer<TMessage extends QueueMessage> {\n /**\n * @param bus - Where a parsed message goes.\n * @param queueClient - Parks an unparseable body in the dead letter.\n * @param schema - What a body may be: the application's `messages.queueSchema`.\n * @param wrapDispatch - Wraps one dispatch in the application's context. Default: run it as is.\n */\n constructor(\n private readonly bus: DispatchBus<TMessage>,\n private readonly queueClient: Pick<IQueueClient<TMessage>, 'sendToDlq'>,\n private readonly schema: z.ZodType<TMessage>,\n private readonly wrapDispatch: DispatchWrapper<TMessage> = (_message, _attempt, run) => run(),\n ) {}\n\n /** Every message settles on its own. A settlement that throws rejects the batch after its siblings settle. */\n async processBatch(batch: MessageBatch): Promise<void> {\n logger.debug('queue_batch_received', { count: batch.messages.length })\n const results = await Promise.allSettled(\n batch.messages.map((message) => this.processMessage(message)),\n )\n const rejected = results\n .filter((result) => result.status === 'rejected')\n .map((result) => result.reason)\n\n if (rejected.length > 0) {\n throw new AggregateError(rejected, 'Queue message settlement failed')\n }\n }\n\n private async processMessage(message: Message): Promise<void> {\n let parsed: TMessage | null = null\n\n try {\n const body = await this.parseMessage(message)\n if (body === null) {\n // An unparseable body cannot succeed on retry, so park it for inspection.\n logger.error('invalid_message_format', {\n details: { attempt: message.attempts, queueMessageId: message.id },\n })\n await this.parkPoison(message)\n return\n }\n parsed = body\n\n await this.wrapDispatch(body, message.attempts, async () => {\n await this.bus.handle(body)\n })\n message.ack()\n logger.debug(`queue_message_processed:${body.name}`, { name: body.name })\n } catch (cause) {\n this.handleFailure(message, parsed, cause)\n }\n }\n\n /** Returns null on failure; the caller owns the single log point. */\n private async parseMessage(message: Message): Promise<TMessage | null> {\n const result = await this.schema.safeParseAsync(message.body)\n return result.success ? result.data : null\n }\n\n /** Park an unparseable message in the dead letter, then acknowledge its source delivery. */\n private async parkPoison(message: Message): Promise<void> {\n try {\n await this.queueClient.sendToDlq(message)\n message.ack()\n } catch (cause) {\n throw new DeadLetterUnavailableError(cause)\n }\n }\n\n /**\n * Retry declared transient and unknown failures so Cloudflare can enforce the attempt ceiling.\n * Acknowledge only declared terminal failures.\n */\n private handleFailure(message: Message, parsed: TMessage | null, cause: unknown): void {\n if (cause instanceof MessageAlreadyProcessedError) {\n logger.debug('duplicate_skipped', {\n name: parsed?.name,\n messageId: cause.messageId,\n attempt: message.attempts,\n })\n message.ack()\n return\n }\n\n const decision = decideRedelivery(cause, message.attempts)\n if (decision.action === 'ack') {\n logger.error('message_discarded', {\n error: cause,\n details: {\n name: parsed?.name,\n messageId: parsed?.id,\n queueMessageId: message.id,\n attempt: message.attempts,\n },\n })\n message.ack()\n return\n }\n\n const context = {\n name: parsed?.name,\n messageId: parsed?.id,\n queueMessageId: message.id,\n attempt: message.attempts,\n classification: decision.classification,\n delaySeconds: decision.delaySeconds,\n }\n if (decision.classification === 'unknown' && message.attempts === 1) {\n logger.error('queue_unknown_failure', { error: cause, details: context })\n } else {\n logger.debug('redelivery_requested', context)\n }\n message.retry({ delaySeconds: decision.delaySeconds })\n }\n}\n","import type { IOutboxRelay, OutboxRelay } from '@/application'\nimport { createLogger } from '@/core'\n\nconst logger = createLogger('outbox-wake')\n\n/** Register delivery after commit; log a failed drain once and leave recovery to the scheduled sweep. */\nexport function waitUntilWake(\n context: Pick<ExecutionContext, 'waitUntil'>,\n relay: Pick<OutboxRelay, 'drain'>,\n): IOutboxRelay {\n return {\n wake: () =>\n context.waitUntil(\n relay.drain().catch((cause: unknown) => {\n logger.error('outbox_wake_failed', { error: cause })\n }),\n ),\n }\n}\n"],"mappings":";;;;;;;;;;AAOA,MAAa,eAAe;CAC1B,cAAc;CACd,YAAY;CACZ,YAAY;AACd;;AAGA,SAAgB,kBAAkB,SAA+B;CAC/D,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC;AAC3D;;AAMA,SAAgB,aACd,UAC0B;CAC1B,OAAO,SAAS,KAAK,aAAa;EAAE;EAAS,OAAO,kBAAkB,OAAO;CAAE,EAAE;AACnF;;AAGA,SAAgB,YAAY,OAA4C;CACtE,OAAO,MAAM,QAAQ,aAAa;AACpC;;AAGA,SAAgB,cACd,OACc;CACd,MAAM,SAAuB,CAAC;CAC9B,IAAI,UAAsB,CAAC;CAC3B,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OACJ,QAAQ,UAAU,aAAa,cAAc,QAAQ,KAAK,QAAQ,aAAa;EACjF,IAAI,QAAQ,SAAS,KAAK,MAAM;GAC9B,OAAO,KAAK,OAAO;GACnB,UAAU,CAAC;GACX,QAAQ;EACV;EACA,QAAQ,KAAK,KAAK,OAAO;EACzB,SAAS,KAAK;CAChB;CACA,IAAI,QAAQ,SAAS,GAAG,OAAO,KAAK,OAAO;CAC3C,OAAO;AACT;;;;AChDA,MAAM,2BAA2B;CAC/B,MAAM;CACN,YAAY;CACZ,UAAU;AACZ;;;;;;AAOA,MAAM,8CAA8B,IAAI,IAAmC;CACzE,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,WAAW;CACnB,CAAC,OAAO,UAAU;CAClB,CAAC,OAAO,UAAU;CAClB,CAAC,MAAO,SAAS;AACnB,CAAC;AAED,SAAS,sBAAsB,OAAoC;CACjE,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,MAAM,QAAQ,iBAAiB,KAAK,MAAM,OAAO;CACjD,OAAO,QAAQ,OAAO,SAAY,SAAY,OAAO,MAAM,EAAE;AAC/D;AAEA,SAAS,2BAA2B,OAAmD;CACrF,MAAM,OAAO,sBAAsB,KAAK;CACxC,OAAO,SAAS,SAAY,SAAY,4BAA4B,IAAI,IAAI;AAC9E;;AAGA,SAAgB,mBAAmB,OAAuC;CACxE,OAAO,sBAAsB;EAC3B;EACA,kBAAkB;EAClB,mBAAmB;CACrB,CAAC;AACH;;AAGA,IAAa,iBAAb,cAAoC,SAAS;CAC3C,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;CAET,YAAY,MAAuE;EACjF,MAAM,yBAAyB,KAAK,YAAY,EAAE,OAAO,KAAK,MAAM,CAAC;EACrE,KAAK,iBAAiB,mBAAmB,KAAK,KAAK;EACnD,KAAK,YAAY,KAAK;CACxB;AACF;;;;;AAMA,IAAa,wBAAb,cAA2C,SAAS;CAClD,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAC1B,AAAS;CACT,AAAS;CACT,AAAS;CAET,YAAY,MAIT;EACD,MACE,kBAAkB,KAAK,YAAY,OAAO,OAAO,KAAK,KAAK,EAAE,mBAAmB,OAAO,KAAK,KAAK,EAAE,OACrG;EACA,KAAK,cAAc,KAAK;EACxB,KAAK,QAAQ,KAAK;EAClB,KAAK,QAAQ,KAAK;CACpB;AACF;;;;;AAMA,IAAa,6BAAb,cAAgD,SAAS;CACvD,AAAS,OAAO;CAChB,AAAS,iBAAiB;CAE1B,YAAY,OAAgB;EAC1B,MAAM,8CAA8C,EAAE,MAAM,CAAC;CAC/D;AACF;;;;AC5DA,MAAMA,WAAS,aAAa,cAAc;;AAG1C,MAAM,cAAc;AACpB,MAAM,sBAAsB;;AAG5B,SAAS,gBAAgB,OAAgC;CACvD,OAAO,mBAAmB;EACxB,gBAAgB,MAAM;EACtB,YAAY;EACZ,OAAO;CACT,CAAC;AACH;AAEA,SAAS,eAAe,OAA0D;CAChF,OAAO,IAAI,sBAAsB;EAC/B,aAAa,MAAM,QAAQ;EAC3B,OAAO,MAAM;EACb,OAAO,aAAa;CACtB,CAAC;AACH;;;;;AAMA,IAAa,cAAb,MAGoC;CACL;CAA7B,YAAY,AAAiB,SAA6C;EAA7C;CAA8C;;CAG3E,MAAM,KAAK,SAAmB,SAA2C;EACvE,MAAM,QAAQ;GAAE;GAAS,OAAO,kBAAkB,OAAO;EAAE;EAC3D,IAAI,YAAY,KAAK,GAAG;GACtB,SAAO,KAAK,2BAA2B;IACrC,MAAM,QAAQ;IACd,MAAM,QAAQ;IACd,OAAO,MAAM;GACf,CAAC;GACD,MAAM,eAAe,KAAK;EAC5B;EAEA,MAAM,QAAQ,KAAK,SAAS,OAAO;EACnC,SAAO,MAAM,mBAAmB;GAC9B,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,cAAc,SAAS;EACzB,CAAC;EAED,MAAM,KAAK,UAAU,QAAQ,YAAY;GACvC,IAAI,SACF,MAAM,MAAM,KAAK,SAAS,OAAO;QAEjC,MAAM,MAAM,KAAK,OAAO;EAE5B,CAAC;CACH;;CAGA,MAAM,UAAU,UAAkD;EAChE,MAAM,QAAQ,aAAa,QAAQ;EACnC,MAAM,YAAY,MAAM,KAAK,WAAW;EACxC,IAAI,WAAW,MAAM,eAAe,SAAS;EAC7C,MAAM,KAAK,eAAe,KAAK;CACjC;CAEA,MAAM,iBACJ,UAC8B;EAC9B,MAAM,QAAQ,aAAa,QAAQ;EACnC,MAAM,KAAK,eAAe,MAAM,QAAQ,SAAS,CAAC,YAAY,IAAI,CAAC,CAAC;EACpE,OAAO,MAAM,OAAO,WAAW,CAAC,CAAC,KAAK,SAAS,KAAK,OAAO;CAC7D;CAEA,MAAM,UAAU,SAAiC;EAC/C,SAAO,MAAM,kBAAkB,EAAE,gBAAgB,QAAQ,GAAG,CAAC;EAC7D,MAAM,KAAK,UAAU,YAAY,YAAY;GAC3C,MAAM,KAAK,QAAQ,WAAW,KAAK,QAAQ,IAAI;EACjD,CAAC;CACH;;CAGA,MAAc,eAAe,OAA6D;EACxF,MAAM,yBAAS,IAAI,IAAoC;EACvD,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,OAAO,KAAK,QAAQ,OAAO,KAAK,OAAO;GAC7C,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK,CAAC;GACnC,MAAM,KAAK,IAAI;GACf,OAAO,IAAI,MAAM,KAAK;EACxB;EAEA,SAAO,MAAM,iBAAiB;GAC5B,OAAO,MAAM;GACb,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY;IAAE;IAAM,OAAO,MAAM;GAAO,EAAE;EAC3E,CAAC;EAED,MAAM,QAAyB,CAAC;EAChC,KAAK,MAAM,CAAC,MAAM,UAAU,QAAQ;GAClC,MAAM,QAAQ,KAAK,QAAQ,OAAO;GAClC,KAAK,MAAM,SAAS,cAAc,KAAK,GACrC,MAAM,KACJ,KAAK,UAAU,cAAc,YAAY;IACvC,MAAM,MAAM,UAAU,MAAM,KAAK,UAAU,EAAE,KAAK,EAAE,CAAC;GACvD,CAAC,CACH;EAEJ;EACA,MAAM,QAAQ,IAAI,KAAK;CACzB;CAEA,AAAQ,SAAS,SAA2C;EAC1D,OAAO,KAAK,QAAQ,OAAO,KAAK,QAAQ,OAAO,OAAO;CACxD;CAEA,MAAc,UAAa,WAA2B,MAAoC;EACxF,MAAM,SAAS,MAAM,OAAO,WAC1B;GACE,KAAK;GACL,QAAQ,UAAU,IAAI,eAAe;IAAE;IAAW;GAAM,CAAC;EAC3D,GACA,EACE,OAAO;GACL,OAAO;GACP,SAAS;GACT,SAAS;GACT,QAAQ;GACR,aAAa;EACf,EACF,CACF;EACA,IAAI,OAAO,MAAM,GAAG,MAAM,OAAO;EACjC,OAAO,OAAO;CAChB;AACF;;;;AC3KA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAC3B,MAAM,wBAAwB;;AAE9B,MAAM,0BAA0B;;;;;;;AAiBhC,SAAgB,iBAAiB,OAAgB,SAAqC;CACpF,IAAI,iBAAiB,kBAAkB,MAAM,OAAO,SAAS,GAAG;EAE9D,MAAM,UADY,MAAM,OAAO,KAAK,WAAW,iBAAiB,QAAQ,OAAO,CACvD,CAAC,CAAC,QAAQ,aAAa,SAAS,WAAW,OAAO;EAC1E,IAAI,QAAQ,WAAW,GAAG,OAAO;GAAE,QAAQ;GAAO,gBAAgB;EAAW;EAG7E,OAAO;GACL,QAAQ;GACR,gBAAgB,QAAQ,MAAM,aAAa,SAAS,mBAAmB,SAAS,IAC5E,YACA;GACJ,cAAc,KAAK,IAAI,GAAG,QAAQ,KAAK,aAAa,SAAS,YAAY,CAAC;EAC5E;CACF;CAEA,MAAM,iBAAiB,gBAAgB,KAAK;CAC5C,IAAI,mBAAmB,YAAY,OAAO;EAAE,QAAQ;EAAO;CAAe;CAG1E,IAAI,CADa,mBAAmB;EAAE;EAAgB,YAAY;EAAM,OAAO;CAAU,CAC7E,GAAG,OAAO;EAAE,QAAQ;EAAO,gBAAgB;CAAW;CAElE,MAAM,oBAAoB,oBAAoB,KAAK;CACnD,MAAM,UACJ,sBAAsB,OAClB,yBAAyB,qBAAqB,oBAAoB,OAAO,IACzE,oBAAoB,MAAQ,eAAe,qBAAqB;CAEtE,OAAO;EACL,QAAQ;EACR;EACA,cAAc,KAAK,IAAI,yBAAyB,KAAK,IAAI,GAAG,KAAK,KAAK,UAAU,GAAK,CAAC,CAAC;CACzF;AACF;;;;ACrDA,MAAMC,WAAS,aAAa,gBAAgB;;;;;;AAoB5C,IAAa,gBAAb,MAA0D;CAQrC;CACA;CACA;CACA;;;;;;;CAJnB,YACE,AAAiB,KACjB,AAAiB,aACjB,AAAiB,QACjB,AAAiB,gBAA2C,UAAU,UAAU,QAAQ,IAAI,GAC5F;EAJiB;EACA;EACA;EACA;CAChB;;CAGH,MAAM,aAAa,OAAoC;EACrD,SAAO,MAAM,wBAAwB,EAAE,OAAO,MAAM,SAAS,OAAO,CAAC;EAIrE,MAAM,YAAW,MAHK,QAAQ,WAC5B,MAAM,SAAS,KAAK,YAAY,KAAK,eAAe,OAAO,CAAC,CAC9D,EACwB,CACrB,QAAQ,WAAW,OAAO,WAAW,UAAU,CAAC,CAChD,KAAK,WAAW,OAAO,MAAM;EAEhC,IAAI,SAAS,SAAS,GACpB,MAAM,IAAI,eAAe,UAAU,iCAAiC;CAExE;CAEA,MAAc,eAAe,SAAiC;EAC5D,IAAI,SAA0B;EAE9B,IAAI;GACF,MAAM,OAAO,MAAM,KAAK,aAAa,OAAO;GAC5C,IAAI,SAAS,MAAM;IAEjB,SAAO,MAAM,0BAA0B,EACrC,SAAS;KAAE,SAAS,QAAQ;KAAU,gBAAgB,QAAQ;IAAG,EACnE,CAAC;IACD,MAAM,KAAK,WAAW,OAAO;IAC7B;GACF;GACA,SAAS;GAET,MAAM,KAAK,aAAa,MAAM,QAAQ,UAAU,YAAY;IAC1D,MAAM,KAAK,IAAI,OAAO,IAAI;GAC5B,CAAC;GACD,QAAQ,IAAI;GACZ,SAAO,MAAM,2BAA2B,KAAK,QAAQ,EAAE,MAAM,KAAK,KAAK,CAAC;EAC1E,SAAS,OAAO;GACd,KAAK,cAAc,SAAS,QAAQ,KAAK;EAC3C;CACF;;CAGA,MAAc,aAAa,SAA4C;EACrE,MAAM,SAAS,MAAM,KAAK,OAAO,eAAe,QAAQ,IAAI;EAC5D,OAAO,OAAO,UAAU,OAAO,OAAO;CACxC;;CAGA,MAAc,WAAW,SAAiC;EACxD,IAAI;GACF,MAAM,KAAK,YAAY,UAAU,OAAO;GACxC,QAAQ,IAAI;EACd,SAAS,OAAO;GACd,MAAM,IAAI,2BAA2B,KAAK;EAC5C;CACF;;;;;CAMA,AAAQ,cAAc,SAAkB,QAAyB,OAAsB;EACrF,IAAI,iBAAiB,8BAA8B;GACjD,SAAO,MAAM,qBAAqB;IAChC,MAAM,QAAQ;IACd,WAAW,MAAM;IACjB,SAAS,QAAQ;GACnB,CAAC;GACD,QAAQ,IAAI;GACZ;EACF;EAEA,MAAM,WAAW,iBAAiB,OAAO,QAAQ,QAAQ;EACzD,IAAI,SAAS,WAAW,OAAO;GAC7B,SAAO,MAAM,qBAAqB;IAChC,OAAO;IACP,SAAS;KACP,MAAM,QAAQ;KACd,WAAW,QAAQ;KACnB,gBAAgB,QAAQ;KACxB,SAAS,QAAQ;IACnB;GACF,CAAC;GACD,QAAQ,IAAI;GACZ;EACF;EAEA,MAAM,UAAU;GACd,MAAM,QAAQ;GACd,WAAW,QAAQ;GACnB,gBAAgB,QAAQ;GACxB,SAAS,QAAQ;GACjB,gBAAgB,SAAS;GACzB,cAAc,SAAS;EACzB;EACA,IAAI,SAAS,mBAAmB,aAAa,QAAQ,aAAa,GAChE,SAAO,MAAM,yBAAyB;GAAE,OAAO;GAAO,SAAS;EAAQ,CAAC;OAExE,SAAO,MAAM,wBAAwB,OAAO;EAE9C,QAAQ,MAAM,EAAE,cAAc,SAAS,aAAa,CAAC;CACvD;AACF;;;;AC9IA,MAAM,SAAS,aAAa,aAAa;;AAGzC,SAAgB,cACd,SACA,OACc;CACd,OAAO,EACL,YACE,QAAQ,UACN,MAAM,MAAM,CAAC,CAAC,OAAO,UAAmB;EACtC,OAAO,MAAM,sBAAsB,EAAE,OAAO,MAAM,CAAC;CACrD,CAAC,CACH,EACJ;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { J as MessageId } from "../index-B5O6mNjZ.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/infrastructure/cloudflare/scheduled/scheduled-handler.d.ts
|
|
4
4
|
/** The work one schedule runs. Nothing returns to the platform, so each run reports its own outcome. */
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as DrizzleSchema, t as
|
|
1
|
+
import { G as DomainEvent, J as MessageId, nt as DatabaseError, rt as DatabaseFailure, tt as D1ConstraintKind } from "./index-B5O6mNjZ.js";
|
|
2
|
+
import { A as IOutboxRepository, M as OutboxClaim, N as OutboxMessage, P as IMessageStore, k as DeadLetterReason, w as ITransaction } from "./index-DG4jkwad.js";
|
|
3
|
+
import { n as eventCollector, r as DrizzleSchema, t as OutboxRepositoryOptions } from "./outbox-repository-options-BiilQf4_.js";
|
|
4
4
|
import { BatchItem } from "drizzle-orm/batch";
|
|
5
5
|
import { AnyD1Database, DrizzleD1Database } from "drizzle-orm/d1";
|
|
6
6
|
//#region src/infrastructure/cloudflare/d1-database-types.d.ts
|
|
@@ -297,5 +297,31 @@ export declare class D1BatchTransaction<Schema extends DrizzleSchema> implements
|
|
|
297
297
|
/** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */
|
|
298
298
|
export declare function executeDatabase<T>(operation: string, work: () => PromiseLike<T>): Promise<T>;
|
|
299
299
|
//#endregion
|
|
300
|
-
|
|
300
|
+
//#region src/infrastructure/cloudflare/d1-outbox-repository.d.ts
|
|
301
|
+
/**
|
|
302
|
+
* Typist's atomic D1 claims on a plain connection, outside a unit of work.
|
|
303
|
+
* Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.
|
|
304
|
+
*/
|
|
305
|
+
export declare class D1OutboxRepository<Schema extends DrizzleSchema> implements IOutboxRepository {
|
|
306
|
+
private readonly db;
|
|
307
|
+
private readonly now;
|
|
308
|
+
private readonly options;
|
|
309
|
+
constructor(db: D1Db<Schema>, options?: OutboxRepositoryOptions, now?: () => Date);
|
|
310
|
+
/** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */
|
|
311
|
+
claimPending(batchSize: number): Promise<OutboxClaim>;
|
|
312
|
+
/** Mark only supplied rows that still hold this token as published. */
|
|
313
|
+
markPublished(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void>;
|
|
314
|
+
/** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */
|
|
315
|
+
releaseClaim(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void>;
|
|
316
|
+
/** Retain the full payload and record a closed reason with a readable error message. */
|
|
317
|
+
deadLetter(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>, reason: DeadLetterReason, errorMessage: string): Promise<void>;
|
|
318
|
+
/** Delete at most batchSize published rows, strictly before the cutoff. */
|
|
319
|
+
deletePublishedBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
320
|
+
/** Delete at most batchSize failed rows; pending and published rows remain untouched. */
|
|
321
|
+
deleteFailedBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
322
|
+
/** Delete at most batchSize receipts, strictly before the cutoff. */
|
|
323
|
+
deleteReceiptsBefore(cutoff: Date, batchSize: number): Promise<number>;
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
export { type AggregateEvents, type D1Connection, type D1ConstraintKind, type D1Db, type D1ReadDb, type D1Tx, DatabaseError, type DatabaseFailure, type OutboxRepositoryOptions, eventCollector };
|
|
301
327
|
//# sourceMappingURL=cloudflare.d.ts.map
|
package/dist/cloudflare.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.d.ts","names":[],"sources":["../src/infrastructure/cloudflare/d1-database-types.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-database-execution.ts"],"mappings":";;;;;;;KAKY,eAAe;;KAGf,KAAK,eAAe,iBAAiB,kBAAkB;;KAGvD,SAAS,eAAe,iBAAiB,KACnD,KAAK;;KAKK,KAAK,eAAe;WACrB,IAAI,KAAK;;;;;UCRH;EACf,iBAAiB,cAAc;EAC/B;;;;;;8BAOoB,iBAAiB,eAAe;qBACrB,IAAI,KAAK;EAAxC,YAA+B,IAAI,KAAK;;YAG9B,MAAM,WAAW;;YAKjB,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;;;;wBCVtB,gBAAgB,gBAAgB,qBAAyB;;;;qBCb5D,eAAe,eAAe,uBACjC,iBAAiB,mBACd;EAEL,MAAM,WAAW,YAAY;EAc7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCtB9C,qDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAYA,iDAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCPb,mBAAmB,eAAe,0BAA0B,aACvE,KAAK;mBAEwB;EAA7B,YAA6B,IAAI,KAAK;EAEhC,YAAY,GAAG,OAAO,IAAI,KAAK,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ"}
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","names":[],"sources":["../src/infrastructure/cloudflare/d1-database-types.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/d1-outbox-repository.ts"],"mappings":";;;;;;;KAKY,eAAe;;KAGf,KAAK,eAAe,iBAAiB,kBAAkB;;KAGvD,SAAS,eAAe,iBAAiB,KACnD,KAAK;;KAKK,KAAK,eAAe;WACrB,IAAI,KAAK;;;;;UCRH;EACf,iBAAiB,cAAc;EAC/B;;;;;;8BAOoB,iBAAiB,eAAe;qBACrB,IAAI,KAAK;EAAxC,YAA+B,IAAI,KAAK;;YAG9B,MAAM,WAAW;;YAKjB,KAAK,GAAG,mBAAmB,aAAa,YAAY,KAAK,QAAQ;;YAKjE,eAAe,WAAW;;;;;;;;wBCVtB,gBAAgB,gBAAgB,qBAAyB;;;;qBCb5D,eAAe,eAAe,uBACjC,iBAAiB,mBACd;EAEL,MAAM,WAAW,YAAY;EAc7B,cAAc,QAAQ,cAAc,eAAe;;;;;qBCtB9C,qDAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAAjB;;;;;;;;;;;;;;;;qBAYA,iDAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCPb,mBAAmB,eAAe,0BAA0B,aACvE,KAAK;mBAEwB;EAA7B,YAA6B,IAAI,KAAK;EAEhC,YAAY,GAAG,OAAO,IAAI,KAAK,YAAY,QAAQ,KAAK,QAAQ;;;;;wBCblD,gBAAgB,GACpC,mBACA,YAAY,YAAY,KACvB,QAAQ;;;;;;;qBCYE,mBAAmB,eAAe,0BAA0B;mBAIpD;mBAEA;mBALF;EAEjB,YACmB,IAAI,KAAK,SAC1B,UAAS,yBACQ,YAAW;;EAMxB,aAAa,oBAAoB,QAAQ;;EAmDzC,cAAc,OAAO,aAAa,UAAU,cAAc,iBAAiB;;EAgB3E,aAAa,OAAO,aAAa,UAAU,cAAc,iBAAiB;;EA6B1E,WACJ,OAAO,aACP,UAAU,cAAc,gBACxB,QAAQ,kBACR,uBACC;;EAqBG,sBAAsB,QAAQ,MAAM,oBAAoB;;EAqBxD,mBAAmB,QAAQ,MAAM,oBAAoB;;EAqBrD,qBAAqB,QAAQ,MAAM,oBAAoB"}
|
package/dist/cloudflare.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { $ as AppError, D as DatabaseError, c as exponentialDelayCapMs } from "./core-BiqdOMwU.js";
|
|
2
|
+
import { o as MessageAlreadyProcessedError } from "./application-DnlgWcOZ.js";
|
|
3
|
+
import { a as stageD1Statement, i as runWithUnitOfWorkContext, n as collectD1Statements, r as eventCollector, t as OUTBOX_REPOSITORY_DEFAULTS } from "./outbox-repository-options-BI75j7b7.js";
|
|
4
4
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
5
|
-
import { eq, sql } from "drizzle-orm";
|
|
5
|
+
import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
6
6
|
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
7
7
|
|
|
8
8
|
//#region src/infrastructure/cloudflare/classify-d1-error.ts
|
|
@@ -165,5 +165,98 @@ var D1BatchTransaction = class {
|
|
|
165
165
|
};
|
|
166
166
|
|
|
167
167
|
//#endregion
|
|
168
|
-
|
|
168
|
+
//#region src/infrastructure/cloudflare/d1-outbox-repository.ts
|
|
169
|
+
/**
|
|
170
|
+
* Typist's atomic D1 claims on a plain connection, outside a unit of work.
|
|
171
|
+
* Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.
|
|
172
|
+
*/
|
|
173
|
+
var D1OutboxRepository = class {
|
|
174
|
+
db;
|
|
175
|
+
now;
|
|
176
|
+
options;
|
|
177
|
+
constructor(db, options = {}, now = () => /* @__PURE__ */ new Date()) {
|
|
178
|
+
this.db = db;
|
|
179
|
+
this.now = now;
|
|
180
|
+
this.options = {
|
|
181
|
+
...OUTBOX_REPOSITORY_DEFAULTS,
|
|
182
|
+
...options
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */
|
|
186
|
+
async claimPending(batchSize) {
|
|
187
|
+
const token = crypto.randomUUID();
|
|
188
|
+
const now = this.now();
|
|
189
|
+
const rows = await executeDatabase("claim pending outbox messages", () => this.db.update(messageOutbox).set({
|
|
190
|
+
claimToken: token,
|
|
191
|
+
claimExpiresAt: deadline(now.getTime() + this.options.leaseMs),
|
|
192
|
+
attempts: sql`${messageOutbox.attempts} + 1`
|
|
193
|
+
}).where(inArray(messageOutbox.id, this.db.select({ id: messageOutbox.id }).from(messageOutbox).where(and(isNull(messageOutbox.publishedAt), isNull(messageOutbox.failedAt), lte(messageOutbox.availableAt, now), or(isNull(messageOutbox.claimExpiresAt), lte(messageOutbox.claimExpiresAt, now)))).orderBy(asc(messageOutbox.availableAt), asc(messageOutbox.createdAt), asc(messageOutbox.id)).limit(batchSize))).returning({
|
|
194
|
+
payload: messageOutbox.payload,
|
|
195
|
+
attempts: messageOutbox.attempts,
|
|
196
|
+
availableAt: messageOutbox.availableAt,
|
|
197
|
+
createdAt: messageOutbox.createdAt
|
|
198
|
+
}));
|
|
199
|
+
rows.sort((a, b) => a.availableAt.getTime() - b.availableAt.getTime() || a.createdAt.getTime() - b.createdAt.getTime() || a.payload.id.localeCompare(b.payload.id));
|
|
200
|
+
return {
|
|
201
|
+
token,
|
|
202
|
+
messages: rows.map(({ payload, attempts }) => ({
|
|
203
|
+
payload,
|
|
204
|
+
attempts
|
|
205
|
+
}))
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Mark only supplied rows that still hold this token as published. */
|
|
209
|
+
async markPublished(claim, messages) {
|
|
210
|
+
if (messages.length === 0) return;
|
|
211
|
+
await executeDatabase("mark outbox messages published", () => this.db.update(messageOutbox).set({
|
|
212
|
+
publishedAt: this.now(),
|
|
213
|
+
claimToken: null,
|
|
214
|
+
claimExpiresAt: null
|
|
215
|
+
}).where(and(inArray(messageOutbox.id, outboxIds(messages)), eq(messageOutbox.claimToken, claim.token))));
|
|
216
|
+
}
|
|
217
|
+
/** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */
|
|
218
|
+
async releaseClaim(claim, messages) {
|
|
219
|
+
const now = this.now();
|
|
220
|
+
for (const attempts of new Set(messages.map((row) => row.attempts))) {
|
|
221
|
+
const rows = messages.filter((row) => row.attempts === attempts);
|
|
222
|
+
const delay = exponentialDelayCapMs(this.options.retryBaseDelayMs, this.options.retryMaxDelayMs, attempts);
|
|
223
|
+
await executeDatabase("release outbox claim", () => this.db.update(messageOutbox).set({
|
|
224
|
+
availableAt: deadline(now.getTime() + delay),
|
|
225
|
+
claimToken: null,
|
|
226
|
+
claimExpiresAt: null
|
|
227
|
+
}).where(and(inArray(messageOutbox.id, outboxIds(rows)), eq(messageOutbox.claimToken, claim.token))));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Retain the full payload and record a closed reason with a readable error message. */
|
|
231
|
+
async deadLetter(claim, messages, reason, errorMessage) {
|
|
232
|
+
if (messages.length === 0) return;
|
|
233
|
+
await executeDatabase("dead-letter outbox messages", () => this.db.update(messageOutbox).set({
|
|
234
|
+
failedAt: this.now(),
|
|
235
|
+
lastError: `${reason}: ${errorMessage}`,
|
|
236
|
+
claimToken: null,
|
|
237
|
+
claimExpiresAt: null
|
|
238
|
+
}).where(and(inArray(messageOutbox.id, outboxIds(messages)), eq(messageOutbox.claimToken, claim.token))));
|
|
239
|
+
}
|
|
240
|
+
/** Delete at most batchSize published rows, strictly before the cutoff. */
|
|
241
|
+
async deletePublishedBefore(cutoff, batchSize) {
|
|
242
|
+
return (await executeDatabase("delete published outbox messages", () => this.db.delete(messageOutbox).where(inArray(messageOutbox.id, this.db.select({ id: messageOutbox.id }).from(messageOutbox).where(lt(messageOutbox.publishedAt, cutoff)).orderBy(asc(messageOutbox.publishedAt), asc(messageOutbox.id)).limit(batchSize))).returning({ id: messageOutbox.id }))).length;
|
|
243
|
+
}
|
|
244
|
+
/** Delete at most batchSize failed rows; pending and published rows remain untouched. */
|
|
245
|
+
async deleteFailedBefore(cutoff, batchSize) {
|
|
246
|
+
return (await executeDatabase("delete failed outbox messages", () => this.db.delete(messageOutbox).where(inArray(messageOutbox.id, this.db.select({ id: messageOutbox.id }).from(messageOutbox).where(and(isNull(messageOutbox.publishedAt), lt(messageOutbox.failedAt, cutoff))).orderBy(asc(messageOutbox.failedAt), asc(messageOutbox.id)).limit(batchSize))).returning({ id: messageOutbox.id }))).length;
|
|
247
|
+
}
|
|
248
|
+
/** Delete at most batchSize receipts, strictly before the cutoff. */
|
|
249
|
+
async deleteReceiptsBefore(cutoff, batchSize) {
|
|
250
|
+
return (await executeDatabase("delete message receipts", () => this.db.delete(processedMessages).where(inArray(processedMessages.messageId, this.db.select({ messageId: processedMessages.messageId }).from(processedMessages).where(lt(processedMessages.processedAt, cutoff)).orderBy(asc(processedMessages.processedAt), asc(processedMessages.messageId)).limit(batchSize))).returning({ messageId: processedMessages.messageId }))).length;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
function outboxIds(messages) {
|
|
254
|
+
return sql`(select value from json_each(${JSON.stringify(messages.map((row) => row.payload.id))}))`;
|
|
255
|
+
}
|
|
256
|
+
function deadline(ms) {
|
|
257
|
+
return /* @__PURE__ */ new Date(Math.ceil(ms / 1e3) * 1e3);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
//#endregion
|
|
261
|
+
export { BaseD1Repository, D1BatchTransaction, D1MessageStore, D1OutboxRepository, DatabaseError, classifyD1Error, eventCollector, executeDatabase, messageOutbox, processedMessages };
|
|
169
262
|
//# sourceMappingURL=cloudflare.js.map
|
package/dist/cloudflare.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.js","names":[],"sources":["../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type D1ConstraintKind, type FailureClassification } from '@/core'\n\nconst KIND_BY_CODE: ReadonlyMap<string, D1ConstraintKind> = new Map([\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique'],\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key'],\n ['SQLITE_CONSTRAINT_CHECK', 'check'],\n])\n\nconst KIND_BY_PROSE: ReadonlyArray<readonly [string, D1ConstraintKind]> = [\n ['UNIQUE constraint failed', 'unique'],\n ['NOT NULL constraint failed', 'not_null'],\n ['FOREIGN KEY constraint failed', 'foreign_key'],\n ['CHECK constraint failed', 'check'],\n]\n\n/**\n * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.\n * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.\n */\nexport function classifyD1Error(cause: unknown, operation = 'D1 query'): DatabaseError | undefined {\n if (cause instanceof AppError || (cause instanceof Error && cause.name === 'AbortError'))\n return undefined\n const message = messagesOf(cause)\n const kind = kindOf(message)\n const classification = kind === undefined ? classificationOf(message) : 'terminal'\n const code = message.match(/\\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\\b/)?.[0]\n if (code === undefined && classification === 'unknown' && !(cause instanceof DrizzleQueryError))\n return undefined\n // A composite unique constraint must not be mistaken for the processed-message primary key.\n const location = message.match(/constraint failed: (\\w+)\\.(\\w+)(?=\\s*(?::\\s*SQLITE_|$|\\n))/)\n const check = kind === 'check' ? message.match(/CHECK constraint failed: ([A-Za-z_]\\w*)/) : null\n return new DatabaseError(\n operation,\n classification,\n {\n database: 'd1',\n code,\n kind,\n table: location?.[1],\n column: location?.[2],\n constraintName: check?.[1],\n },\n { cause },\n )\n}\n\nfunction kindOf(message: string): D1ConstraintKind | undefined {\n const code = message.match(/SQLITE_CONSTRAINT_\\w+/)?.[0]\n if (code !== undefined) return KIND_BY_CODE.get(code)\n return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1]\n}\n\n// Drizzle query messages contain SQL and values, which are not evidence about the database failure.\nfunction messagesOf(cause: unknown): string {\n const messages: string[] = []\n const seen = new Set<unknown>()\n let current = cause\n while (current instanceof Error && !seen.has(current)) {\n seen.add(current)\n if (!(current instanceof DrizzleQueryError)) messages.push(current.message)\n current = current.cause\n }\n // oxlint-disable-next-line anti-slop/no-runtime-typeof -- D1 can expose a raw string cause; this boundary accepts only that representation.\n if (typeof current === 'string') messages.push(current)\n return messages.join('\\n')\n}\n\n// Cloudflare documents these conditions separately from transient storage resets.\nconst TERMINAL_FAILURE =\n /\\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\\b|\\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i\nconst TRANSIENT_FAILURE =\n /Network connection lost\\.|Replica disconnected from primary\\.|D1 DB reset because its code was updated\\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\\.|Cannot resolve D1 DB due to transient issue on remote node\\.|Can't read from request stream because client disconnected\\./i\n\nfunction classificationOf(message: string): FailureClassification {\n if (TERMINAL_FAILURE.test(message)) return 'terminal'\n if (TRANSIENT_FAILURE.test(message)) return 'transient'\n return 'unknown'\n}\n","import { classifyD1Error } from './classify-d1-error.ts'\n\n/** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyD1Error(cause, operation) ?? cause\n }\n}\n","import type { BatchItem } from 'drizzle-orm/batch'\n\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector, stageD1Statement } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/**\n * Shared plumbing for a repository that writes inside a D1 unit of work.\n * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.\n */\nexport abstract class BaseD1Repository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: D1Db<Schema>) {}\n\n /** Add a statement to the current D1 batch without executing it. */\n protected stage(statement: BatchItem<'sqlite'>): void {\n stageD1Statement(statement)\n }\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = sqliteTable(\n 'processed_messages',\n {\n messageId: text('message_id').primaryKey(),\n processedAt: integer('processed_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n },\n (table) => [index('idx_processed_messages_processed_at').on(table.processedAt)],\n)\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = sqliteTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: text('payload', { mode: 'json' }).$type<DomainEvent>().notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n availableAt: integer('available_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: integer('claim_expires_at', { mode: 'timestamp' }),\n publishedAt: integer('published_at', { mode: 'timestamp' }),\n failedAt: integer('failed_at', { mode: 'timestamp' }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(sql`${table.publishedAt} IS NULL`),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { eq } from 'drizzle-orm'\n\nimport { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { BaseD1Repository } from './base-d1-repository.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */\nexport class D1MessageStore<Schema extends DrizzleSchema>\n extends BaseD1Repository<Schema>\n implements IMessageStore\n{\n async claim(messageId: MessageId): Promise<void> {\n const processed = await this.read('claim message', () =>\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(eq(processedMessages.messageId, messageId))\n .get(),\n )\n if (processed !== undefined) throw new MessageAlreadyProcessedError(messageId)\n\n // Concurrent deliveries can pass the read; the primary key enforces the claim at commit.\n this.stage(this.db.insert(processedMessages).values({ messageId }))\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n this.stage(\n this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event }))),\n )\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { collectD1Statements, runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db, D1Tx } from './d1-database-types.ts'\n\n/**\n * The D1 transaction boundary: every write is staged during `work` and committed as one\n * `db.batch()`. A throw inside `work` means the batch never runs.\n */\nexport class D1BatchTransaction<Schema extends DrizzleSchema> implements ITransaction<\n D1Tx<Schema>\n> {\n constructor(private readonly db: D1Db<Schema>) {}\n\n async transaction<T>(work: (tx: D1Tx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('d1', async () => {\n const value = await work({ db: this.db })\n const [first, ...rest] = collectD1Statements()\n if (first !== undefined) {\n await executeDatabase('commit unit of work', () => this.db.batch([first, ...rest]))\n }\n return value\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cloudflare.js","names":[],"sources":["../src/infrastructure/cloudflare/classify-d1-error.ts","../src/infrastructure/cloudflare/d1-database-execution.ts","../src/infrastructure/cloudflare/base-d1-repository.ts","../src/infrastructure/cloudflare/d1-schema.ts","../src/infrastructure/cloudflare/d1-message-store.ts","../src/infrastructure/cloudflare/d1-batch-transaction.ts","../src/infrastructure/cloudflare/d1-outbox-repository.ts"],"sourcesContent":["import { DrizzleQueryError } from 'drizzle-orm/errors'\n\nimport { AppError, DatabaseError, type D1ConstraintKind, type FailureClassification } from '@/core'\n\nconst KIND_BY_CODE: ReadonlyMap<string, D1ConstraintKind> = new Map([\n ['SQLITE_CONSTRAINT_PRIMARYKEY', 'unique'],\n ['SQLITE_CONSTRAINT_UNIQUE', 'unique'],\n ['SQLITE_CONSTRAINT_NOTNULL', 'not_null'],\n ['SQLITE_CONSTRAINT_FOREIGNKEY', 'foreign_key'],\n ['SQLITE_CONSTRAINT_CHECK', 'check'],\n])\n\nconst KIND_BY_PROSE: ReadonlyArray<readonly [string, D1ConstraintKind]> = [\n ['UNIQUE constraint failed', 'unique'],\n ['NOT NULL constraint failed', 'not_null'],\n ['FOREIGN KEY constraint failed', 'foreign_key'],\n ['CHECK constraint failed', 'check'],\n]\n\n/**\n * Recognize D1 failures and preserve the original cause. Unrecognized callback exceptions remain unchanged.\n * D1 exposes error codes and constraint locations in text, so parsing stays at this boundary.\n */\nexport function classifyD1Error(cause: unknown, operation = 'D1 query'): DatabaseError | undefined {\n if (cause instanceof AppError || (cause instanceof Error && cause.name === 'AbortError'))\n return undefined\n const message = messagesOf(cause)\n const kind = kindOf(message)\n const classification = kind === undefined ? classificationOf(message) : 'terminal'\n const code = message.match(/\\b(?:SQLITE_[A-Z_]+|D1_[A-Z_]+)\\b/)?.[0]\n if (code === undefined && classification === 'unknown' && !(cause instanceof DrizzleQueryError))\n return undefined\n // A composite unique constraint must not be mistaken for the processed-message primary key.\n const location = message.match(/constraint failed: (\\w+)\\.(\\w+)(?=\\s*(?::\\s*SQLITE_|$|\\n))/)\n const check = kind === 'check' ? message.match(/CHECK constraint failed: ([A-Za-z_]\\w*)/) : null\n return new DatabaseError(\n operation,\n classification,\n {\n database: 'd1',\n code,\n kind,\n table: location?.[1],\n column: location?.[2],\n constraintName: check?.[1],\n },\n { cause },\n )\n}\n\nfunction kindOf(message: string): D1ConstraintKind | undefined {\n const code = message.match(/SQLITE_CONSTRAINT_\\w+/)?.[0]\n if (code !== undefined) return KIND_BY_CODE.get(code)\n return KIND_BY_PROSE.find(([prose]) => message.includes(prose))?.[1]\n}\n\n// Drizzle query messages contain SQL and values, which are not evidence about the database failure.\nfunction messagesOf(cause: unknown): string {\n const messages: string[] = []\n const seen = new Set<unknown>()\n let current = cause\n while (current instanceof Error && !seen.has(current)) {\n seen.add(current)\n if (!(current instanceof DrizzleQueryError)) messages.push(current.message)\n current = current.cause\n }\n // oxlint-disable-next-line anti-slop/no-runtime-typeof -- D1 can expose a raw string cause; this boundary accepts only that representation.\n if (typeof current === 'string') messages.push(current)\n return messages.join('\\n')\n}\n\n// Cloudflare documents these conditions separately from transient storage resets.\nconst TERMINAL_FAILURE =\n /\\bSQLITE_(?:ERROR|MISMATCH|TOOBIG|FULL|AUTH|PERM|READONLY)\\b|\\bD1_(?:TYPE_ERROR|COLUMN_NOTFOUND)\\b|no such (?:table|column|function)|syntax error|datatype mismatch|exceeded (?:timeout|its memory limit|its CPU time limit|maximum DB size)|maximum account storage limit|free tier daily row (?:read|write) limit/i\nconst TRANSIENT_FAILURE =\n /Network connection lost\\.|Replica disconnected from primary\\.|D1 DB reset because its code was updated\\.|Internal error (?:while starting up|in) D1 DB storage caused object to be reset\\.|Cannot resolve D1 DB due to transient issue on remote node\\.|Can't read from request stream because client disconnected\\./i\n\nfunction classificationOf(message: string): FailureClassification {\n if (TERMINAL_FAILURE.test(message)) return 'terminal'\n if (TRANSIENT_FAILURE.test(message)) return 'transient'\n return 'unknown'\n}\n","import { classifyD1Error } from './classify-d1-error.ts'\n\n/** Execute once without retries; convert recognized D1 failures and preserve other exceptions unchanged. */\nexport async function executeDatabase<T>(\n operation: string,\n work: () => PromiseLike<T>,\n): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n throw classifyD1Error(cause, operation) ?? cause\n }\n}\n","import type { BatchItem } from 'drizzle-orm/batch'\n\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { eventCollector, stageD1Statement } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-database-types.ts'\n\n/** A domain aggregate that accumulates events for the unit of work to drain. */\nexport interface AggregateEvents {\n collectEvents(): ReadonlyArray<DomainEvent>\n clearEvents(): void\n}\n\n/**\n * Shared plumbing for a repository that writes inside a D1 unit of work.\n * Writes are staged, never run; the boundary commits them as one batch. Reads run on `db` now.\n */\nexport abstract class BaseD1Repository<Schema extends DrizzleSchema> {\n constructor(protected readonly db: D1Db<Schema>) {}\n\n /** Add a statement to the current D1 batch without executing it. */\n protected stage(statement: BatchItem<'sqlite'>): void {\n stageD1Statement(statement)\n }\n\n /** Execute one read with database error conversion and no automatic retries. */\n protected read<T>(operation: string, query: () => PromiseLike<T>): Promise<T> {\n return executeDatabase(operation, query)\n }\n\n /** Drain an aggregate's pending events into the active boundary. */\n protected dispatchEvents(aggregate: AggregateEvents): void {\n const events = aggregate.collectEvents()\n if (events.length === 0) return\n for (const event of events) eventCollector.emit(event)\n aggregate.clearEvents()\n }\n}\n","import { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\nimport type { DomainEvent } from '@/core'\n\n/** One row per committed message. The primary key is the idempotency claim. */\nexport const processedMessages = sqliteTable(\n 'processed_messages',\n {\n messageId: text('message_id').primaryKey(),\n processedAt: integer('processed_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n },\n (table) => [index('idx_processed_messages_processed_at').on(table.processedAt)],\n)\n\n/** Events committed with their state change, waiting for the relay to deliver them. */\nexport const messageOutbox = sqliteTable(\n 'message_outbox',\n {\n id: text('id').primaryKey(),\n payload: text('payload', { mode: 'json' }).$type<DomainEvent>().notNull(),\n createdAt: integer('created_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n availableAt: integer('available_at', { mode: 'timestamp' })\n .notNull()\n .$defaultFn(() => new Date()),\n attempts: integer('attempts').notNull().default(0),\n claimToken: text('claim_token'),\n claimExpiresAt: integer('claim_expires_at', { mode: 'timestamp' }),\n publishedAt: integer('published_at', { mode: 'timestamp' }),\n failedAt: integer('failed_at', { mode: 'timestamp' }),\n lastError: text('last_error'),\n },\n (table) => [\n index('idx_message_outbox_due')\n .on(table.availableAt, table.createdAt)\n .where(sql`${table.publishedAt} IS NULL`),\n index('idx_message_outbox_claim_token').on(table.claimToken),\n ],\n)\n","import { eq } from 'drizzle-orm'\n\nimport { MessageAlreadyProcessedError, type IMessageStore, type MessageId } from '@/application'\nimport type { DomainEvent } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { BaseD1Repository } from './base-d1-repository.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/** Processed messages and outbox events share one D1 batch; the primary key prevents concurrent duplicate commits. */\nexport class D1MessageStore<Schema extends DrizzleSchema>\n extends BaseD1Repository<Schema>\n implements IMessageStore\n{\n async claim(messageId: MessageId): Promise<void> {\n const processed = await this.read('claim message', () =>\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(eq(processedMessages.messageId, messageId))\n .get(),\n )\n if (processed !== undefined) throw new MessageAlreadyProcessedError(messageId)\n\n // Concurrent deliveries can pass the read; the primary key enforces the claim at commit.\n this.stage(this.db.insert(processedMessages).values({ messageId }))\n }\n\n async persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void> {\n if (events.length === 0) return\n this.stage(\n this.db\n .insert(messageOutbox)\n .values(events.map((event) => ({ id: event.id, payload: event }))),\n )\n }\n}\n","import type { ITransaction } from '@/application'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport { collectD1Statements, runWithUnitOfWorkContext } from '../unit-of-work-context.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db, D1Tx } from './d1-database-types.ts'\n\n/**\n * The D1 transaction boundary: every write is staged during `work` and committed as one\n * `db.batch()`. A throw inside `work` means the batch never runs.\n */\nexport class D1BatchTransaction<Schema extends DrizzleSchema> implements ITransaction<\n D1Tx<Schema>\n> {\n constructor(private readonly db: D1Db<Schema>) {}\n\n async transaction<T>(work: (tx: D1Tx<Schema>) => Promise<T>): Promise<T> {\n return runWithUnitOfWorkContext('d1', async () => {\n const value = await work({ db: this.db })\n const [first, ...rest] = collectD1Statements()\n if (first !== undefined) {\n await executeDatabase('commit unit of work', () => this.db.batch([first, ...rest]))\n }\n return value\n })\n }\n}\n","import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from 'drizzle-orm'\n\nimport type { DeadLetterReason, IOutboxRepository, OutboxClaim, OutboxMessage } from '@/application'\nimport { exponentialDelayCapMs } from '@/core'\n\nimport type { DrizzleSchema } from '../drizzle-schema.ts'\nimport {\n OUTBOX_REPOSITORY_DEFAULTS,\n type OutboxRepositoryOptions,\n} from '../outbox-repository-options.ts'\nimport { executeDatabase } from './d1-database-execution.ts'\nimport type { D1Db } from './d1-database-types.ts'\nimport { messageOutbox, processedMessages } from './d1-schema.ts'\n\n/**\n * Typist's atomic D1 claims on a plain connection, outside a unit of work.\n * Every write runs once; lease expiry and the scheduled sweep recover interrupted operations.\n */\nexport class D1OutboxRepository<Schema extends DrizzleSchema> implements IOutboxRepository {\n private readonly options: Required<OutboxRepositoryOptions>\n\n constructor(\n private readonly db: D1Db<Schema>,\n options: OutboxRepositoryOptions = {},\n private readonly now: () => Date = () => new Date(),\n ) {\n this.options = { ...OUTBOX_REPOSITORY_DEFAULTS, ...options }\n }\n\n /** Lease due rows in one statement; increment attempts even when reclaiming an expired lease. */\n async claimPending(batchSize: number): Promise<OutboxClaim> {\n const token = crypto.randomUUID()\n const now = this.now()\n const rows = await executeDatabase('claim pending outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n claimToken: token,\n claimExpiresAt: deadline(now.getTime() + this.options.leaseMs),\n attempts: sql`${messageOutbox.attempts} + 1`,\n })\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(\n and(\n isNull(messageOutbox.publishedAt),\n isNull(messageOutbox.failedAt),\n lte(messageOutbox.availableAt, now),\n or(isNull(messageOutbox.claimExpiresAt), lte(messageOutbox.claimExpiresAt, now)),\n ),\n )\n .orderBy(\n asc(messageOutbox.availableAt),\n asc(messageOutbox.createdAt),\n asc(messageOutbox.id),\n )\n .limit(batchSize),\n ),\n )\n .returning({\n payload: messageOutbox.payload,\n attempts: messageOutbox.attempts,\n availableAt: messageOutbox.availableAt,\n createdAt: messageOutbox.createdAt,\n }),\n )\n // UPDATE RETURNING does not preserve the selection order.\n rows.sort(\n (a, b) =>\n a.availableAt.getTime() - b.availableAt.getTime() ||\n a.createdAt.getTime() - b.createdAt.getTime() ||\n a.payload.id.localeCompare(b.payload.id),\n )\n return { token, messages: rows.map(({ payload, attempts }) => ({ payload, attempts })) }\n }\n\n /** Mark only supplied rows that still hold this token as published. */\n async markPublished(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('mark outbox messages published', () =>\n this.db\n .update(messageOutbox)\n .set({ publishedAt: this.now(), claimToken: null, claimExpiresAt: null })\n .where(\n and(\n inArray(messageOutbox.id, outboxIds(messages)),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Release supplied rows with per-row backoff; a stale token cannot change a newer claim. */\n async releaseClaim(claim: OutboxClaim, messages: ReadonlyArray<OutboxMessage>): Promise<void> {\n const now = this.now()\n for (const attempts of new Set(messages.map((row) => row.attempts))) {\n const rows = messages.filter((row) => row.attempts === attempts)\n const delay = exponentialDelayCapMs(\n this.options.retryBaseDelayMs,\n this.options.retryMaxDelayMs,\n attempts,\n )\n // oxlint-disable-next-line no-await-in-loop -- Stop releasing rows on the first database failure.\n await executeDatabase('release outbox claim', () =>\n this.db\n .update(messageOutbox)\n .set({\n availableAt: deadline(now.getTime() + delay),\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(\n and(\n inArray(messageOutbox.id, outboxIds(rows)),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n }\n\n /** Retain the full payload and record a closed reason with a readable error message. */\n async deadLetter(\n claim: OutboxClaim,\n messages: ReadonlyArray<OutboxMessage>,\n reason: DeadLetterReason,\n errorMessage: string,\n ): Promise<void> {\n if (messages.length === 0) return\n await executeDatabase('dead-letter outbox messages', () =>\n this.db\n .update(messageOutbox)\n .set({\n failedAt: this.now(),\n lastError: `${reason}: ${errorMessage}`,\n claimToken: null,\n claimExpiresAt: null,\n })\n .where(\n and(\n inArray(messageOutbox.id, outboxIds(messages)),\n eq(messageOutbox.claimToken, claim.token),\n ),\n ),\n )\n }\n\n /** Delete at most batchSize published rows, strictly before the cutoff. */\n async deletePublishedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete published outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(lt(messageOutbox.publishedAt, cutoff))\n .orderBy(asc(messageOutbox.publishedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize failed rows; pending and published rows remain untouched. */\n async deleteFailedBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete failed outbox messages', () =>\n this.db\n .delete(messageOutbox)\n .where(\n inArray(\n messageOutbox.id,\n this.db\n .select({ id: messageOutbox.id })\n .from(messageOutbox)\n .where(and(isNull(messageOutbox.publishedAt), lt(messageOutbox.failedAt, cutoff)))\n .orderBy(asc(messageOutbox.failedAt), asc(messageOutbox.id))\n .limit(batchSize),\n ),\n )\n .returning({ id: messageOutbox.id }),\n )\n return rows.length\n }\n\n /** Delete at most batchSize receipts, strictly before the cutoff. */\n async deleteReceiptsBefore(cutoff: Date, batchSize: number): Promise<number> {\n const rows = await executeDatabase('delete message receipts', () =>\n this.db\n .delete(processedMessages)\n .where(\n inArray(\n processedMessages.messageId,\n this.db\n .select({ messageId: processedMessages.messageId })\n .from(processedMessages)\n .where(lt(processedMessages.processedAt, cutoff))\n .orderBy(asc(processedMessages.processedAt), asc(processedMessages.messageId))\n .limit(batchSize),\n ),\n )\n .returning({ messageId: processedMessages.messageId }),\n )\n return rows.length\n }\n}\n\n// One JSON parameter keeps large claims below D1's 100-parameter limit.\nfunction outboxIds(messages: ReadonlyArray<OutboxMessage>) {\n return sql`(select value from json_each(${JSON.stringify(messages.map((row) => row.payload.id))}))`\n}\n\n// D1 timestamps repository seconds; round future deadlines up so a lease or delay never ends early.\nfunction deadline(ms: number): Date {\n return new Date(Math.ceil(ms / 1_000) * 1_000)\n}\n"],"mappings":"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"}
|
|
@@ -681,6 +681,7 @@ function exponentialEqualJitterMs(baseDelayMs, maxDelayMs, attempt) {
|
|
|
681
681
|
const halfCapMs = exponentialDelayCapMs(baseDelayMs, maxDelayMs, attempt) / 2;
|
|
682
682
|
return Math.floor(halfCapMs) + spreadJitterMs(halfCapMs);
|
|
683
683
|
}
|
|
684
|
+
/** Capped exponential delay for a durable retry recorded in storage. */
|
|
684
685
|
function exponentialDelayCapMs(baseDelayMs, maxDelayMs, attempt) {
|
|
685
686
|
if (!Number.isSafeInteger(attempt) || attempt < 1) return panic("attempt must be a positive integer");
|
|
686
687
|
return Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
|
|
@@ -903,5 +904,5 @@ function createLogger(module, config) {
|
|
|
903
904
|
}
|
|
904
905
|
|
|
905
906
|
//#endregion
|
|
906
|
-
export {
|
|
907
|
-
//# sourceMappingURL=core-
|
|
907
|
+
export { AppError as $, ProblemDetailsSchema as A, NotAuthorizedError as B, defineMessages as C, DatabaseError as D, unreachable as E, ResourceNotFoundError as F, BounceTypeSchema as G, RateLimitError as H, UpgradeRequiredError as I, shouldUpdateBounce as J, isEmailDeliverable as K, ValidationError as L, ValidationProblemErrorSchema as M, MalformedRequestError as N, defineErrors as O, RequestTimeoutError as P, EmailAddressSchema as Q, InternalServerError as R, Entity as S, notImplemented as T, AuthenticationError as U, StaleSessionError as V, RecordEmailEventPayloadSchema as W, InvalidEmailIdempotencyKeyError as X, EmailServiceError as Y, emailIdempotencyKey as Z, isCloudflareRetryableError as _, createLogger as a, isTransientTransportError as b, exponentialDelayCapMs as c, classifyBoundaryError as d, classifyFailure as f, isCloudflareOverloadedError as g, isCloudflareObjectResetError as h, consoleSink as i, ValidationProblemDetailsSchema as j, sharedErrors as k, exponentialEqualJitterMs as l, CLOUDFLARE_TRANSIENT_ERROR_PATTERNS as m, Logger as n, setLogSink as o, retryAfterSecondsOf as p, parseBounceType as q, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, spreadJitterMs as u, isTransientCloudflareRuntimeError as v, defineId as w, shouldRetryFailure as x, isAbortError as y, ServiceUnavailableError as z };
|
|
908
|
+
//# sourceMappingURL=core-BiqdOMwU.js.map
|