@amqp-contract/worker 0.11.0 → 0.12.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/index.cjs +104 -232
- package/dist/index.d.cts +17 -43
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +17 -43
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +104 -231
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +76 -226
- package/package.json +4 -4
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["error"],"sources":["../src/errors.ts","../src/decompression.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":["/**\n * Base error class for worker errors\n */\nabstract class WorkerError extends Error {\n protected constructor(message: string) {\n super(message);\n this.name = \"WorkerError\";\n // Node.js specific stack trace capture\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error for technical/runtime failures in worker operations\n * This includes validation failures, parsing failures, and processing failures\n */\nexport class TechnicalError extends WorkerError {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"TechnicalError\";\n }\n}\n\n/**\n * Error thrown when message validation fails\n */\nexport class MessageValidationError extends WorkerError {\n constructor(\n public readonly consumerName: string,\n public readonly issues: unknown,\n ) {\n super(`Message validation failed for consumer \"${consumerName}\"`);\n this.name = \"MessageValidationError\";\n }\n}\n\n/**\n * Retryable errors - transient failures that may succeed on retry\n * Examples: network timeouts, rate limiting, temporary service unavailability\n *\n * Use this error type when the operation might succeed if retried.\n * The worker will apply exponential backoff and retry the message.\n */\nexport class RetryableError extends WorkerError {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"RetryableError\";\n }\n}\n\n/**\n * Non-retryable errors - permanent failures that should not be retried\n * Examples: invalid data, business rule violations, permanent external failures\n *\n * Use this error type when retrying would not help - the message will be\n * immediately sent to the dead letter queue (DLQ) if configured.\n */\nexport class NonRetryableError extends WorkerError {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"NonRetryableError\";\n }\n}\n\n/**\n * Union type representing all handler errors.\n * Use this type when defining handlers that explicitly signal error outcomes.\n */\nexport type HandlerError = RetryableError | NonRetryableError;\n","import { gunzip, inflate } from \"node:zlib\";\nimport { promisify } from \"node:util\";\n\nconst gunzipAsync = promisify(gunzip);\nconst inflateAsync = promisify(inflate);\n\n/**\n * Decompress a buffer based on the content-encoding header.\n *\n * @param buffer - The buffer to decompress\n * @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')\n * @returns A promise that resolves to the decompressed buffer\n * @throws Error if decompression fails or if the encoding is unsupported\n *\n * @internal\n */\nexport async function decompressBuffer(\n buffer: Buffer,\n contentEncoding: string | undefined,\n): Promise<Buffer> {\n if (!contentEncoding) {\n return buffer; // No compression\n }\n\n switch (contentEncoding.toLowerCase()) {\n case \"gzip\":\n return gunzipAsync(buffer);\n case \"deflate\":\n return inflateAsync(buffer);\n default:\n throw new Error(`Unsupported content-encoding: ${contentEncoding}`);\n }\n}\n","import {\n AmqpClient,\n type Logger,\n type TelemetryProvider,\n defaultTelemetryProvider,\n endSpanError,\n endSpanSuccess,\n recordConsumeMetric,\n startConsumeSpan,\n} from \"@amqp-contract/core\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport type { Channel, ConsumeMessage, Message } from \"amqplib\";\nimport type {\n ConsumerDefinition,\n ContractDefinition,\n InferConsumerNames,\n} from \"@amqp-contract/contract\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport { MessageValidationError, NonRetryableError, TechnicalError } from \"./errors.js\";\nimport type {\n WorkerInferConsumedMessage,\n WorkerInferSafeConsumerHandler,\n WorkerInferSafeConsumerHandlers,\n} from \"./types.js\";\nimport type { HandlerError } from \"./errors.js\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { decompressBuffer } from \"./decompression.js\";\n\n/**\n * Internal type for consumer options extracted from handler tuples.\n * Not exported - options are specified inline in the handler tuple types.\n *\n * Note: Retry configuration is now defined at the queue level in the contract,\n * not at the handler level. See `QueueDefinition.retry` for configuration options.\n */\ntype ConsumerOptions = {\n /** Number of messages to prefetch */\n prefetch?: number;\n};\n\n/**\n * Internal retry configuration with all values resolved.\n */\ntype ResolvedRetryConfig = {\n mode: \"ttl-backoff\" | \"quorum-native\";\n maxRetries: number;\n initialDelayMs: number;\n maxDelayMs: number;\n backoffMultiplier: number;\n jitter: boolean;\n};\n\n/**\n * Type guard to check if a handler entry is a tuple format [handler, options].\n */\nfunction isHandlerTuple(entry: unknown): entry is [unknown, ConsumerOptions] {\n return Array.isArray(entry) && entry.length === 2;\n}\n\n/**\n * Type guard to check if a value is a Standard Schema v1 compliant schema.\n */\nfunction isStandardSchema(value: unknown): value is StandardSchemaV1 {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n if (!(\"~standard\" in value)) {\n return false;\n }\n const standard = (value as { \"~standard\": unknown })[\"~standard\"];\n if (typeof standard !== \"object\" || standard === null) {\n return false;\n }\n if (!(\"validate\" in standard)) {\n return false;\n }\n return typeof (standard as { validate: unknown }).validate === \"function\";\n}\n\n/**\n * Options for creating a type-safe AMQP worker.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * const options: CreateWorkerOptions<typeof contract> = {\n * contract: myContract,\n * handlers: {\n * // Simple handler\n * processOrder: ({ payload }) => {\n * console.log('Processing order:', payload.orderId);\n * return Future.value(Result.Ok(undefined));\n * },\n * // Handler with prefetch configuration\n * processPayment: [\n * ({ payload }) => {\n * console.log('Processing payment:', payload.paymentId);\n * return Future.value(Result.Ok(undefined));\n * },\n * { prefetch: 10 }\n * ]\n * },\n * urls: ['amqp://localhost'],\n * connectionOptions: {\n * heartbeatIntervalInSeconds: 30\n * },\n * logger: myLogger\n * };\n * ```\n *\n * Note: Retry configuration is defined at the queue level in the contract,\n * not at the handler level. See `QueueDefinition.retry` for configuration options.\n */\nexport type CreateWorkerOptions<TContract extends ContractDefinition> = {\n /** The AMQP contract definition specifying consumers and their message schemas */\n contract: TContract;\n /**\n * Handlers for each consumer defined in the contract.\n * Handlers must return `Future<Result<void, HandlerError>>` for explicit error handling.\n * Use defineHandler() to create handlers.\n */\n handlers: WorkerInferSafeConsumerHandlers<TContract>;\n /** AMQP broker URL(s). Multiple URLs provide failover support */\n urls: ConnectionUrl[];\n /** Optional connection configuration (heartbeat, reconnect settings, etc.) */\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n /** Optional logger for logging message consumption and errors */\n logger?: Logger | undefined;\n /**\n * Optional telemetry provider for tracing and metrics.\n * If not provided, uses the default provider which attempts to load OpenTelemetry.\n * OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed.\n */\n telemetry?: TelemetryProvider | undefined;\n};\n\n/**\n * Type-safe AMQP worker for consuming messages from RabbitMQ.\n *\n * This class provides automatic message validation, connection management,\n * and error handling for consuming messages based on a contract definition.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * import { TypedAmqpWorker } from '@amqp-contract/worker';\n * import { z } from 'zod';\n *\n * const contract = defineContract({\n * queues: {\n * orderProcessing: defineQueue('order-processing', { durable: true })\n * },\n * consumers: {\n * processOrder: defineConsumer('order-processing', z.object({\n * orderId: z.string(),\n * amount: z.number()\n * }))\n * }\n * });\n *\n * const worker = await TypedAmqpWorker.create({\n * contract,\n * handlers: {\n * processOrder: async (message) => {\n * console.log('Processing order', message.orderId);\n * // Process the order...\n * }\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n *\n * // Close when done\n * await worker.close().resultToPromise();\n * ```\n */\nexport class TypedAmqpWorker<TContract extends ContractDefinition> {\n /**\n * Internal handler storage - handlers returning `Future<Result>`.\n */\n private readonly actualHandlers: Partial<\n Record<\n InferConsumerNames<TContract>,\n WorkerInferSafeConsumerHandler<TContract, InferConsumerNames<TContract>>\n >\n >;\n private readonly consumerOptions: Partial<Record<InferConsumerNames<TContract>, ConsumerOptions>>;\n private readonly consumerTags: Set<string> = new Set();\n private readonly telemetry: TelemetryProvider;\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n handlers: WorkerInferSafeConsumerHandlers<TContract>,\n private readonly logger?: Logger,\n telemetry?: TelemetryProvider,\n ) {\n this.telemetry = telemetry ?? defaultTelemetryProvider;\n\n // Extract handlers and options from the handlers object\n this.actualHandlers = {};\n this.consumerOptions = {};\n\n // Cast handlers to a generic record for iteration\n const handlersRecord = handlers as Record<string, unknown>;\n\n for (const consumerName of Object.keys(handlersRecord)) {\n const handlerEntry = handlersRecord[consumerName];\n const typedConsumerName = consumerName as InferConsumerNames<TContract>;\n\n if (isHandlerTuple(handlerEntry)) {\n // Tuple format: [handler, options]\n const [handler, options] = handlerEntry;\n this.actualHandlers[typedConsumerName] = handler as WorkerInferSafeConsumerHandler<\n TContract,\n InferConsumerNames<TContract>\n >;\n this.consumerOptions[typedConsumerName] = options;\n } else {\n // Direct function format\n this.actualHandlers[typedConsumerName] = handlerEntry as WorkerInferSafeConsumerHandler<\n TContract,\n InferConsumerNames<TContract>\n >;\n }\n }\n }\n\n /**\n * Create a type-safe AMQP worker from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The worker will set up\n * consumers for all contract-defined handlers asynchronously in the background\n * once the underlying connection and channels are ready.\n *\n * Connections are automatically shared across clients and workers with the same\n * URLs and connection options, following RabbitMQ best practices.\n *\n * @param options - Configuration options for the worker\n * @returns A Future that resolves to a Result containing the worker or an error\n *\n * @example\n * ```typescript\n * const worker = await TypedAmqpWorker.create({\n * contract: myContract,\n * handlers: {\n * processOrder: async ({ payload }) => console.log('Order:', payload.orderId)\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n * ```\n */\n static create<TContract extends ContractDefinition>({\n contract,\n handlers,\n urls,\n connectionOptions,\n logger,\n telemetry,\n }: CreateWorkerOptions<TContract>): Future<Result<TypedAmqpWorker<TContract>, TechnicalError>> {\n const worker = new TypedAmqpWorker(\n contract,\n new AmqpClient(contract, {\n urls,\n connectionOptions,\n }),\n handlers,\n logger,\n telemetry,\n );\n\n // Note: Wait queues are now created by the core package in setupAmqpTopology\n // when the queue's retry mode is \"ttl-backoff\"\n return worker\n .waitForConnectionReady()\n .flatMapOk(() => worker.validateRetryConfiguration())\n .flatMapOk(() => worker.consumeAll())\n .mapOk(() => worker);\n }\n\n /**\n * Close the AMQP channel and connection.\n *\n * This gracefully closes the connection to the AMQP broker,\n * stopping all message consumption and cleaning up resources.\n *\n * @returns A Future that resolves to a Result indicating success or failure\n *\n * @example\n * ```typescript\n * const closeResult = await worker.close().resultToPromise();\n * if (closeResult.isOk()) {\n * console.log('Worker closed successfully');\n * }\n * ```\n */\n close(): Future<Result<void, TechnicalError>> {\n return Future.all(\n Array.from(this.consumerTags).map((consumerTag) =>\n Future.fromPromise(this.amqpClient.channel.cancel(consumerTag)).mapErrorToResult(\n (error) => {\n this.logger?.warn(\"Failed to cancel consumer during close\", { consumerTag, error });\n return Result.Ok(undefined);\n },\n ),\n ),\n )\n .map(Result.all)\n .tapOk(() => {\n // Clear consumer tags after successful cancellation\n this.consumerTags.clear();\n })\n .flatMapOk(() => Future.fromPromise(this.amqpClient.close()))\n .mapError((error) => new TechnicalError(\"Failed to close AMQP connection\", error))\n .mapOk(() => undefined);\n }\n\n /**\n * Validate retry configuration for all consumers.\n *\n * For quorum-native mode, validates that the queue is properly configured.\n * For TTL-backoff mode, wait queues are created by setupAmqpTopology in the core package.\n */\n private validateRetryConfiguration(): Future<Result<void, TechnicalError>> {\n if (!this.contract.consumers) {\n return Future.value(Result.Ok(undefined));\n }\n\n for (const consumerName of Object.keys(\n this.contract.consumers,\n ) as InferConsumerNames<TContract>[]) {\n const consumer = this.contract.consumers[consumerName as string];\n if (!consumer) continue;\n\n const queue = consumer.queue;\n const retryMode = queue.retry?.mode ?? \"ttl-backoff\";\n\n // For quorum-native mode, validate queue configuration\n if (retryMode === \"quorum-native\") {\n const validationError = this.validateQuorumNativeConfigForConsumer(\n String(consumerName),\n consumer,\n );\n if (validationError) {\n return Future.value(Result.Error(validationError));\n }\n this.logger?.info(\"Using quorum-native retry mode\", {\n consumerName: String(consumerName),\n queueName: queue.name,\n });\n } else {\n // TTL-backoff mode\n if (queue.deadLetter) {\n this.logger?.info(\"Using TTL-backoff retry mode\", {\n consumerName: String(consumerName),\n queueName: queue.name,\n });\n } else {\n this.logger?.warn(\n \"Queue has no deadLetter configured - retries will use nack with requeue\",\n {\n consumerName: String(consumerName),\n queueName: queue.name,\n },\n );\n }\n }\n }\n\n return Future.value(Result.Ok(undefined));\n }\n\n /**\n * Get the resolved retry configuration for a consumer's queue.\n * Reads retry config from the queue definition in the contract.\n */\n private getRetryConfigForConsumer(consumer: ConsumerDefinition): ResolvedRetryConfig {\n const queue = consumer.queue;\n const retryOptions = queue.retry;\n\n // For quorum-native mode, TTL-backoff options are not used\n // but we still provide defaults for internal consistency\n if (retryOptions?.mode === \"quorum-native\") {\n return {\n mode: \"quorum-native\",\n maxRetries: 0, // Not used in quorum-native mode\n initialDelayMs: 0, // Not used in quorum-native mode\n maxDelayMs: 0, // Not used in quorum-native mode\n backoffMultiplier: 0, // Not used in quorum-native mode\n jitter: false, // Not used in quorum-native mode\n };\n }\n\n // TTL-backoff mode (default): extract options with defaults\n if (retryOptions?.mode === \"ttl-backoff\") {\n return {\n mode: \"ttl-backoff\",\n maxRetries: retryOptions.maxRetries ?? 3,\n initialDelayMs: retryOptions.initialDelayMs ?? 1000,\n maxDelayMs: retryOptions.maxDelayMs ?? 30000,\n backoffMultiplier: retryOptions.backoffMultiplier ?? 2,\n jitter: retryOptions.jitter ?? true,\n };\n }\n\n // No retry configured - use default TTL-backoff\n return {\n mode: \"ttl-backoff\",\n maxRetries: 3,\n initialDelayMs: 1000,\n maxDelayMs: 30000,\n backoffMultiplier: 2,\n jitter: true,\n };\n }\n\n /**\n * Validate that quorum-native retry mode is properly configured for a specific consumer.\n *\n * Requirements for quorum-native mode:\n * - Consumer queue must be a quorum queue\n * - Consumer queue must have deliveryLimit configured\n * - Consumer queue should have DLX configured (warning if not)\n *\n * @returns TechnicalError if validation fails, null if valid\n */\n private validateQuorumNativeConfigForConsumer(\n consumerName: string,\n consumer: ConsumerDefinition,\n ): TechnicalError | null {\n const queue = consumer.queue;\n\n // Check if queue is a quorum queue\n if (queue.type !== \"quorum\") {\n return new TechnicalError(\n `Consumer \"${consumerName}\" uses queue \"${queue.name}\" with type \"${queue.type}\". ` +\n `Quorum-native retry mode requires quorum queues.`,\n );\n }\n\n // Check if deliveryLimit is configured (now queue is narrowed to QuorumQueueDefinition)\n if (queue.deliveryLimit === undefined) {\n return new TechnicalError(\n `Consumer \"${consumerName}\" uses queue \"${queue.name}\" without deliveryLimit configured. ` +\n `Quorum-native retry mode requires deliveryLimit to be set on the queue definition.`,\n );\n }\n\n // Check if DLX is configured (warning only)\n if (!queue.deadLetter) {\n this.logger?.warn(\n `Consumer \"${consumerName}\" uses queue \"${queue.name}\" without deadLetter configured. ` +\n `Messages exceeding deliveryLimit will be dropped instead of dead-lettered.`,\n );\n }\n\n return null;\n }\n\n /**\n * Start consuming messages for all consumers\n */\n private consumeAll(): Future<Result<void, TechnicalError>> {\n if (!this.contract.consumers) {\n return Future.value(Result.Error(new TechnicalError(\"No consumers defined in contract\")));\n }\n\n // Calculate the maximum prefetch value among all consumers\n // Since prefetch is per-channel in AMQP 0.9.1, we use the maximum value\n const consumerNames = Object.keys(this.contract.consumers) as InferConsumerNames<TContract>[];\n let maxPrefetch = 0;\n\n for (const consumerName of consumerNames) {\n const options = this.consumerOptions[consumerName];\n if (options?.prefetch !== undefined) {\n if (options.prefetch <= 0 || !Number.isInteger(options.prefetch)) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Invalid prefetch value for \"${String(consumerName)}\": must be a positive integer`,\n ),\n ),\n );\n }\n maxPrefetch = Math.max(maxPrefetch, options.prefetch);\n }\n }\n\n // Apply the maximum prefetch if any consumer specified it\n if (maxPrefetch > 0) {\n this.amqpClient.channel.addSetup(async (channel: Channel) => {\n await channel.prefetch(maxPrefetch);\n });\n }\n\n return Future.all(consumerNames.map((consumerName) => this.consume(consumerName)))\n .map(Result.all)\n .mapOk(() => undefined);\n }\n\n private waitForConnectionReady(): Future<Result<void, TechnicalError>> {\n return Future.fromPromise(this.amqpClient.channel.waitForConnect()).mapError(\n (error) => new TechnicalError(\"Failed to wait for connection ready\", error),\n );\n }\n\n /**\n * Start consuming messages for a specific consumer\n */\n private consume<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n ): Future<Result<void, TechnicalError>> {\n const consumers = this.contract.consumers;\n if (!consumers) {\n return Future.value(Result.Error(new TechnicalError(\"No consumers defined in contract\")));\n }\n\n const consumer = consumers[consumerName as string];\n if (!consumer) {\n const availableConsumers = Object.keys(consumers);\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Consumer not found: \"${String(consumerName)}\". Available consumers: ${available}`,\n ),\n ),\n );\n }\n\n const handler = this.actualHandlers[consumerName];\n if (!handler) {\n return Future.value(\n Result.Error(new TechnicalError(`Handler for \"${String(consumerName)}\" not provided`)),\n );\n }\n\n return this.consumeSingle(\n consumerName,\n consumer,\n // All handlers are now safe handlers (Future<Result>)\n handler as (\n message: WorkerInferConsumedMessage<TContract, TName>,\n rawMessage: ConsumeMessage,\n ) => Future<Result<void, HandlerError>>,\n );\n }\n\n /**\n * Parse and validate a message from AMQP\n * @returns `Future<Result<consumed message, void>>` - Ok with validated consumed message (payload + headers), or Error (already handled with nack)\n */\n private parseAndValidateMessage<TName extends InferConsumerNames<TContract>>(\n msg: Message,\n consumer: ConsumerDefinition,\n consumerName: TName,\n ): Future<Result<WorkerInferConsumedMessage<TContract, TName>, void>> {\n // Decompress message if needed\n const decompressMessage: Future<Result<Buffer, void>> = Future.fromPromise(\n decompressBuffer(msg.content, msg.properties.contentEncoding),\n )\n .tapError((error) => {\n this.logger?.error(\"Error decompressing message\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n contentEncoding: msg.properties.contentEncoding,\n error,\n });\n\n // Reject message with no requeue (decompression failed)\n this.amqpClient.channel.nack(msg, false, false);\n })\n .mapError((): void => undefined);\n\n // Parse message\n const parseMessage = (buffer: Buffer): Future<Result<unknown, void>> => {\n const parseResult = Result.fromExecution(() => JSON.parse(buffer.toString()));\n if (parseResult.isError()) {\n this.logger?.error(\"Error parsing message\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error: parseResult.error,\n });\n\n // Reject message with no requeue (malformed JSON)\n this.amqpClient.channel.nack(msg, false, false);\n return Future.value(Result.Error<unknown, void>(undefined));\n }\n return Future.value(Result.Ok<unknown, void>(parseResult.value));\n };\n\n // Validate payload\n const validatePayload = (parsedMessage: unknown): Future<Result<unknown, void>> => {\n const rawValidation = consumer.message.payload[\"~standard\"].validate(parsedMessage);\n return Future.fromPromise(\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation),\n )\n .mapError((): void => undefined)\n .mapOkToResult((validationResult) => {\n if (validationResult.issues) {\n const error = new MessageValidationError(String(consumerName), validationResult.issues);\n this.logger?.error(\"Message payload validation failed\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error,\n });\n\n // Reject message with no requeue (validation failed)\n this.amqpClient.channel.nack(msg, false, false);\n return Result.Error<unknown, void>(undefined);\n }\n\n return Result.Ok<unknown, void>(validationResult.value);\n });\n };\n\n // Validate headers (if schema is defined)\n const validateHeaders = (): Future<Result<unknown, void>> => {\n const headersSchema = consumer.message.headers;\n if (!headersSchema) {\n // No headers schema defined - return undefined for headers\n return Future.value(Result.Ok<unknown, void>(undefined));\n }\n\n // Validate that the schema is a Standard Schema v1 compliant schema\n if (!isStandardSchema(headersSchema)) {\n const error = new MessageValidationError(\n String(consumerName),\n \"Invalid headers schema: not a Standard Schema v1 compliant schema\",\n );\n this.logger?.error(\"Message headers validation failed\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error,\n });\n // Reject message with no requeue (invalid schema configuration)\n this.amqpClient.channel.nack(msg, false, false);\n return Future.value(Result.Error<unknown, void>(undefined));\n }\n\n // After type guard, we know headersSchema is a valid StandardSchemaV1\n const validSchema: StandardSchemaV1 = headersSchema;\n const rawHeaders = msg.properties.headers ?? {};\n const rawValidation = validSchema[\"~standard\"].validate(rawHeaders);\n return Future.fromPromise(\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation),\n )\n .mapError((): void => undefined)\n .mapOkToResult((validationResult: StandardSchemaV1.Result<unknown>) => {\n if (validationResult.issues) {\n const error = new MessageValidationError(String(consumerName), validationResult.issues);\n this.logger?.error(\"Message headers validation failed\", {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n error,\n });\n\n // Reject message with no requeue (validation failed)\n this.amqpClient.channel.nack(msg, false, false);\n return Result.Error<unknown, void>(undefined);\n }\n\n return Result.Ok<unknown, void>(validationResult.value);\n });\n };\n\n // Build the consumed message\n const buildConsumedMessage = (\n validatedPayload: unknown,\n ): Future<Result<WorkerInferConsumedMessage<TContract, TName>, void>> => {\n return validateHeaders().mapOk((validatedHeaders) => {\n // Always return both payload and headers (headers may be undefined)\n return {\n payload: validatedPayload,\n headers: validatedHeaders,\n } as WorkerInferConsumedMessage<TContract, TName>;\n });\n };\n\n return decompressMessage\n .flatMapOk(parseMessage)\n .flatMapOk(validatePayload)\n .flatMapOk(buildConsumedMessage);\n }\n\n /**\n * Consume messages one at a time\n */\n private consumeSingle<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n consumer: ConsumerDefinition,\n handler: (\n message: WorkerInferConsumedMessage<TContract, TName>,\n rawMessage: ConsumeMessage,\n ) => Future<Result<void, HandlerError>>,\n ): Future<Result<void, TechnicalError>> {\n const queueName = consumer.queue.name;\n\n // Start consuming\n return Future.fromPromise(\n this.amqpClient.channel.consume(queueName, async (msg) => {\n // Handle null messages (consumer cancellation)\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(consumerName),\n queueName,\n });\n return;\n }\n\n const startTime = Date.now();\n const span = startConsumeSpan(this.telemetry, queueName, String(consumerName), {\n \"messaging.rabbitmq.message.delivery_tag\": msg.fields.deliveryTag,\n });\n\n // Parse and validate message\n await this.parseAndValidateMessage(msg, consumer, consumerName)\n .flatMapOk((validatedMessage) =>\n handler(validatedMessage, msg)\n .flatMapOk(() => {\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(consumerName),\n queueName,\n });\n // Acknowledge message on success\n this.amqpClient.channel.ack(msg);\n\n // Record telemetry success\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordConsumeMetric(\n this.telemetry,\n queueName,\n String(consumerName),\n true,\n durationMs,\n );\n\n return Future.value(Result.Ok<void, HandlerError>(undefined));\n })\n .flatMapError((handlerError: HandlerError) => {\n // Handler returned an error\n this.logger?.error(\"Error processing message\", {\n consumerName: String(consumerName),\n queueName,\n errorType: handlerError.name,\n error: handlerError.message,\n });\n\n // Record telemetry failure\n const durationMs = Date.now() - startTime;\n endSpanError(span, handlerError);\n recordConsumeMetric(\n this.telemetry,\n queueName,\n String(consumerName),\n false,\n durationMs,\n );\n\n // Handle the error using retry mechanism\n return this.handleError(handlerError, msg, String(consumerName), consumer);\n }),\n )\n .tapError(() => {\n // Record telemetry failure for validation errors\n // Note: The actual validation error is logged in parseAndValidateMessage,\n // here we just record that validation failed for telemetry purposes\n const durationMs = Date.now() - startTime;\n endSpanError(span, new Error(\"Message validation failed\"));\n recordConsumeMetric(this.telemetry, queueName, String(consumerName), false, durationMs);\n })\n .toPromise();\n }),\n )\n .tapOk((reply) => {\n // Store consumer tag for later cancellation\n this.consumerTags.add(reply.consumerTag);\n })\n .mapError(\n (error) =>\n new TechnicalError(`Failed to start consuming for \"${String(consumerName)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n\n /**\n * Handle error in message processing with retry logic.\n *\n * Flow depends on retry mode:\n *\n * **quorum-native mode:**\n * 1. If NonRetryableError -> send directly to DLQ (no retry)\n * 2. Otherwise -> nack with requeue=true (RabbitMQ handles delivery count)\n *\n * **ttl-backoff mode:**\n * 1. If NonRetryableError -> send directly to DLQ (no retry)\n * 2. If max retries exceeded -> send to DLQ\n * 3. Otherwise -> publish to wait queue with TTL for retry\n *\n * **Legacy mode (no retry config):**\n * 1. nack with requeue=true (immediate requeue)\n */\n private handleError(\n error: Error,\n msg: Message,\n consumerName: string,\n consumer: ConsumerDefinition,\n ): Future<Result<void, TechnicalError>> {\n // NonRetryableError -> send directly to DLQ without retrying\n if (error instanceof NonRetryableError) {\n this.logger?.error(\"Non-retryable error, sending to DLQ immediately\", {\n consumerName,\n errorType: error.name,\n error: error.message,\n });\n this.sendToDLQ(msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n // Get retry config from the queue definition in the contract\n const config = this.getRetryConfigForConsumer(consumer);\n\n // Quorum-native mode: let RabbitMQ handle retry via x-delivery-count\n if (config.mode === \"quorum-native\") {\n return this.handleErrorQuorumNative(error, msg, consumerName, consumer);\n }\n\n // TTL-backoff mode: use wait queue with exponential backoff\n return this.handleErrorTtlBackoff(error, msg, consumerName, consumer, config);\n }\n\n /**\n * Handle error using quorum queue's native delivery limit feature.\n *\n * Simply requeues the message with nack(requeue=true). RabbitMQ automatically:\n * - Increments x-delivery-count header\n * - Dead-letters the message when count exceeds x-delivery-limit\n *\n * This is simpler than TTL-based retry but provides immediate retries only.\n */\n private handleErrorQuorumNative(\n error: Error,\n msg: Message,\n consumerName: string,\n consumer: ConsumerDefinition,\n ): Future<Result<void, TechnicalError>> {\n const queue = consumer.queue;\n const queueName = queue.name;\n // x-delivery-count is incremented on each delivery attempt\n // When x-delivery-count equals x-delivery-limit, message is dead-lettered on next attempt\n const deliveryCount = (msg.properties.headers?.[\"x-delivery-count\"] as number) ?? 0;\n // This function is only called for quorum-native mode, which requires quorum queues\n const deliveryLimit = queue.type === \"quorum\" ? queue.deliveryLimit : undefined;\n\n // After this requeue, RabbitMQ will increment deliveryCount\n // Message is dead-lettered when deliveryCount reaches deliveryLimit\n // So if deliveryCount == deliveryLimit - 1, the next failure will dead-letter the message\n const attemptsBeforeDeadLetter =\n deliveryLimit !== undefined ? Math.max(0, deliveryLimit - deliveryCount - 1) : \"unknown\";\n\n // Log warning if this is the last attempt before dead-lettering\n if (deliveryLimit !== undefined && deliveryCount >= deliveryLimit - 1) {\n this.logger?.warn(\"Message at final delivery attempt (quorum-native mode)\", {\n consumerName,\n queueName,\n deliveryCount,\n deliveryLimit,\n willDeadLetterOnNextFailure: deliveryCount === deliveryLimit - 1,\n alreadyExceededLimit: deliveryCount >= deliveryLimit,\n error: error.message,\n });\n } else {\n this.logger?.warn(\"Retrying message (quorum-native mode)\", {\n consumerName,\n queueName,\n deliveryCount,\n deliveryLimit,\n attemptsBeforeDeadLetter,\n error: error.message,\n });\n }\n\n // Requeue the message - RabbitMQ tracks delivery count and handles dead-lettering\n this.amqpClient.channel.nack(msg, false, true);\n return Future.value(Result.Ok(undefined));\n }\n\n /**\n * Handle error using TTL + wait queue pattern for exponential backoff.\n */\n private handleErrorTtlBackoff(\n error: Error,\n msg: Message,\n consumerName: string,\n consumer: ConsumerDefinition,\n config: ResolvedRetryConfig,\n ): Future<Result<void, TechnicalError>> {\n // Get retry count from headers\n const retryCount = (msg.properties.headers?.[\"x-retry-count\"] as number) ?? 0;\n\n // Max retries exceeded -> DLQ\n if (retryCount >= config.maxRetries) {\n this.logger?.error(\"Max retries exceeded, sending to DLQ\", {\n consumerName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n this.sendToDLQ(msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n // Retry with exponential backoff\n const delayMs = this.calculateRetryDelay(retryCount, config);\n this.logger?.warn(\"Retrying message (ttl-backoff mode)\", {\n consumerName,\n retryCount: retryCount + 1,\n delayMs,\n error: error.message,\n });\n\n return this.publishForRetry(msg, consumer, retryCount + 1, delayMs, error);\n }\n\n /**\n * Calculate retry delay with exponential backoff and optional jitter.\n */\n private calculateRetryDelay(retryCount: number, config: ResolvedRetryConfig): number {\n const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = config;\n\n let delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, retryCount), maxDelayMs);\n\n if (jitter) {\n // Add jitter: random value between 50% and 100% of calculated delay\n delay = delay * (0.5 + Math.random() * 0.5);\n }\n\n return Math.floor(delay);\n }\n\n /**\n * Parse message content for republishing.\n * Prevents double JSON serialization by converting Buffer to object when possible.\n */\n private parseMessageContentForRetry(msg: Message, queueName: string): Buffer | unknown {\n let content: Buffer | unknown = msg.content;\n\n // If message is not compressed (no contentEncoding), parse it to get the original object\n if (!msg.properties.contentEncoding) {\n try {\n content = JSON.parse(msg.content.toString());\n } catch (err) {\n this.logger?.warn(\"Failed to parse message for retry, using original buffer\", {\n queueName,\n error: err,\n });\n }\n }\n\n return content;\n }\n\n /**\n * Publish message to wait queue for retry after TTL expires.\n *\n * ┌─────────────────────────────────────────────────────────────────┐\n * │ Retry Flow (Native RabbitMQ TTL + DLX Pattern) │\n * ├─────────────────────────────────────────────────────────────────┤\n * │ │\n * │ 1. Handler throws any Error │\n * │ ↓ │\n * │ 2. Worker publishes to DLX with routing key: {queue}-wait │\n * │ ↓ │\n * │ 3. DLX routes to wait queue: {queue}-wait │\n * │ (with expiration: calculated backoff delay) │\n * │ ↓ │\n * │ 4. Message waits in queue until TTL expires │\n * │ ↓ │\n * │ 5. Expired message dead-lettered to DLX │\n * │ (with routing key: {queue}) │\n * │ ↓ │\n * │ 6. DLX routes back to main queue → RETRY │\n * │ ↓ │\n * │ 7. If retries exhausted: nack without requeue → DLQ │\n * │ │\n * └─────────────────────────────────────────────────────────────────┘\n */\n private publishForRetry(\n msg: Message,\n consumer: ConsumerDefinition,\n newRetryCount: number,\n delayMs: number,\n error: Error,\n ): Future<Result<void, TechnicalError>> {\n const queueName = consumer.queue.name;\n const deadLetter = consumer.queue.deadLetter;\n\n if (!deadLetter) {\n this.logger?.warn(\n \"Cannot retry: queue does not have DLX configured, falling back to nack with requeue\",\n {\n queueName,\n },\n );\n this.amqpClient.channel.nack(msg, false, true);\n return Future.value(Result.Ok(undefined));\n }\n\n const dlxName = deadLetter.exchange.name;\n const waitRoutingKey = `${queueName}-wait`;\n\n // Acknowledge original message\n this.amqpClient.channel.ack(msg);\n\n const content = this.parseMessageContentForRetry(msg, queueName);\n\n // Publish to DLX with wait routing key\n return Future.fromPromise(\n this.amqpClient.channel.publish(dlxName, waitRoutingKey, content, {\n ...msg.properties,\n expiration: delayMs.toString(), // Per-message TTL\n headers: {\n ...msg.properties.headers,\n \"x-retry-count\": newRetryCount,\n \"x-last-error\": error.message,\n \"x-first-failure-timestamp\":\n msg.properties.headers?.[\"x-first-failure-timestamp\"] ?? Date.now(),\n },\n }),\n )\n .mapError((error) => new TechnicalError(\"Failed to publish message for retry\", error))\n .mapOkToResult((published) => {\n if (!published) {\n this.logger?.error(\"Failed to publish message for retry (write buffer full)\", {\n queueName,\n waitRoutingKey,\n retryCount: newRetryCount,\n });\n return Result.Error(\n new TechnicalError(\"Failed to publish message for retry (write buffer full)\"),\n );\n }\n\n this.logger?.info(\"Message published for retry\", {\n queueName,\n waitRoutingKey,\n retryCount: newRetryCount,\n delayMs,\n });\n return Result.Ok(undefined);\n });\n }\n\n /**\n * Send message to dead letter queue.\n * Nacks the message without requeue, relying on DLX configuration.\n */\n private sendToDLQ(msg: Message, consumer: ConsumerDefinition): void {\n const queueName = consumer.queue.name;\n const hasDeadLetter = consumer.queue.deadLetter !== undefined;\n\n if (!hasDeadLetter) {\n this.logger?.warn(\"Queue does not have DLX configured - message will be lost on nack\", {\n queueName,\n });\n }\n\n this.logger?.info(\"Sending message to DLQ\", {\n queueName,\n deliveryTag: msg.fields.deliveryTag,\n });\n\n // Nack without requeue - relies on DLX configuration\n this.amqpClient.channel.nack(msg, false, false);\n }\n}\n","import type { ContractDefinition, InferConsumerNames } from \"@amqp-contract/contract\";\nimport type {\n WorkerInferSafeConsumerHandler,\n WorkerInferSafeConsumerHandlerEntry,\n WorkerInferSafeConsumerHandlers,\n} from \"./types.js\";\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Validate that a consumer exists in the contract\n */\nfunction validateConsumerExists<TContract extends ContractDefinition>(\n contract: TContract,\n consumerName: string,\n): void {\n const consumers = contract.consumers;\n\n if (!consumers || !(consumerName in consumers)) {\n const availableConsumers = consumers ? Object.keys(consumers) : [];\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n throw new Error(\n `Consumer \"${consumerName}\" not found in contract. Available consumers: ${available}`,\n );\n }\n}\n\n/**\n * Validate that all handlers reference valid consumers\n */\nfunction validateHandlers<TContract extends ContractDefinition>(\n contract: TContract,\n handlers: Record<string, unknown>,\n): void {\n const consumers = contract.consumers;\n const availableConsumers = Object.keys(consumers ?? {});\n const availableConsumerNames =\n availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n\n for (const handlerName of Object.keys(handlers)) {\n if (!consumers || !(handlerName in consumers)) {\n throw new Error(\n `Consumer \"${handlerName}\" not found in contract. Available consumers: ${availableConsumerNames}`,\n );\n }\n }\n}\n\n// =============================================================================\n// Safe Handler Definitions (Recommended)\n// =============================================================================\n\n/**\n * Define a type-safe handler for a specific consumer in a contract.\n *\n * **Recommended:** This function creates handlers that return `Future<Result<void, HandlerError>>`,\n * providing explicit error handling and better control over retry behavior.\n *\n * Supports two patterns:\n * 1. Simple handler: just the function\n * 2. Handler with options: [handler, { prefetch: 10 }]\n *\n * @template TContract - The contract definition type\n * @template TName - The consumer name from the contract\n * @param contract - The contract definition containing the consumer\n * @param consumerName - The name of the consumer from the contract\n * @param handler - The handler function that returns `Future<Result<void, HandlerError>>`\n * @param options - Optional consumer options (prefetch)\n * @returns A type-safe handler that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandler, RetryableError, NonRetryableError } from '@amqp-contract/worker';\n * import { Future, Result } from '@swan-io/boxed';\n * import { orderContract } from './contract';\n *\n * // Simple handler with explicit error handling using mapError\n * const processOrderHandler = defineHandler(\n * orderContract,\n * 'processOrder',\n * ({ payload }) =>\n * Future.fromPromise(processPayment(payload))\n * .mapOk(() => undefined)\n * .mapError((error) => new RetryableError('Payment failed', error))\n * );\n *\n * // Handler with validation (non-retryable error)\n * const validateOrderHandler = defineHandler(\n * orderContract,\n * 'validateOrder',\n * ({ payload }) => {\n * if (payload.amount < 1) {\n * // Won't be retried - goes directly to DLQ\n * return Future.value(Result.Error(new NonRetryableError('Invalid order amount')));\n * }\n * return Future.value(Result.Ok(undefined));\n * }\n * );\n * ```\n */\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferSafeConsumerHandler<TContract, TName>,\n): WorkerInferSafeConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferSafeConsumerHandler<TContract, TName>,\n options: { prefetch?: number },\n): WorkerInferSafeConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferSafeConsumerHandler<TContract, TName>,\n options?: { prefetch?: number },\n): WorkerInferSafeConsumerHandlerEntry<TContract, TName> {\n validateConsumerExists(contract, String(consumerName));\n\n if (options) {\n return [handler, options];\n }\n return handler;\n}\n\n/**\n * Define multiple type-safe handlers for consumers in a contract.\n *\n * **Recommended:** This function creates handlers that return `Future<Result<void, HandlerError>>`,\n * providing explicit error handling and better control over retry behavior.\n *\n * @template TContract - The contract definition type\n * @param contract - The contract definition containing the consumers\n * @param handlers - An object with handler functions for each consumer\n * @returns A type-safe handlers object that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandlers, RetryableError } from '@amqp-contract/worker';\n * import { Future } from '@swan-io/boxed';\n * import { orderContract } from './contract';\n *\n * const handlers = defineHandlers(orderContract, {\n * processOrder: ({ payload }) =>\n * Future.fromPromise(processPayment(payload))\n * .mapOk(() => undefined)\n * .mapError((error) => new RetryableError('Payment failed', error)),\n * notifyOrder: ({ payload }) =>\n * Future.fromPromise(sendNotification(payload))\n * .mapOk(() => undefined)\n * .mapError((error) => new RetryableError('Notification failed', error)),\n * });\n * ```\n */\nexport function defineHandlers<TContract extends ContractDefinition>(\n contract: TContract,\n handlers: WorkerInferSafeConsumerHandlers<TContract>,\n): WorkerInferSafeConsumerHandlers<TContract> {\n validateHandlers(contract, handlers as unknown as Record<string, unknown>);\n return handlers;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/errors.ts","../src/decompression.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":["/**\n * Error thrown when message validation fails\n */\nexport class MessageValidationError extends Error {\n constructor(\n public readonly consumerName: string,\n public readonly issues: unknown,\n ) {\n super(`Message validation failed for consumer \"${consumerName}\"`);\n this.name = \"MessageValidationError\";\n // Node.js specific stack trace capture\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Retryable errors - transient failures that may succeed on retry\n * Examples: network timeouts, rate limiting, temporary service unavailability\n *\n * Use this error type when the operation might succeed if retried.\n * The worker will apply exponential backoff and retry the message.\n */\nexport class RetryableError extends Error {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"RetryableError\";\n // Node.js specific stack trace capture\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Non-retryable errors - permanent failures that should not be retried\n * Examples: invalid data, business rule violations, permanent external failures\n *\n * Use this error type when retrying would not help - the message will be\n * immediately sent to the dead letter queue (DLQ) if configured.\n */\nexport class NonRetryableError extends Error {\n constructor(\n message: string,\n public override readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"NonRetryableError\";\n // Node.js specific stack trace capture\n const ErrorConstructor = Error as unknown as {\n captureStackTrace?: (target: object, constructor: Function) => void;\n };\n if (typeof ErrorConstructor.captureStackTrace === \"function\") {\n ErrorConstructor.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Union type representing all handler errors.\n * Use this type when defining handlers that explicitly signal error outcomes.\n */\nexport type HandlerError = RetryableError | NonRetryableError;\n","import { Future, Result } from \"@swan-io/boxed\";\nimport { gunzip, inflate } from \"node:zlib\";\nimport { TechnicalError } from \"@amqp-contract/core\";\nimport { promisify } from \"node:util\";\n\nconst gunzipAsync = promisify(gunzip);\nconst inflateAsync = promisify(inflate);\n\n/**\n * Supported content encodings for message decompression.\n */\nconst SUPPORTED_ENCODINGS = [\"gzip\", \"deflate\"] as const;\n\n/**\n * Type for supported content encodings.\n */\ntype SupportedEncoding = (typeof SUPPORTED_ENCODINGS)[number];\n\n/**\n * Type guard to check if a string is a supported encoding.\n */\nfunction isSupportedEncoding(encoding: string): encoding is SupportedEncoding {\n return SUPPORTED_ENCODINGS.includes(encoding.toLowerCase() as SupportedEncoding);\n}\n\n/**\n * Decompress a buffer based on the content-encoding header.\n *\n * @param buffer - The buffer to decompress\n * @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')\n * @returns A Future with the decompressed buffer or a TechnicalError\n *\n * @internal\n */\nexport function decompressBuffer(\n buffer: Buffer,\n contentEncoding: string | undefined,\n): Future<Result<Buffer, TechnicalError>> {\n if (!contentEncoding) {\n return Future.value(Result.Ok(buffer));\n }\n\n const normalizedEncoding = contentEncoding.toLowerCase();\n\n if (!isSupportedEncoding(normalizedEncoding)) {\n return Future.value(\n Result.Error(\n new TechnicalError(\n `Unsupported content-encoding: \"${contentEncoding}\". ` +\n `Supported encodings are: ${SUPPORTED_ENCODINGS.join(\", \")}. ` +\n `Please check your publisher configuration.`,\n ),\n ),\n );\n }\n\n switch (normalizedEncoding) {\n case \"gzip\":\n return Future.fromPromise(gunzipAsync(buffer)).mapError(\n (error) => new TechnicalError(\"Failed to decompress gzip\", error),\n );\n case \"deflate\":\n return Future.fromPromise(inflateAsync(buffer)).mapError(\n (error) => new TechnicalError(\"Failed to decompress deflate\", error),\n );\n }\n}\n","import {\n AmqpClient,\n type Logger,\n TechnicalError,\n type TelemetryProvider,\n defaultTelemetryProvider,\n endSpanError,\n endSpanSuccess,\n recordConsumeMetric,\n startConsumeSpan,\n} from \"@amqp-contract/core\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport type { Channel, ConsumeMessage } from \"amqplib\";\nimport type {\n ConsumerDefinition,\n ContractDefinition,\n InferConsumerNames,\n ResolvedRetryOptions,\n ResolvedTtlBackoffRetryOptions,\n} from \"@amqp-contract/contract\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport { MessageValidationError, NonRetryableError } from \"./errors.js\";\nimport type {\n WorkerInferConsumedMessage,\n WorkerInferSafeConsumerHandler,\n WorkerInferSafeConsumerHandlers,\n} from \"./types.js\";\nimport type { HandlerError } from \"./errors.js\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { decompressBuffer } from \"./decompression.js\";\n\n/**\n * Internal type for consumer options extracted from handler tuples.\n * Not exported - options are specified inline in the handler tuple types.\n *\n * Note: Retry configuration is now defined at the queue level in the contract,\n * not at the handler level. See `QueueDefinition.retry` for configuration options.\n */\ntype ConsumerOptions = {\n /** Number of messages to prefetch */\n prefetch?: number;\n};\n\n/**\n * Retry configuration from the contract with all values resolved.\n * This is a discriminated union on `mode` - TTL-backoff has the config fields,\n * quorum-native does not.\n */\ntype ResolvedRetryConfig = ResolvedRetryOptions;\n\n/**\n * Type guard to check if a handler entry is a tuple format [handler, options].\n */\nfunction isHandlerTuple(entry: unknown): entry is [unknown, ConsumerOptions] {\n return Array.isArray(entry) && entry.length === 2;\n}\n\n/**\n * Options for creating a type-safe AMQP worker.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * const options: CreateWorkerOptions<typeof contract> = {\n * contract: myContract,\n * handlers: {\n * // Simple handler\n * processOrder: ({ payload }) => {\n * console.log('Processing order:', payload.orderId);\n * return Future.value(Result.Ok(undefined));\n * },\n * // Handler with prefetch configuration\n * processPayment: [\n * ({ payload }) => {\n * console.log('Processing payment:', payload.paymentId);\n * return Future.value(Result.Ok(undefined));\n * },\n * { prefetch: 10 }\n * ]\n * },\n * urls: ['amqp://localhost'],\n * connectionOptions: {\n * heartbeatIntervalInSeconds: 30\n * },\n * logger: myLogger\n * };\n * ```\n *\n * Note: Retry configuration is defined at the queue level in the contract,\n * not at the handler level. See `QueueDefinition.retry` for configuration options.\n */\nexport type CreateWorkerOptions<TContract extends ContractDefinition> = {\n /** The AMQP contract definition specifying consumers and their message schemas */\n contract: TContract;\n /**\n * Handlers for each consumer defined in the contract.\n * Handlers must return `Future<Result<void, HandlerError>>` for explicit error handling.\n * Use defineHandler() to create handlers.\n */\n handlers: WorkerInferSafeConsumerHandlers<TContract>;\n /** AMQP broker URL(s). Multiple URLs provide failover support */\n urls: ConnectionUrl[];\n /** Optional connection configuration (heartbeat, reconnect settings, etc.) */\n connectionOptions?: AmqpConnectionManagerOptions | undefined;\n /** Optional logger for logging message consumption and errors */\n logger?: Logger | undefined;\n /**\n * Optional telemetry provider for tracing and metrics.\n * If not provided, uses the default provider which attempts to load OpenTelemetry.\n * OpenTelemetry instrumentation is automatically enabled if @opentelemetry/api is installed.\n */\n telemetry?: TelemetryProvider | undefined;\n};\n\n/**\n * Type-safe AMQP worker for consuming messages from RabbitMQ.\n *\n * This class provides automatic message validation, connection management,\n * and error handling for consuming messages based on a contract definition.\n *\n * @typeParam TContract - The contract definition type\n *\n * @example\n * ```typescript\n * import { TypedAmqpWorker } from '@amqp-contract/worker';\n * import { z } from 'zod';\n *\n * const contract = defineContract({\n * queues: {\n * orderProcessing: defineQueue('order-processing', { durable: true })\n * },\n * consumers: {\n * processOrder: defineConsumer('order-processing', z.object({\n * orderId: z.string(),\n * amount: z.number()\n * }))\n * }\n * });\n *\n * const worker = await TypedAmqpWorker.create({\n * contract,\n * handlers: {\n * processOrder: async (message) => {\n * console.log('Processing order', message.orderId);\n * // Process the order...\n * }\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n *\n * // Close when done\n * await worker.close().resultToPromise();\n * ```\n */\nexport class TypedAmqpWorker<TContract extends ContractDefinition> {\n /**\n * Internal handler storage - handlers returning `Future<Result>`.\n */\n private readonly actualHandlers: Partial<\n Record<\n InferConsumerNames<TContract>,\n WorkerInferSafeConsumerHandler<TContract, InferConsumerNames<TContract>>\n >\n >;\n private readonly consumerOptions: Partial<Record<InferConsumerNames<TContract>, ConsumerOptions>>;\n private readonly consumerTags: Set<string> = new Set();\n private readonly telemetry: TelemetryProvider;\n\n private constructor(\n private readonly contract: TContract,\n private readonly amqpClient: AmqpClient,\n handlers: WorkerInferSafeConsumerHandlers<TContract>,\n private readonly logger?: Logger,\n telemetry?: TelemetryProvider,\n ) {\n this.telemetry = telemetry ?? defaultTelemetryProvider;\n\n // Extract handlers and options from the handlers object\n this.actualHandlers = {};\n this.consumerOptions = {};\n\n // Cast handlers to a generic record for iteration\n const handlersRecord = handlers as Record<string, unknown>;\n\n for (const consumerName of Object.keys(handlersRecord)) {\n const handlerEntry = handlersRecord[consumerName];\n const typedConsumerName = consumerName as InferConsumerNames<TContract>;\n\n if (isHandlerTuple(handlerEntry)) {\n // Tuple format: [handler, options]\n const [handler, options] = handlerEntry;\n this.actualHandlers[typedConsumerName] = handler as WorkerInferSafeConsumerHandler<\n TContract,\n InferConsumerNames<TContract>\n >;\n this.consumerOptions[typedConsumerName] = options;\n } else {\n // Direct function format\n this.actualHandlers[typedConsumerName] = handlerEntry as WorkerInferSafeConsumerHandler<\n TContract,\n InferConsumerNames<TContract>\n >;\n }\n }\n }\n\n /**\n * Create a type-safe AMQP worker from a contract.\n *\n * Connection management (including automatic reconnection) is handled internally\n * by amqp-connection-manager via the {@link AmqpClient}. The worker will set up\n * consumers for all contract-defined handlers asynchronously in the background\n * once the underlying connection and channels are ready.\n *\n * Connections are automatically shared across clients and workers with the same\n * URLs and connection options, following RabbitMQ best practices.\n *\n * @param options - Configuration options for the worker\n * @returns A Future that resolves to a Result containing the worker or an error\n *\n * @example\n * ```typescript\n * const worker = await TypedAmqpWorker.create({\n * contract: myContract,\n * handlers: {\n * processOrder: async ({ payload }) => console.log('Order:', payload.orderId)\n * },\n * urls: ['amqp://localhost']\n * }).resultToPromise();\n * ```\n */\n static create<TContract extends ContractDefinition>({\n contract,\n handlers,\n urls,\n connectionOptions,\n logger,\n telemetry,\n }: CreateWorkerOptions<TContract>): Future<Result<TypedAmqpWorker<TContract>, TechnicalError>> {\n const worker = new TypedAmqpWorker(\n contract,\n new AmqpClient(contract, {\n urls,\n connectionOptions,\n }),\n handlers,\n logger,\n telemetry,\n );\n\n // Note: Wait queues are now created by the core package in setupAmqpTopology\n // when the queue's retry mode is \"ttl-backoff\"\n return worker\n .waitForConnectionReady()\n .flatMapOk(() => worker.consumeAll())\n .mapOk(() => worker);\n }\n\n /**\n * Close the AMQP channel and connection.\n *\n * This gracefully closes the connection to the AMQP broker,\n * stopping all message consumption and cleaning up resources.\n *\n * @returns A Future that resolves to a Result indicating success or failure\n *\n * @example\n * ```typescript\n * const closeResult = await worker.close().resultToPromise();\n * if (closeResult.isOk()) {\n * console.log('Worker closed successfully');\n * }\n * ```\n */\n close(): Future<Result<void, TechnicalError>> {\n return Future.all(\n Array.from(this.consumerTags).map((consumerTag) =>\n this.amqpClient.cancel(consumerTag).mapErrorToResult((error) => {\n this.logger?.warn(\"Failed to cancel consumer during close\", { consumerTag, error });\n return Result.Ok(undefined);\n }),\n ),\n )\n .map(Result.all)\n .tapOk(() => {\n // Clear consumer tags after successful cancellation\n this.consumerTags.clear();\n })\n .flatMapOk(() => this.amqpClient.close())\n .mapOk(() => undefined);\n }\n\n /**\n * Get the retry configuration for a consumer's queue.\n * Defaults are applied in the contract's defineQueue, so we just return the config.\n */\n private getRetryConfigForConsumer(consumer: ConsumerDefinition): ResolvedRetryConfig {\n return consumer.queue.retry;\n }\n\n /**\n * Start consuming messages for all consumers.\n * TypeScript guarantees consumers exist (handlers require matching consumers).\n */\n private consumeAll(): Future<Result<void, TechnicalError>> {\n const consumers = this.contract.consumers!;\n const consumerNames = Object.keys(consumers) as InferConsumerNames<TContract>[];\n\n // Calculate max prefetch (AMQP 0.9.1 prefetch is per-channel)\n const maxPrefetch = consumerNames.reduce((max, name) => {\n const prefetch = this.consumerOptions[name]?.prefetch;\n return prefetch ? Math.max(max, prefetch) : max;\n }, 0);\n\n if (maxPrefetch > 0) {\n this.amqpClient.addSetup(async (channel: Channel) => {\n await channel.prefetch(maxPrefetch);\n });\n }\n\n return Future.all(consumerNames.map((name) => this.consume(name)))\n .map(Result.all)\n .mapOk(() => undefined);\n }\n\n private waitForConnectionReady(): Future<Result<void, TechnicalError>> {\n return this.amqpClient.waitForConnect();\n }\n\n /**\n * Start consuming messages for a specific consumer.\n * TypeScript guarantees consumer and handler exist for valid consumer names.\n */\n private consume<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n ): Future<Result<void, TechnicalError>> {\n // Non-null assertions safe: TypeScript guarantees these exist for valid TName\n const consumer = this.contract.consumers![consumerName as string]!;\n const handler = this.actualHandlers[consumerName]!;\n\n return this.consumeSingle(\n consumerName,\n consumer,\n handler as Parameters<typeof this.consumeSingle<TName>>[2],\n );\n }\n\n /**\n * Validate data against a Standard Schema and handle errors.\n */\n private validateSchema(\n schema: StandardSchemaV1,\n data: unknown,\n context: { consumerName: string; queueName: string; field: string },\n msg: ConsumeMessage,\n ): Future<Result<unknown, TechnicalError>> {\n const rawValidation = schema[\"~standard\"].validate(data);\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n\n return Future.fromPromise(validationPromise)\n .mapError((error) => new TechnicalError(`Error validating ${context.field}`, error))\n .mapOkToResult((result) => {\n if (result.issues) {\n return Result.Error(\n new TechnicalError(\n `${context.field} validation failed`,\n new MessageValidationError(context.consumerName, result.issues),\n ),\n );\n }\n return Result.Ok(result.value);\n })\n .tapError((error) => {\n this.logger?.error(`${context.field} validation failed`, {\n consumerName: context.consumerName,\n queueName: context.queueName,\n error,\n });\n this.amqpClient.nack(msg, false, false);\n });\n }\n\n /**\n * Parse and validate a message from AMQP.\n * @returns Ok with validated message (payload + headers), or Error (message already nacked)\n */\n private parseAndValidateMessage<TName extends InferConsumerNames<TContract>>(\n msg: ConsumeMessage,\n consumer: ConsumerDefinition,\n consumerName: TName,\n ): Future<Result<WorkerInferConsumedMessage<TContract, TName>, TechnicalError>> {\n const context = {\n consumerName: String(consumerName),\n queueName: consumer.queue.name,\n };\n\n const nackAndError = (message: string, error?: unknown): TechnicalError => {\n this.logger?.error(message, { ...context, error });\n this.amqpClient.nack(msg, false, false);\n return new TechnicalError(message, error);\n };\n\n // Decompress → Parse JSON → Validate payload\n const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding)\n .tapError((error) => {\n this.logger?.error(\"Failed to decompress message\", { ...context, error });\n this.amqpClient.nack(msg, false, false);\n })\n .mapOkToResult((buffer) =>\n Result.fromExecution(() => JSON.parse(buffer.toString()) as unknown).mapError((error) =>\n nackAndError(\"Failed to parse JSON\", error),\n ),\n )\n .flatMapOk((parsed) =>\n this.validateSchema(\n consumer.message.payload as StandardSchemaV1,\n parsed,\n { ...context, field: \"payload\" },\n msg,\n ),\n );\n\n // Validate headers (if schema defined)\n const parseHeaders = consumer.message.headers\n ? this.validateSchema(\n consumer.message.headers as StandardSchemaV1,\n msg.properties.headers ?? {},\n { ...context, field: \"headers\" },\n msg,\n )\n : Future.value(Result.Ok<unknown, TechnicalError>(undefined));\n\n return Future.allFromDict({ payload: parsePayload, headers: parseHeaders }).map(\n Result.allFromDict,\n ) as Future<Result<WorkerInferConsumedMessage<TContract, TName>, TechnicalError>>;\n }\n\n /**\n * Consume messages one at a time\n */\n private consumeSingle<TName extends InferConsumerNames<TContract>>(\n consumerName: TName,\n consumer: ConsumerDefinition,\n handler: (\n message: WorkerInferConsumedMessage<TContract, TName>,\n rawMessage: ConsumeMessage,\n ) => Future<Result<void, HandlerError>>,\n ): Future<Result<void, TechnicalError>> {\n const queueName = consumer.queue.name;\n\n // Start consuming\n return this.amqpClient\n .consume(queueName, async (msg) => {\n // Handle null messages (consumer cancellation)\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(consumerName),\n queueName,\n });\n return;\n }\n\n const startTime = Date.now();\n const span = startConsumeSpan(this.telemetry, queueName, String(consumerName), {\n \"messaging.rabbitmq.message.delivery_tag\": msg.fields.deliveryTag,\n });\n\n // Parse and validate message\n await this.parseAndValidateMessage(msg, consumer, consumerName)\n .flatMapOk((validatedMessage) =>\n handler(validatedMessage, msg)\n .flatMapOk(() => {\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(consumerName),\n queueName,\n });\n // Acknowledge message on success\n this.amqpClient.ack(msg);\n\n // Record telemetry success\n const durationMs = Date.now() - startTime;\n endSpanSuccess(span);\n recordConsumeMetric(\n this.telemetry,\n queueName,\n String(consumerName),\n true,\n durationMs,\n );\n\n return Future.value(Result.Ok<void, HandlerError>(undefined));\n })\n .flatMapError((handlerError: HandlerError) => {\n // Handler returned an error\n this.logger?.error(\"Error processing message\", {\n consumerName: String(consumerName),\n queueName,\n errorType: handlerError.name,\n error: handlerError.message,\n });\n\n // Record telemetry failure\n const durationMs = Date.now() - startTime;\n endSpanError(span, handlerError);\n recordConsumeMetric(\n this.telemetry,\n queueName,\n String(consumerName),\n false,\n durationMs,\n );\n\n // Handle the error using retry mechanism\n return this.handleError(handlerError, msg, String(consumerName), consumer);\n }),\n )\n .tapError(() => {\n // Record telemetry failure for validation errors\n // Note: The actual validation error is logged in parseAndValidateMessage,\n // here we just record that validation failed for telemetry purposes\n const durationMs = Date.now() - startTime;\n endSpanError(span, new Error(\"Message validation failed\"));\n recordConsumeMetric(this.telemetry, queueName, String(consumerName), false, durationMs);\n })\n .toPromise();\n })\n .tapOk((consumerTag) => {\n // Store consumer tag for later cancellation\n this.consumerTags.add(consumerTag);\n })\n .mapError(\n (error) =>\n new TechnicalError(`Failed to start consuming for \"${String(consumerName)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n\n /**\n * Handle error in message processing with retry logic.\n *\n * Flow depends on retry mode:\n *\n * **quorum-native mode:**\n * 1. If NonRetryableError -> send directly to DLQ (no retry)\n * 2. Otherwise -> nack with requeue=true (RabbitMQ handles delivery count)\n *\n * **ttl-backoff mode:**\n * 1. If NonRetryableError -> send directly to DLQ (no retry)\n * 2. If max retries exceeded -> send to DLQ\n * 3. Otherwise -> publish to wait queue with TTL for retry\n *\n * **Legacy mode (no retry config):**\n * 1. nack with requeue=true (immediate requeue)\n */\n private handleError(\n error: Error,\n msg: ConsumeMessage,\n consumerName: string,\n consumer: ConsumerDefinition,\n ): Future<Result<void, TechnicalError>> {\n // NonRetryableError -> send directly to DLQ without retrying\n if (error instanceof NonRetryableError) {\n this.logger?.error(\"Non-retryable error, sending to DLQ immediately\", {\n consumerName,\n errorType: error.name,\n error: error.message,\n });\n this.sendToDLQ(msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n // Get retry config from the queue definition in the contract\n const config = this.getRetryConfigForConsumer(consumer);\n\n // Quorum-native mode: let RabbitMQ handle retry via x-delivery-count\n if (config.mode === \"quorum-native\") {\n return this.handleErrorQuorumNative(error, msg, consumerName, consumer);\n }\n\n // TTL-backoff mode: use wait queue with exponential backoff\n return this.handleErrorTtlBackoff(error, msg, consumerName, consumer, config);\n }\n\n /**\n * Handle error using quorum queue's native delivery limit feature.\n *\n * Simply requeues the message with nack(requeue=true). RabbitMQ automatically:\n * - Increments x-delivery-count header\n * - Dead-letters the message when count exceeds x-delivery-limit\n *\n * This is simpler than TTL-based retry but provides immediate retries only.\n */\n private handleErrorQuorumNative(\n error: Error,\n msg: ConsumeMessage,\n consumerName: string,\n consumer: ConsumerDefinition,\n ): Future<Result<void, TechnicalError>> {\n const queue = consumer.queue;\n const queueName = queue.name;\n // x-delivery-count is incremented on each delivery attempt\n // When x-delivery-count equals x-delivery-limit, message is dead-lettered on next attempt\n const deliveryCount = (msg.properties.headers?.[\"x-delivery-count\"] as number) ?? 0;\n // This function is only called for quorum-native mode, which requires quorum queues\n const deliveryLimit = queue.type === \"quorum\" ? queue.deliveryLimit : undefined;\n\n // After this requeue, RabbitMQ will increment deliveryCount\n // Message is dead-lettered when deliveryCount reaches deliveryLimit\n // So if deliveryCount == deliveryLimit - 1, the next failure will dead-letter the message\n const attemptsBeforeDeadLetter =\n deliveryLimit !== undefined ? Math.max(0, deliveryLimit - deliveryCount - 1) : \"unknown\";\n\n // Log warning if this is the last attempt before dead-lettering\n if (deliveryLimit !== undefined && deliveryCount >= deliveryLimit - 1) {\n this.logger?.warn(\"Message at final delivery attempt (quorum-native mode)\", {\n consumerName,\n queueName,\n deliveryCount,\n deliveryLimit,\n willDeadLetterOnNextFailure: deliveryCount === deliveryLimit - 1,\n alreadyExceededLimit: deliveryCount >= deliveryLimit,\n error: error.message,\n });\n } else {\n this.logger?.warn(\"Retrying message (quorum-native mode)\", {\n consumerName,\n queueName,\n deliveryCount,\n deliveryLimit,\n attemptsBeforeDeadLetter,\n error: error.message,\n });\n }\n\n // Requeue the message - RabbitMQ tracks delivery count and handles dead-lettering\n this.amqpClient.nack(msg, false, true);\n return Future.value(Result.Ok(undefined));\n }\n\n /**\n * Handle error using TTL + wait queue pattern for exponential backoff.\n */\n private handleErrorTtlBackoff(\n error: Error,\n msg: ConsumeMessage,\n consumerName: string,\n consumer: ConsumerDefinition,\n config: ResolvedTtlBackoffRetryOptions,\n ): Future<Result<void, TechnicalError>> {\n // Get retry count from headers\n const retryCount = (msg.properties.headers?.[\"x-retry-count\"] as number) ?? 0;\n\n // Max retries exceeded -> DLQ\n if (retryCount >= config.maxRetries) {\n this.logger?.error(\"Max retries exceeded, sending to DLQ\", {\n consumerName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n this.sendToDLQ(msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n // Retry with exponential backoff\n const delayMs = this.calculateRetryDelay(retryCount, config);\n this.logger?.warn(\"Retrying message (ttl-backoff mode)\", {\n consumerName,\n retryCount: retryCount + 1,\n delayMs,\n error: error.message,\n });\n\n return this.publishForRetry(msg, consumer, retryCount + 1, delayMs, error);\n }\n\n /**\n * Calculate retry delay with exponential backoff and optional jitter.\n */\n private calculateRetryDelay(retryCount: number, config: ResolvedTtlBackoffRetryOptions): number {\n const { initialDelayMs, maxDelayMs, backoffMultiplier, jitter } = config;\n\n let delay = Math.min(initialDelayMs * Math.pow(backoffMultiplier, retryCount), maxDelayMs);\n\n if (jitter) {\n // Add jitter: random value between 50% and 100% of calculated delay\n delay = delay * (0.5 + Math.random() * 0.5);\n }\n\n return Math.floor(delay);\n }\n\n /**\n * Parse message content for republishing.\n * Prevents double JSON serialization by converting Buffer to object when possible.\n */\n private parseMessageContentForRetry(msg: ConsumeMessage, queueName: string): Buffer | unknown {\n let content: Buffer | unknown = msg.content;\n\n // If message is not compressed (no contentEncoding), parse it to get the original object\n if (!msg.properties.contentEncoding) {\n try {\n content = JSON.parse(msg.content.toString());\n } catch (err) {\n this.logger?.warn(\"Failed to parse message for retry, using original buffer\", {\n queueName,\n error: err,\n });\n }\n }\n\n return content;\n }\n\n /**\n * Publish message to wait queue for retry after TTL expires.\n *\n * ┌─────────────────────────────────────────────────────────────────┐\n * │ Retry Flow (Native RabbitMQ TTL + DLX Pattern) │\n * ├─────────────────────────────────────────────────────────────────┤\n * │ │\n * │ 1. Handler throws any Error │\n * │ ↓ │\n * │ 2. Worker publishes to DLX with routing key: {queue}-wait │\n * │ ↓ │\n * │ 3. DLX routes to wait queue: {queue}-wait │\n * │ (with expiration: calculated backoff delay) │\n * │ ↓ │\n * │ 4. Message waits in queue until TTL expires │\n * │ ↓ │\n * │ 5. Expired message dead-lettered to DLX │\n * │ (with routing key: {queue}) │\n * │ ↓ │\n * │ 6. DLX routes back to main queue → RETRY │\n * │ ↓ │\n * │ 7. If retries exhausted: nack without requeue → DLQ │\n * │ │\n * └─────────────────────────────────────────────────────────────────┘\n */\n private publishForRetry(\n msg: ConsumeMessage,\n consumer: ConsumerDefinition,\n newRetryCount: number,\n delayMs: number,\n error: Error,\n ): Future<Result<void, TechnicalError>> {\n const queueName = consumer.queue.name;\n const deadLetter = consumer.queue.deadLetter;\n\n if (!deadLetter) {\n this.logger?.warn(\n \"Cannot retry: queue does not have DLX configured, falling back to nack with requeue\",\n {\n queueName,\n },\n );\n this.amqpClient.nack(msg, false, true);\n return Future.value(Result.Ok(undefined));\n }\n\n const dlxName = deadLetter.exchange.name;\n const waitRoutingKey = `${queueName}-wait`;\n\n // Acknowledge original message\n this.amqpClient.ack(msg);\n\n const content = this.parseMessageContentForRetry(msg, queueName);\n\n // Publish to DLX with wait routing key\n return this.amqpClient\n .publish(dlxName, waitRoutingKey, content, {\n ...msg.properties,\n expiration: delayMs.toString(), // Per-message TTL\n headers: {\n ...msg.properties.headers,\n \"x-retry-count\": newRetryCount,\n \"x-last-error\": error.message,\n \"x-first-failure-timestamp\":\n msg.properties.headers?.[\"x-first-failure-timestamp\"] ?? Date.now(),\n },\n })\n .mapOkToResult((published) => {\n if (!published) {\n this.logger?.error(\"Failed to publish message for retry (write buffer full)\", {\n queueName,\n waitRoutingKey,\n retryCount: newRetryCount,\n });\n return Result.Error(\n new TechnicalError(\"Failed to publish message for retry (write buffer full)\"),\n );\n }\n\n this.logger?.info(\"Message published for retry\", {\n queueName,\n waitRoutingKey,\n retryCount: newRetryCount,\n delayMs,\n });\n return Result.Ok(undefined);\n });\n }\n\n /**\n * Send message to dead letter queue.\n * Nacks the message without requeue, relying on DLX configuration.\n */\n private sendToDLQ(msg: ConsumeMessage, consumer: ConsumerDefinition): void {\n const queueName = consumer.queue.name;\n const hasDeadLetter = consumer.queue.deadLetter !== undefined;\n\n if (!hasDeadLetter) {\n this.logger?.warn(\"Queue does not have DLX configured - message will be lost on nack\", {\n queueName,\n });\n }\n\n this.logger?.info(\"Sending message to DLQ\", {\n queueName,\n deliveryTag: msg.fields.deliveryTag,\n });\n\n // Nack without requeue - relies on DLX configuration\n this.amqpClient.nack(msg, false, false);\n }\n}\n","import type { ContractDefinition, InferConsumerNames } from \"@amqp-contract/contract\";\nimport type {\n WorkerInferSafeConsumerHandler,\n WorkerInferSafeConsumerHandlerEntry,\n WorkerInferSafeConsumerHandlers,\n} from \"./types.js\";\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Validate that a consumer exists in the contract\n */\nfunction validateConsumerExists<TContract extends ContractDefinition>(\n contract: TContract,\n consumerName: string,\n): void {\n const consumers = contract.consumers;\n\n if (!consumers || !(consumerName in consumers)) {\n const availableConsumers = consumers ? Object.keys(consumers) : [];\n const available = availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n throw new Error(\n `Consumer \"${consumerName}\" not found in contract. Available consumers: ${available}`,\n );\n }\n}\n\n/**\n * Validate that all handlers reference valid consumers\n */\nfunction validateHandlers<TContract extends ContractDefinition>(\n contract: TContract,\n handlers: Record<string, unknown>,\n): void {\n const consumers = contract.consumers;\n const availableConsumers = Object.keys(consumers ?? {});\n const availableConsumerNames =\n availableConsumers.length > 0 ? availableConsumers.join(\", \") : \"none\";\n\n for (const handlerName of Object.keys(handlers)) {\n if (!consumers || !(handlerName in consumers)) {\n throw new Error(\n `Consumer \"${handlerName}\" not found in contract. Available consumers: ${availableConsumerNames}`,\n );\n }\n }\n}\n\n// =============================================================================\n// Safe Handler Definitions (Recommended)\n// =============================================================================\n\n/**\n * Define a type-safe handler for a specific consumer in a contract.\n *\n * **Recommended:** This function creates handlers that return `Future<Result<void, HandlerError>>`,\n * providing explicit error handling and better control over retry behavior.\n *\n * Supports two patterns:\n * 1. Simple handler: just the function\n * 2. Handler with options: [handler, { prefetch: 10 }]\n *\n * @template TContract - The contract definition type\n * @template TName - The consumer name from the contract\n * @param contract - The contract definition containing the consumer\n * @param consumerName - The name of the consumer from the contract\n * @param handler - The handler function that returns `Future<Result<void, HandlerError>>`\n * @param options - Optional consumer options (prefetch)\n * @returns A type-safe handler that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandler, RetryableError, NonRetryableError } from '@amqp-contract/worker';\n * import { Future, Result } from '@swan-io/boxed';\n * import { orderContract } from './contract';\n *\n * // Simple handler with explicit error handling using mapError\n * const processOrderHandler = defineHandler(\n * orderContract,\n * 'processOrder',\n * ({ payload }) =>\n * Future.fromPromise(processPayment(payload))\n * .mapOk(() => undefined)\n * .mapError((error) => new RetryableError('Payment failed', error))\n * );\n *\n * // Handler with validation (non-retryable error)\n * const validateOrderHandler = defineHandler(\n * orderContract,\n * 'validateOrder',\n * ({ payload }) => {\n * if (payload.amount < 1) {\n * // Won't be retried - goes directly to DLQ\n * return Future.value(Result.Error(new NonRetryableError('Invalid order amount')));\n * }\n * return Future.value(Result.Ok(undefined));\n * }\n * );\n * ```\n */\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferSafeConsumerHandler<TContract, TName>,\n): WorkerInferSafeConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferSafeConsumerHandler<TContract, TName>,\n options: { prefetch?: number },\n): WorkerInferSafeConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferSafeConsumerHandler<TContract, TName>,\n options?: { prefetch?: number },\n): WorkerInferSafeConsumerHandlerEntry<TContract, TName> {\n validateConsumerExists(contract, String(consumerName));\n\n if (options) {\n return [handler, options];\n }\n return handler;\n}\n\n/**\n * Define multiple type-safe handlers for consumers in a contract.\n *\n * **Recommended:** This function creates handlers that return `Future<Result<void, HandlerError>>`,\n * providing explicit error handling and better control over retry behavior.\n *\n * @template TContract - The contract definition type\n * @param contract - The contract definition containing the consumers\n * @param handlers - An object with handler functions for each consumer\n * @returns A type-safe handlers object that can be used with TypedAmqpWorker\n *\n * @example\n * ```typescript\n * import { defineHandlers, RetryableError } from '@amqp-contract/worker';\n * import { Future } from '@swan-io/boxed';\n * import { orderContract } from './contract';\n *\n * const handlers = defineHandlers(orderContract, {\n * processOrder: ({ payload }) =>\n * Future.fromPromise(processPayment(payload))\n * .mapOk(() => undefined)\n * .mapError((error) => new RetryableError('Payment failed', error)),\n * notifyOrder: ({ payload }) =>\n * Future.fromPromise(sendNotification(payload))\n * .mapOk(() => undefined)\n * .mapError((error) => new RetryableError('Notification failed', error)),\n * });\n * ```\n */\nexport function defineHandlers<TContract extends ContractDefinition>(\n contract: TContract,\n handlers: WorkerInferSafeConsumerHandlers<TContract>,\n): WorkerInferSafeConsumerHandlers<TContract> {\n validateHandlers(contract, handlers as unknown as Record<string, unknown>);\n return handlers;\n}\n"],"mappings":";;;;;;;;;AAGA,IAAa,yBAAb,cAA4C,MAAM;CAChD,YACE,AAAgB,cAChB,AAAgB,QAChB;AACA,QAAM,2CAA2C,aAAa,GAAG;EAHjD;EACA;AAGhB,OAAK,OAAO;EAEZ,MAAM,mBAAmB;AAGzB,MAAI,OAAO,iBAAiB,sBAAsB,WAChD,kBAAiB,kBAAkB,MAAM,KAAK,YAAY;;;;;;;;;;AAYhE,IAAa,iBAAb,cAAoC,MAAM;CACxC,YACE,SACA,AAAyB,OACzB;AACA,QAAM,QAAQ;EAFW;AAGzB,OAAK,OAAO;EAEZ,MAAM,mBAAmB;AAGzB,MAAI,OAAO,iBAAiB,sBAAsB,WAChD,kBAAiB,kBAAkB,MAAM,KAAK,YAAY;;;;;;;;;;AAYhE,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YACE,SACA,AAAyB,OACzB;AACA,QAAM,QAAQ;EAFW;AAGzB,OAAK,OAAO;EAEZ,MAAM,mBAAmB;AAGzB,MAAI,OAAO,iBAAiB,sBAAsB,WAChD,kBAAiB,kBAAkB,MAAM,KAAK,YAAY;;;;;;AC1DhE,MAAM,cAAc,UAAU,OAAO;AACrC,MAAM,eAAe,UAAU,QAAQ;;;;AAKvC,MAAM,sBAAsB,CAAC,QAAQ,UAAU;;;;AAU/C,SAAS,oBAAoB,UAAiD;AAC5E,QAAO,oBAAoB,SAAS,SAAS,aAAa,CAAsB;;;;;;;;;;;AAYlF,SAAgB,iBACd,QACA,iBACwC;AACxC,KAAI,CAAC,gBACH,QAAO,OAAO,MAAM,OAAO,GAAG,OAAO,CAAC;CAGxC,MAAM,qBAAqB,gBAAgB,aAAa;AAExD,KAAI,CAAC,oBAAoB,mBAAmB,CAC1C,QAAO,OAAO,MACZ,OAAO,MACL,IAAI,eACF,kCAAkC,gBAAgB,8BACpB,oBAAoB,KAAK,KAAK,CAAC,8CAE9D,CACF,CACF;AAGH,SAAQ,oBAAR;EACE,KAAK,OACH,QAAO,OAAO,YAAY,YAAY,OAAO,CAAC,CAAC,UAC5C,UAAU,IAAI,eAAe,6BAA6B,MAAM,CAClE;EACH,KAAK,UACH,QAAO,OAAO,YAAY,aAAa,OAAO,CAAC,CAAC,UAC7C,UAAU,IAAI,eAAe,gCAAgC,MAAM,CACrE;;;;;;;;;ACXP,SAAS,eAAe,OAAqD;AAC3E,QAAO,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqGlD,IAAa,kBAAb,MAAa,gBAAsD;;;;CAIjE,AAAiB;CAMjB,AAAiB;CACjB,AAAiB,+BAA4B,IAAI,KAAK;CACtD,AAAiB;CAEjB,AAAQ,YACN,AAAiB,UACjB,AAAiB,YACjB,UACA,AAAiB,QACjB,WACA;EALiB;EACA;EAEA;AAGjB,OAAK,YAAY,aAAa;AAG9B,OAAK,iBAAiB,EAAE;AACxB,OAAK,kBAAkB,EAAE;EAGzB,MAAM,iBAAiB;AAEvB,OAAK,MAAM,gBAAgB,OAAO,KAAK,eAAe,EAAE;GACtD,MAAM,eAAe,eAAe;GACpC,MAAM,oBAAoB;AAE1B,OAAI,eAAe,aAAa,EAAE;IAEhC,MAAM,CAAC,SAAS,WAAW;AAC3B,SAAK,eAAe,qBAAqB;AAIzC,SAAK,gBAAgB,qBAAqB;SAG1C,MAAK,eAAe,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiC/C,OAAO,OAA6C,EAClD,UACA,UACA,MACA,mBACA,QACA,aAC6F;EAC7F,MAAM,SAAS,IAAI,gBACjB,UACA,IAAI,WAAW,UAAU;GACvB;GACA;GACD,CAAC,EACF,UACA,QACA,UACD;AAID,SAAO,OACJ,wBAAwB,CACxB,gBAAgB,OAAO,YAAY,CAAC,CACpC,YAAY,OAAO;;;;;;;;;;;;;;;;;;CAmBxB,QAA8C;AAC5C,SAAO,OAAO,IACZ,MAAM,KAAK,KAAK,aAAa,CAAC,KAAK,gBACjC,KAAK,WAAW,OAAO,YAAY,CAAC,kBAAkB,UAAU;AAC9D,QAAK,QAAQ,KAAK,0CAA0C;IAAE;IAAa;IAAO,CAAC;AACnF,UAAO,OAAO,GAAG,OAAU;IAC3B,CACH,CACF,CACE,IAAI,OAAO,IAAI,CACf,YAAY;AAEX,QAAK,aAAa,OAAO;IACzB,CACD,gBAAgB,KAAK,WAAW,OAAO,CAAC,CACxC,YAAY,OAAU;;;;;;CAO3B,AAAQ,0BAA0B,UAAmD;AACnF,SAAO,SAAS,MAAM;;;;;;CAOxB,AAAQ,aAAmD;EACzD,MAAM,YAAY,KAAK,SAAS;EAChC,MAAM,gBAAgB,OAAO,KAAK,UAAU;EAG5C,MAAM,cAAc,cAAc,QAAQ,KAAK,SAAS;GACtD,MAAM,WAAW,KAAK,gBAAgB,OAAO;AAC7C,UAAO,WAAW,KAAK,IAAI,KAAK,SAAS,GAAG;KAC3C,EAAE;AAEL,MAAI,cAAc,EAChB,MAAK,WAAW,SAAS,OAAO,YAAqB;AACnD,SAAM,QAAQ,SAAS,YAAY;IACnC;AAGJ,SAAO,OAAO,IAAI,cAAc,KAAK,SAAS,KAAK,QAAQ,KAAK,CAAC,CAAC,CAC/D,IAAI,OAAO,IAAI,CACf,YAAY,OAAU;;CAG3B,AAAQ,yBAA+D;AACrE,SAAO,KAAK,WAAW,gBAAgB;;;;;;CAOzC,AAAQ,QACN,cACsC;EAEtC,MAAM,WAAW,KAAK,SAAS,UAAW;EAC1C,MAAM,UAAU,KAAK,eAAe;AAEpC,SAAO,KAAK,cACV,cACA,UACA,QACD;;;;;CAMH,AAAQ,eACN,QACA,MACA,SACA,KACyC;EACzC,MAAM,gBAAgB,OAAO,aAAa,SAAS,KAAK;EACxD,MAAM,oBACJ,yBAAyB,UAAU,gBAAgB,QAAQ,QAAQ,cAAc;AAEnF,SAAO,OAAO,YAAY,kBAAkB,CACzC,UAAU,UAAU,IAAI,eAAe,oBAAoB,QAAQ,SAAS,MAAM,CAAC,CACnF,eAAe,WAAW;AACzB,OAAI,OAAO,OACT,QAAO,OAAO,MACZ,IAAI,eACF,GAAG,QAAQ,MAAM,qBACjB,IAAI,uBAAuB,QAAQ,cAAc,OAAO,OAAO,CAChE,CACF;AAEH,UAAO,OAAO,GAAG,OAAO,MAAM;IAC9B,CACD,UAAU,UAAU;AACnB,QAAK,QAAQ,MAAM,GAAG,QAAQ,MAAM,qBAAqB;IACvD,cAAc,QAAQ;IACtB,WAAW,QAAQ;IACnB;IACD,CAAC;AACF,QAAK,WAAW,KAAK,KAAK,OAAO,MAAM;IACvC;;;;;;CAON,AAAQ,wBACN,KACA,UACA,cAC8E;EAC9E,MAAM,UAAU;GACd,cAAc,OAAO,aAAa;GAClC,WAAW,SAAS,MAAM;GAC3B;EAED,MAAM,gBAAgB,SAAiB,UAAoC;AACzE,QAAK,QAAQ,MAAM,SAAS;IAAE,GAAG;IAAS;IAAO,CAAC;AAClD,QAAK,WAAW,KAAK,KAAK,OAAO,MAAM;AACvC,UAAO,IAAI,eAAe,SAAS,MAAM;;EAI3C,MAAM,eAAe,iBAAiB,IAAI,SAAS,IAAI,WAAW,gBAAgB,CAC/E,UAAU,UAAU;AACnB,QAAK,QAAQ,MAAM,gCAAgC;IAAE,GAAG;IAAS;IAAO,CAAC;AACzE,QAAK,WAAW,KAAK,KAAK,OAAO,MAAM;IACvC,CACD,eAAe,WACd,OAAO,oBAAoB,KAAK,MAAM,OAAO,UAAU,CAAC,CAAY,CAAC,UAAU,UAC7E,aAAa,wBAAwB,MAAM,CAC5C,CACF,CACA,WAAW,WACV,KAAK,eACH,SAAS,QAAQ,SACjB,QACA;GAAE,GAAG;GAAS,OAAO;GAAW,EAChC,IACD,CACF;EAGH,MAAM,eAAe,SAAS,QAAQ,UAClC,KAAK,eACH,SAAS,QAAQ,SACjB,IAAI,WAAW,WAAW,EAAE,EAC5B;GAAE,GAAG;GAAS,OAAO;GAAW,EAChC,IACD,GACD,OAAO,MAAM,OAAO,GAA4B,OAAU,CAAC;AAE/D,SAAO,OAAO,YAAY;GAAE,SAAS;GAAc,SAAS;GAAc,CAAC,CAAC,IAC1E,OAAO,YACR;;;;;CAMH,AAAQ,cACN,cACA,UACA,SAIsC;EACtC,MAAM,YAAY,SAAS,MAAM;AAGjC,SAAO,KAAK,WACT,QAAQ,WAAW,OAAO,QAAQ;AAEjC,OAAI,QAAQ,MAAM;AAChB,SAAK,QAAQ,KAAK,gCAAgC;KAChD,cAAc,OAAO,aAAa;KAClC;KACD,CAAC;AACF;;GAGF,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,OAAO,iBAAiB,KAAK,WAAW,WAAW,OAAO,aAAa,EAAE,EAC7E,2CAA2C,IAAI,OAAO,aACvD,CAAC;AAGF,SAAM,KAAK,wBAAwB,KAAK,UAAU,aAAa,CAC5D,WAAW,qBACV,QAAQ,kBAAkB,IAAI,CAC3B,gBAAgB;AACf,SAAK,QAAQ,KAAK,iCAAiC;KACjD,cAAc,OAAO,aAAa;KAClC;KACD,CAAC;AAEF,SAAK,WAAW,IAAI,IAAI;IAGxB,MAAM,aAAa,KAAK,KAAK,GAAG;AAChC,mBAAe,KAAK;AACpB,wBACE,KAAK,WACL,WACA,OAAO,aAAa,EACpB,MACA,WACD;AAED,WAAO,OAAO,MAAM,OAAO,GAAuB,OAAU,CAAC;KAC7D,CACD,cAAc,iBAA+B;AAE5C,SAAK,QAAQ,MAAM,4BAA4B;KAC7C,cAAc,OAAO,aAAa;KAClC;KACA,WAAW,aAAa;KACxB,OAAO,aAAa;KACrB,CAAC;IAGF,MAAM,aAAa,KAAK,KAAK,GAAG;AAChC,iBAAa,MAAM,aAAa;AAChC,wBACE,KAAK,WACL,WACA,OAAO,aAAa,EACpB,OACA,WACD;AAGD,WAAO,KAAK,YAAY,cAAc,KAAK,OAAO,aAAa,EAAE,SAAS;KAC1E,CACL,CACA,eAAe;IAId,MAAM,aAAa,KAAK,KAAK,GAAG;AAChC,iBAAa,sBAAM,IAAI,MAAM,4BAA4B,CAAC;AAC1D,wBAAoB,KAAK,WAAW,WAAW,OAAO,aAAa,EAAE,OAAO,WAAW;KACvF,CACD,WAAW;IACd,CACD,OAAO,gBAAgB;AAEtB,QAAK,aAAa,IAAI,YAAY;IAClC,CACD,UACE,UACC,IAAI,eAAe,kCAAkC,OAAO,aAAa,CAAC,IAAI,MAAM,CACvF,CACA,YAAY,OAAU;;;;;;;;;;;;;;;;;;;CAoB3B,AAAQ,YACN,OACA,KACA,cACA,UACsC;AAEtC,MAAI,iBAAiB,mBAAmB;AACtC,QAAK,QAAQ,MAAM,mDAAmD;IACpE;IACA,WAAW,MAAM;IACjB,OAAO,MAAM;IACd,CAAC;AACF,QAAK,UAAU,KAAK,SAAS;AAC7B,UAAO,OAAO,MAAM,OAAO,GAAG,OAAU,CAAC;;EAI3C,MAAM,SAAS,KAAK,0BAA0B,SAAS;AAGvD,MAAI,OAAO,SAAS,gBAClB,QAAO,KAAK,wBAAwB,OAAO,KAAK,cAAc,SAAS;AAIzE,SAAO,KAAK,sBAAsB,OAAO,KAAK,cAAc,UAAU,OAAO;;;;;;;;;;;CAY/E,AAAQ,wBACN,OACA,KACA,cACA,UACsC;EACtC,MAAM,QAAQ,SAAS;EACvB,MAAM,YAAY,MAAM;EAGxB,MAAM,gBAAiB,IAAI,WAAW,UAAU,uBAAkC;EAElF,MAAM,gBAAgB,MAAM,SAAS,WAAW,MAAM,gBAAgB;EAKtE,MAAM,2BACJ,kBAAkB,SAAY,KAAK,IAAI,GAAG,gBAAgB,gBAAgB,EAAE,GAAG;AAGjF,MAAI,kBAAkB,UAAa,iBAAiB,gBAAgB,EAClE,MAAK,QAAQ,KAAK,0DAA0D;GAC1E;GACA;GACA;GACA;GACA,6BAA6B,kBAAkB,gBAAgB;GAC/D,sBAAsB,iBAAiB;GACvC,OAAO,MAAM;GACd,CAAC;MAEF,MAAK,QAAQ,KAAK,yCAAyC;GACzD;GACA;GACA;GACA;GACA;GACA,OAAO,MAAM;GACd,CAAC;AAIJ,OAAK,WAAW,KAAK,KAAK,OAAO,KAAK;AACtC,SAAO,OAAO,MAAM,OAAO,GAAG,OAAU,CAAC;;;;;CAM3C,AAAQ,sBACN,OACA,KACA,cACA,UACA,QACsC;EAEtC,MAAM,aAAc,IAAI,WAAW,UAAU,oBAA+B;AAG5E,MAAI,cAAc,OAAO,YAAY;AACnC,QAAK,QAAQ,MAAM,wCAAwC;IACzD;IACA;IACA,YAAY,OAAO;IACnB,OAAO,MAAM;IACd,CAAC;AACF,QAAK,UAAU,KAAK,SAAS;AAC7B,UAAO,OAAO,MAAM,OAAO,GAAG,OAAU,CAAC;;EAI3C,MAAM,UAAU,KAAK,oBAAoB,YAAY,OAAO;AAC5D,OAAK,QAAQ,KAAK,uCAAuC;GACvD;GACA,YAAY,aAAa;GACzB;GACA,OAAO,MAAM;GACd,CAAC;AAEF,SAAO,KAAK,gBAAgB,KAAK,UAAU,aAAa,GAAG,SAAS,MAAM;;;;;CAM5E,AAAQ,oBAAoB,YAAoB,QAAgD;EAC9F,MAAM,EAAE,gBAAgB,YAAY,mBAAmB,WAAW;EAElE,IAAI,QAAQ,KAAK,IAAI,iBAAiB,KAAK,IAAI,mBAAmB,WAAW,EAAE,WAAW;AAE1F,MAAI,OAEF,SAAQ,SAAS,KAAM,KAAK,QAAQ,GAAG;AAGzC,SAAO,KAAK,MAAM,MAAM;;;;;;CAO1B,AAAQ,4BAA4B,KAAqB,WAAqC;EAC5F,IAAI,UAA4B,IAAI;AAGpC,MAAI,CAAC,IAAI,WAAW,gBAClB,KAAI;AACF,aAAU,KAAK,MAAM,IAAI,QAAQ,UAAU,CAAC;WACrC,KAAK;AACZ,QAAK,QAAQ,KAAK,4DAA4D;IAC5E;IACA,OAAO;IACR,CAAC;;AAIN,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BT,AAAQ,gBACN,KACA,UACA,eACA,SACA,OACsC;EACtC,MAAM,YAAY,SAAS,MAAM;EACjC,MAAM,aAAa,SAAS,MAAM;AAElC,MAAI,CAAC,YAAY;AACf,QAAK,QAAQ,KACX,uFACA,EACE,WACD,CACF;AACD,QAAK,WAAW,KAAK,KAAK,OAAO,KAAK;AACtC,UAAO,OAAO,MAAM,OAAO,GAAG,OAAU,CAAC;;EAG3C,MAAM,UAAU,WAAW,SAAS;EACpC,MAAM,iBAAiB,GAAG,UAAU;AAGpC,OAAK,WAAW,IAAI,IAAI;EAExB,MAAM,UAAU,KAAK,4BAA4B,KAAK,UAAU;AAGhE,SAAO,KAAK,WACT,QAAQ,SAAS,gBAAgB,SAAS;GACzC,GAAG,IAAI;GACP,YAAY,QAAQ,UAAU;GAC9B,SAAS;IACP,GAAG,IAAI,WAAW;IAClB,iBAAiB;IACjB,gBAAgB,MAAM;IACtB,6BACE,IAAI,WAAW,UAAU,gCAAgC,KAAK,KAAK;IACtE;GACF,CAAC,CACD,eAAe,cAAc;AAC5B,OAAI,CAAC,WAAW;AACd,SAAK,QAAQ,MAAM,2DAA2D;KAC5E;KACA;KACA,YAAY;KACb,CAAC;AACF,WAAO,OAAO,MACZ,IAAI,eAAe,0DAA0D,CAC9E;;AAGH,QAAK,QAAQ,KAAK,+BAA+B;IAC/C;IACA;IACA,YAAY;IACZ;IACD,CAAC;AACF,UAAO,OAAO,GAAG,OAAU;IAC3B;;;;;;CAON,AAAQ,UAAU,KAAqB,UAAoC;EACzE,MAAM,YAAY,SAAS,MAAM;AAGjC,MAAI,EAFkB,SAAS,MAAM,eAAe,QAGlD,MAAK,QAAQ,KAAK,qEAAqE,EACrF,WACD,CAAC;AAGJ,OAAK,QAAQ,KAAK,0BAA0B;GAC1C;GACA,aAAa,IAAI,OAAO;GACzB,CAAC;AAGF,OAAK,WAAW,KAAK,KAAK,OAAO,MAAM;;;;;;;;;AC3yB3C,SAAS,uBACP,UACA,cACM;CACN,MAAM,YAAY,SAAS;AAE3B,KAAI,CAAC,aAAa,EAAE,gBAAgB,YAAY;EAC9C,MAAM,qBAAqB,YAAY,OAAO,KAAK,UAAU,GAAG,EAAE;EAClE,MAAM,YAAY,mBAAmB,SAAS,IAAI,mBAAmB,KAAK,KAAK,GAAG;AAClF,QAAM,IAAI,MACR,aAAa,aAAa,gDAAgD,YAC3E;;;;;;AAOL,SAAS,iBACP,UACA,UACM;CACN,MAAM,YAAY,SAAS;CAC3B,MAAM,qBAAqB,OAAO,KAAK,aAAa,EAAE,CAAC;CACvD,MAAM,yBACJ,mBAAmB,SAAS,IAAI,mBAAmB,KAAK,KAAK,GAAG;AAElE,MAAK,MAAM,eAAe,OAAO,KAAK,SAAS,CAC7C,KAAI,CAAC,aAAa,EAAE,eAAe,WACjC,OAAM,IAAI,MACR,aAAa,YAAY,gDAAgD,yBAC1E;;AA0EP,SAAgB,cAId,UACA,cACA,SACA,SACuD;AACvD,wBAAuB,UAAU,OAAO,aAAa,CAAC;AAEtD,KAAI,QACF,QAAO,CAAC,SAAS,QAAQ;AAE3B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT,SAAgB,eACd,UACA,UAC4C;AAC5C,kBAAiB,UAAU,SAA+C;AAC1E,QAAO"}
|