@amqp-contract/worker 0.22.0 → 0.23.1

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.mjs CHANGED
@@ -355,19 +355,28 @@ function calculateRetryDelay(retryCount, config) {
355
355
  }
356
356
  /**
357
357
  * Parse message content for republishing.
358
- * Prevents double JSON serialization by converting Buffer to object when possible.
358
+ *
359
+ * The channel is configured with `json: true`, so values published as plain
360
+ * objects are encoded once at publish time. Re-publishing the raw `Buffer`
361
+ * would then trigger a *second* JSON.stringify (turning the bytes into a
362
+ * stringified base64 blob), so for JSON payloads we must round-trip back to
363
+ * the parsed value. For any other content type — or when the message is
364
+ * compressed — we pass the bytes through untouched, since re-parsing would
365
+ * either fail or silently corrupt binary data.
359
366
  */
360
367
  function parseMessageContentForRetry(ctx, msg, queueName) {
361
- let content = msg.content;
362
- if (!msg.properties.contentEncoding) try {
363
- content = JSON.parse(msg.content.toString());
368
+ if (msg.properties.contentEncoding) return msg.content;
369
+ const contentType = msg.properties.contentType;
370
+ if (!(contentType === void 0 || contentType === "application/json" || contentType.startsWith("application/json;") || contentType.endsWith("+json"))) return msg.content;
371
+ try {
372
+ return JSON.parse(msg.content.toString());
364
373
  } catch (err) {
365
- ctx.logger?.warn("Failed to parse message for retry, using original buffer", {
374
+ ctx.logger?.warn("Failed to parse JSON message for retry, using original buffer", {
366
375
  queueName,
367
376
  error: err
368
377
  });
378
+ return msg.content;
369
379
  }
370
- return content;
371
380
  }
372
381
  /**
373
382
  * Publish message with an incremented x-retry-count header and optional TTL.
@@ -618,55 +627,34 @@ var TypedAmqpWorker = class TypedAmqpWorker {
618
627
  return this.consumeSingle(name, view, handler);
619
628
  }
620
629
  /**
621
- * Validate data against a Standard Schema and handle errors.
630
+ * Validate data against a Standard Schema. No side effects; the caller is
631
+ * responsible for ack/nack based on the Result.
622
632
  */
623
- validateSchema(schema, data, context, msg) {
633
+ validateSchema(schema, data, context) {
624
634
  const rawValidation = schema["~standard"].validate(data);
625
635
  const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
626
636
  return Future.fromPromise(validationPromise).mapError((error) => new TechnicalError(`Error validating ${context.field}`, error)).mapOkToResult((result) => {
627
637
  if (result.issues) return Result.Error(new TechnicalError(`${context.field} validation failed`, new MessageValidationError(context.consumerName, result.issues)));
628
638
  return Result.Ok(result.value);
629
- }).tapError((error) => {
630
- this.logger?.error(`${context.field} validation failed`, {
631
- consumerName: context.consumerName,
632
- queueName: context.queueName,
633
- error
634
- });
635
- this.amqpClient.nack(msg, false, false);
636
639
  });
637
640
  }
638
641
  /**
639
- * Parse and validate a message from AMQP.
640
- * @returns Ok with validated message (payload + headers), or Error (message already nacked)
642
+ * Parse and validate a message from AMQP. Pure: returns the validated payload
643
+ * and headers, or an error. The dispatch path in {@link processMessage} routes
644
+ * validation/parse errors directly to the DLQ (single nack) — they never enter
645
+ * the retry pipeline because retrying an unparseable or schema-violating
646
+ * payload cannot succeed.
641
647
  */
642
648
  parseAndValidateMessage(msg, consumer, consumerName) {
643
- const queue = extractQueue(consumer.queue);
644
- const context = {
645
- consumerName: String(consumerName),
646
- queueName: queue.name
647
- };
648
- const nackAndError = (message, error) => {
649
- this.logger?.error(message, {
650
- ...context,
651
- error
652
- });
653
- this.amqpClient.nack(msg, false, false);
654
- return new TechnicalError(message, error);
655
- };
656
- const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding).tapError((error) => {
657
- this.logger?.error("Failed to decompress message", {
658
- ...context,
659
- error
660
- });
661
- this.amqpClient.nack(msg, false, false);
662
- }).mapOkToResult((buffer) => Result.fromExecution(() => JSON.parse(buffer.toString())).mapError((error) => nackAndError("Failed to parse JSON", error))).flatMapOk((parsed) => this.validateSchema(consumer.message.payload, parsed, {
649
+ const context = { consumerName: String(consumerName) };
650
+ const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding).mapErrorToResult((error) => Result.Error(new TechnicalError("Failed to decompress message", error))).mapOkToResult((buffer) => Result.fromExecution(() => JSON.parse(buffer.toString())).mapError((error) => new TechnicalError("Failed to parse JSON", error))).flatMapOk((parsed) => this.validateSchema(consumer.message.payload, parsed, {
663
651
  ...context,
664
652
  field: "payload"
665
- }, msg));
653
+ }));
666
654
  const parseHeaders = consumer.message.headers ? this.validateSchema(consumer.message.headers, msg.properties.headers ?? {}, {
667
655
  ...context,
668
656
  field: "headers"
669
- }, msg) : Future.value(Result.Ok(void 0));
657
+ }) : Future.value(Result.Ok(void 0));
670
658
  return Future.allFromDict({
671
659
  payload: parsePayload,
672
660
  headers: parseHeaders
@@ -678,27 +666,36 @@ var TypedAmqpWorker = class TypedAmqpWorker {
678
666
  * with `routingKey = msg.properties.replyTo`, which works for both
679
667
  * `amq.rabbitmq.reply-to` and any anonymous queue declared by the caller.
680
668
  *
681
- * Validation errors are surfaced as NonRetryableError (handler returned the
682
- * wrong shape retrying the same input will not fix it). Publish errors are
683
- * surfaced as RetryableError so the worker's existing retry logic applies.
669
+ * Failure semantics:
670
+ * - **Missing replyTo / correlationId**: NonRetryableError. The caller is
671
+ * already lost; retrying the original message cannot recover the reply
672
+ * path. The poison message lands in DLQ for inspection rather than being
673
+ * silently ack'd (which would mask a contract violation).
674
+ * - **Schema validation failure**: NonRetryableError — the handler returned
675
+ * the wrong shape; retrying the same input will not fix it.
676
+ * - **Publish failure**: NonRetryableError. The caller has already timed out
677
+ * (or will shortly), so retrying the message wastes the queue's retry
678
+ * budget on a reply that no one is waiting for. The message is logged and
679
+ * DLQ'd; the original work is treated as completed for the purpose of the
680
+ * inbox.
684
681
  */
685
682
  publishRpcResponse(msg, queueName, rpcName, responseSchema, response) {
686
683
  const replyTo = msg.properties.replyTo;
687
684
  const correlationId = msg.properties.correlationId;
688
685
  if (typeof replyTo !== "string" || replyTo.length === 0) {
689
- this.logger?.warn("RPC handler returned a response but the incoming message has no replyTo; dropping response", {
686
+ this.logger?.error("RPC handler returned a response but the incoming message has no replyTo", {
690
687
  rpcName: String(rpcName),
691
688
  queueName
692
689
  });
693
- return Future.value(Result.Ok(void 0));
690
+ return Future.value(Result.Error(new NonRetryableError(`RPC "${String(rpcName)}" received a message without replyTo; cannot deliver response`)));
694
691
  }
695
692
  if (typeof correlationId !== "string" || correlationId.length === 0) {
696
- this.logger?.warn("RPC handler returned a response but the incoming message has no correlationId; dropping response", {
693
+ this.logger?.error("RPC handler returned a response but the incoming message has no correlationId", {
697
694
  rpcName: String(rpcName),
698
695
  queueName,
699
696
  replyTo
700
697
  });
701
- return Future.value(Result.Ok(void 0));
698
+ return Future.value(Result.Error(new NonRetryableError(`RPC "${String(rpcName)}" received a message without correlationId; cannot deliver response`)));
702
699
  }
703
700
  let rawValidation;
704
701
  try {
@@ -713,7 +710,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
713
710
  }).flatMapOk((validatedResponse) => this.amqpClient.publish("", replyTo, validatedResponse, {
714
711
  correlationId,
715
712
  contentType: "application/json"
716
- }).mapErrorToResult((error) => Result.Error(new RetryableError("Failed to publish RPC response", error))).mapOkToResult((published) => published ? Result.Ok(void 0) : Result.Error(new RetryableError("Failed to publish RPC response: channel buffer full"))));
713
+ }).mapErrorToResult((error) => Result.Error(new NonRetryableError("Failed to publish RPC response", error))).mapOkToResult((published) => published ? Result.Ok(void 0) : Result.Error(new NonRetryableError("Failed to publish RPC response: channel buffer full"))));
717
714
  }
718
715
  /**
719
716
  * Process a single consumed message: validate, invoke handler, optionally
@@ -726,8 +723,17 @@ var TypedAmqpWorker = class TypedAmqpWorker {
726
723
  const span = startConsumeSpan(this.telemetry, queueName, String(name), { "messaging.rabbitmq.message.delivery_tag": msg.fields.deliveryTag });
727
724
  let messageHandled = false;
728
725
  let firstError;
729
- return this.parseAndValidateMessage(msg, consumer, name).flatMapOk((validatedMessage) => handler(validatedMessage, msg).flatMapOk((handlerResponse) => {
730
- if (isRpc && responseSchema) return this.publishRpcResponse(msg, queueName, name, responseSchema, handlerResponse).flatMapOk(() => {
726
+ return this.parseAndValidateMessage(msg, consumer, name).flatMap((parseResult) => parseResult.match({
727
+ Ok: (validatedMessage) => handler(validatedMessage, msg).flatMapOk((handlerResponse) => {
728
+ if (isRpc && responseSchema) return this.publishRpcResponse(msg, queueName, name, responseSchema, handlerResponse).flatMapOk(() => {
729
+ this.logger?.info("Message consumed successfully", {
730
+ consumerName: String(name),
731
+ queueName
732
+ });
733
+ this.amqpClient.ack(msg);
734
+ messageHandled = true;
735
+ return Future.value(Result.Ok(void 0));
736
+ });
731
737
  this.logger?.info("Message consumed successfully", {
732
738
  consumerName: String(name),
733
739
  queueName
@@ -735,26 +741,29 @@ var TypedAmqpWorker = class TypedAmqpWorker {
735
741
  this.amqpClient.ack(msg);
736
742
  messageHandled = true;
737
743
  return Future.value(Result.Ok(void 0));
738
- });
739
- this.logger?.info("Message consumed successfully", {
740
- consumerName: String(name),
741
- queueName
742
- });
743
- this.amqpClient.ack(msg);
744
- messageHandled = true;
745
- return Future.value(Result.Ok(void 0));
746
- }).flatMapError((handlerError) => {
747
- this.logger?.error("Error processing message", {
748
- consumerName: String(name),
749
- queueName,
750
- errorType: handlerError.name,
751
- error: handlerError.message
752
- });
753
- firstError = handlerError;
754
- return handleError({
755
- amqpClient: this.amqpClient,
756
- logger: this.logger
757
- }, handlerError, msg, String(name), consumer);
744
+ }).flatMapError((handlerError) => {
745
+ this.logger?.error("Error processing message", {
746
+ consumerName: String(name),
747
+ queueName,
748
+ errorType: handlerError.name,
749
+ error: handlerError.message
750
+ });
751
+ firstError = handlerError;
752
+ return handleError({
753
+ amqpClient: this.amqpClient,
754
+ logger: this.logger
755
+ }, handlerError, msg, String(name), consumer);
756
+ }),
757
+ Error: (parseError) => {
758
+ firstError = parseError;
759
+ this.logger?.error("Failed to parse/validate message; sending to DLQ", {
760
+ consumerName: String(name),
761
+ queueName,
762
+ error: parseError
763
+ });
764
+ this.amqpClient.nack(msg, false, false);
765
+ return Future.value(Result.Error(parseError));
766
+ }
758
767
  })).map((result) => {
759
768
  const durationMs = Date.now() - startTime;
760
769
  if (messageHandled) {
@@ -780,7 +789,16 @@ var TypedAmqpWorker = class TypedAmqpWorker {
780
789
  });
781
790
  return;
782
791
  }
783
- await this.processMessage(msg, view, name, handler).toPromise();
792
+ try {
793
+ await this.processMessage(msg, view, name, handler).toPromise();
794
+ } catch (error) {
795
+ this.logger?.error("Uncaught error in consume callback; nacking message", {
796
+ consumerName: String(name),
797
+ queueName,
798
+ error
799
+ });
800
+ this.amqpClient.nack(msg, false, false);
801
+ }
784
802
  }, this.consumerOptions[name]).tapOk((consumerTag) => {
785
803
  this.consumerTags.add(consumerTag);
786
804
  }).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(name)}"`, error)).mapOk(() => void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/decompression.ts","../src/errors.ts","../src/retry.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":["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","export { MessageValidationError } from \"@amqp-contract/core\";\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\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Type guard to check if an error is a RetryableError.\n *\n * Use this to check error types in catch blocks or error handlers.\n *\n * @param error - The error to check\n * @returns True if the error is a RetryableError\n *\n * @example\n * ```typescript\n * import { isRetryableError } from '@amqp-contract/worker';\n *\n * try {\n * await processMessage();\n * } catch (error) {\n * if (isRetryableError(error)) {\n * console.log('Will retry:', error.message);\n * } else {\n * console.log('Permanent failure:', error);\n * }\n * }\n * ```\n */\nexport function isRetryableError(error: unknown): error is RetryableError {\n return error instanceof RetryableError;\n}\n\n/**\n * Type guard to check if an error is a NonRetryableError.\n *\n * Use this to check error types in catch blocks or error handlers.\n *\n * @param error - The error to check\n * @returns True if the error is a NonRetryableError\n *\n * @example\n * ```typescript\n * import { isNonRetryableError } from '@amqp-contract/worker';\n *\n * try {\n * await processMessage();\n * } catch (error) {\n * if (isNonRetryableError(error)) {\n * console.log('Will not retry:', error.message);\n * }\n * }\n * ```\n */\nexport function isNonRetryableError(error: unknown): error is NonRetryableError {\n return error instanceof NonRetryableError;\n}\n\n/**\n * Type guard to check if an error is any HandlerError (RetryableError or NonRetryableError).\n *\n * @param error - The error to check\n * @returns True if the error is a HandlerError\n *\n * @example\n * ```typescript\n * import { isHandlerError } from '@amqp-contract/worker';\n *\n * function handleError(error: unknown) {\n * if (isHandlerError(error)) {\n * // error is RetryableError | NonRetryableError\n * console.log('Handler error:', error.name, error.message);\n * }\n * }\n * ```\n */\nexport function isHandlerError(error: unknown): error is HandlerError {\n return isRetryableError(error) || isNonRetryableError(error);\n}\n\n// =============================================================================\n// Factory Functions\n// =============================================================================\n\n/**\n * Create a RetryableError with less verbosity.\n *\n * This is a shorthand factory function for creating RetryableError instances.\n * Use it for cleaner error creation in handlers.\n *\n * @param message - Error message describing the failure\n * @param cause - Optional underlying error that caused this failure\n * @returns A new RetryableError instance\n *\n * @example\n * ```typescript\n * import { retryable } from '@amqp-contract/worker';\n * import { Future, Result } from '@swan-io/boxed';\n *\n * const handler = ({ payload }) =>\n * Future.fromPromise(processPayment(payload))\n * .mapOk(() => undefined)\n * .mapError((e) => retryable('Payment service unavailable', e));\n *\n * // Equivalent to:\n * // .mapError((e) => new RetryableError('Payment service unavailable', e));\n * ```\n */\nexport function retryable(message: string, cause?: unknown): RetryableError {\n return new RetryableError(message, cause);\n}\n\n/**\n * Create a NonRetryableError with less verbosity.\n *\n * This is a shorthand factory function for creating NonRetryableError instances.\n * Use it for cleaner error creation in handlers.\n *\n * @param message - Error message describing the failure\n * @param cause - Optional underlying error that caused this failure\n * @returns A new NonRetryableError instance\n *\n * @example\n * ```typescript\n * import { nonRetryable } from '@amqp-contract/worker';\n * import { Future, Result } from '@swan-io/boxed';\n *\n * const handler = ({ payload }) => {\n * if (!isValidPayload(payload)) {\n * return Future.value(Result.Error(nonRetryable('Invalid payload format')));\n * }\n * return Future.value(Result.Ok(undefined));\n * };\n *\n * // Equivalent to:\n * // return Future.value(Result.Error(new NonRetryableError('Invalid payload format')));\n * ```\n */\nexport function nonRetryable(message: string, cause?: unknown): NonRetryableError {\n return new NonRetryableError(message, cause);\n}\n","import {\n type ConsumerDefinition,\n type ResolvedImmediateRequeueRetryOptions,\n type ResolvedTtlBackoffRetryOptions,\n extractQueue,\n isQueueWithTtlBackoffInfrastructure,\n} from \"@amqp-contract/contract\";\nimport { type AmqpClient, type Logger, TechnicalError } from \"@amqp-contract/core\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport type { ConsumeMessage } from \"amqplib\";\nimport { NonRetryableError } from \"./errors.js\";\n\ntype RetryContext = {\n amqpClient: AmqpClient;\n logger?: Logger | undefined;\n};\n\n/**\n * Handle error in message processing with retry logic.\n *\n * Flow depends on retry mode:\n *\n * **immediate-requeue mode:**\n * 1. If NonRetryableError -> send directly to DLQ (no retry)\n * 2. If max retries exceeded -> send to DLQ\n * 3. Otherwise -> requeue immediately for retry\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 * **none mode (no retry config):**\n * 1. send directly to DLQ (no retry)\n */\nexport function handleError(\n ctx: RetryContext,\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 ctx.logger?.error(\"Non-retryable error, sending to DLQ immediately\", {\n consumerName,\n errorType: error.name,\n error: error.message,\n });\n sendToDLQ(ctx, 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 = extractQueue(consumer.queue).retry;\n\n // Immediate-requeue mode: requeue the message immediately\n if (config.mode === \"immediate-requeue\") {\n return handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, config);\n }\n\n // TTL-backoff mode: use wait queue with exponential backoff\n if (config.mode === \"ttl-backoff\") {\n return handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config);\n }\n\n // None mode: no retry, send directly to DLQ or reject\n ctx.logger?.warn(\"Retry disabled (none mode), sending to DLQ\", {\n consumerName,\n error: error.message,\n });\n sendToDLQ(ctx, msg, consumer);\n return Future.value(Result.Ok(undefined));\n}\n\n/**\n * Handle error by requeuing immediately.\n *\n * For quorum queues, messages are requeued with `nack(requeue=true)`, and the worker tracks delivery count via the native RabbitMQ `x-delivery-count` header.\n * For classic queues, messages are re-published on the same queue, and the worker tracks delivery count via a custom `x-retry-count` header.\n * When the count exceeds `maxRetries`, the message is automatically dead-lettered (if DLX is configured) or dropped.\n *\n * This is simpler than TTL-based retry but provides immediate retries only.\n */\nfunction handleErrorImmediateRequeue(\n ctx: RetryContext,\n error: Error,\n msg: ConsumeMessage,\n consumerName: string,\n consumer: ConsumerDefinition,\n config: ResolvedImmediateRequeueRetryOptions,\n): Future<Result<void, TechnicalError>> {\n const queue = extractQueue(consumer.queue);\n const queueName = queue.name;\n\n // Get retry count from headers\n // For quorum queues, the header x-delivery-count is automatically incremented on each delivery attempt\n // For classic queues, the header x-retry-count is manually incremented by the worker when re-publishing messages\n const retryCount =\n queue.type === \"quorum\"\n ? ((msg.properties.headers?.[\"x-delivery-count\"] as number) ?? 0)\n : ((msg.properties.headers?.[\"x-retry-count\"] as number) ?? 0);\n\n // Max retries exceeded -> DLQ\n if (retryCount >= config.maxRetries) {\n ctx.logger?.error(\"Max retries exceeded, sending to DLQ (immediate-requeue mode)\", {\n consumerName,\n queueName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n sendToDLQ(ctx, msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n ctx.logger?.warn(\"Retrying message (immediate-requeue mode)\", {\n consumerName,\n queueName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n\n if (queue.type === \"quorum\") {\n // For quorum queues, nack with requeue=true to trigger native retry mechanism\n ctx.amqpClient.nack(msg, false, true);\n return Future.value(Result.Ok(undefined));\n } else {\n // For classic queues, re-publish the message to the same exchange / routing key immediately with an incremented x-retry-count header\n return publishForRetry(ctx, {\n msg,\n exchange: msg.fields.exchange,\n routingKey: msg.fields.routingKey,\n queueName,\n error,\n });\n }\n}\n\n/**\n * Handle error using TTL + wait queue pattern for exponential backoff.\n *\n * ┌─────────────────────────────────────────────────────────────────┐\n * │ Retry Flow (Native RabbitMQ TTL + Wait queue pattern) │\n * ├─────────────────────────────────────────────────────────────────┤\n * │ │\n * │ 1. Handler throws any Error │\n * │ ↓ │\n * │ 2. Worker publishes to wait exchange |\n * | (with header `x-wait-queue` set to the wait queue name) │\n * │ ↓ │\n * │ 3. Wait exchange routes to wait queue │\n * │ (with expiration: calculated backoff delay) │\n * │ ↓ │\n * │ 4. Message waits in queue until TTL expires │\n * │ ↓ │\n * │ 5. Expired message dead-lettered to retry exchange |\n * | (with header `x-retry-queue` set to the main queue name) │\n * │ ↓ │\n * │ 6. Retry exchange routes back to main queue → RETRY │\n * │ ↓ │\n * │ 7. If retries exhausted: nack without requeue → DLQ │\n * │ │\n * └─────────────────────────────────────────────────────────────────┘\n */\nfunction handleErrorTtlBackoff(\n ctx: RetryContext,\n error: Error,\n msg: ConsumeMessage,\n consumerName: string,\n consumer: ConsumerDefinition,\n config: ResolvedTtlBackoffRetryOptions,\n): Future<Result<void, TechnicalError>> {\n if (!isQueueWithTtlBackoffInfrastructure(consumer.queue)) {\n ctx.logger?.error(\"Queue does not have TTL-backoff infrastructure\", {\n consumerName,\n queueName: consumer.queue.name,\n });\n return Future.value(\n Result.Error(new TechnicalError(\"Queue does not have TTL-backoff infrastructure\")),\n );\n }\n\n const queueEntry = consumer.queue;\n const queue = extractQueue(queueEntry);\n const queueName = queue.name;\n\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 ctx.logger?.error(\"Max retries exceeded, sending to DLQ (ttl-backoff mode)\", {\n consumerName,\n queueName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n sendToDLQ(ctx, msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n // Retry with exponential backoff\n const delayMs = calculateRetryDelay(retryCount, config);\n ctx.logger?.warn(\"Retrying message (ttl-backoff mode)\", {\n consumerName,\n queueName,\n retryCount: retryCount + 1,\n maxRetries: config.maxRetries,\n delayMs,\n error: error.message,\n });\n\n // Re-publish the message to the wait exchange with TTL and incremented x-retry-count header\n return publishForRetry(ctx, {\n msg,\n exchange: queueEntry.waitExchange.name,\n routingKey: msg.fields.routingKey, // Preserve original routing key\n waitQueueName: queueEntry.waitQueue.name,\n queueName,\n delayMs,\n error,\n });\n}\n\n/**\n * Calculate retry delay with exponential backoff and optional jitter.\n */\nfunction 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 */\nfunction parseMessageContentForRetry(\n ctx: RetryContext,\n msg: ConsumeMessage,\n queueName: string,\n): 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 ctx.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 with an incremented x-retry-count header and optional TTL.\n */\nfunction publishForRetry(\n ctx: RetryContext,\n {\n msg,\n exchange,\n routingKey,\n queueName,\n waitQueueName,\n delayMs,\n error,\n }: {\n msg: ConsumeMessage;\n exchange: string;\n routingKey: string;\n queueName: string;\n waitQueueName?: string;\n delayMs?: number;\n error: Error;\n },\n): Future<Result<void, TechnicalError>> {\n // Get retry count from headers\n const retryCount = (msg.properties.headers?.[\"x-retry-count\"] as number) ?? 0;\n const newRetryCount = retryCount + 1;\n\n // Acknowledge original message\n ctx.amqpClient.ack(msg);\n\n const content = parseMessageContentForRetry(ctx, msg, queueName);\n\n // Publish message with incremented x-retry-count header and original error info\n return ctx.amqpClient\n .publish(exchange, routingKey, content, {\n ...msg.properties,\n ...(delayMs !== undefined ? { 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 ...(waitQueueName !== undefined\n ? {\n \"x-wait-queue\": waitQueueName, // For wait exchange routing\n \"x-retry-queue\": queueName, // For retry exchange routing\n }\n : {}),\n },\n })\n .mapOkToResult((published) => {\n if (!published) {\n ctx.logger?.error(\"Failed to publish message for retry (write buffer full)\", {\n queueName,\n retryCount: newRetryCount,\n ...(delayMs !== undefined ? { delayMs } : {}),\n });\n return Result.Error(\n new TechnicalError(\"Failed to publish message for retry (write buffer full)\"),\n );\n }\n\n ctx.logger?.info(\"Message published for retry\", {\n queueName,\n retryCount: newRetryCount,\n ...(delayMs !== undefined ? { 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 */\nfunction sendToDLQ(ctx: RetryContext, msg: ConsumeMessage, consumer: ConsumerDefinition): void {\n const queue = extractQueue(consumer.queue);\n const queueName = queue.name;\n const hasDeadLetter = queue.deadLetter !== undefined;\n\n if (!hasDeadLetter) {\n ctx.logger?.warn(\"Queue does not have DLX configured - message will be lost on nack\", {\n queueName,\n });\n }\n\n ctx.logger?.info(\"Sending message to DLQ\", {\n queueName,\n deliveryTag: msg.fields.deliveryTag,\n });\n\n // Nack without requeue - relies on DLX configuration\n ctx.amqpClient.nack(msg, false, false);\n}\n","import {\n type ConsumerDefinition,\n type ContractDefinition,\n type InferConsumerNames,\n type InferRpcNames,\n extractConsumer,\n extractQueue,\n} from \"@amqp-contract/contract\";\nimport {\n AmqpClient,\n ConsumerOptions as AmqpClientConsumerOptions,\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 { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport type { ConsumeMessage } from \"amqplib\";\nimport { decompressBuffer } from \"./decompression.js\";\nimport type { HandlerError } from \"./errors.js\";\nimport { MessageValidationError, NonRetryableError, RetryableError } from \"./errors.js\";\nimport { handleError } from \"./retry.js\";\nimport type { WorkerInferHandlers } from \"./types.js\";\n\n/**\n * Either a regular consumer name or an RPC name from the contract.\n */\ntype HandlerName<TContract extends ContractDefinition> =\n | InferConsumerNames<TContract>\n | InferRpcNames<TContract>;\n\n/**\n * Resolved handler entry stored on the worker, regardless of whether the\n * source is a `consumers` or `rpcs` slot. The handler signature is widened\n * here because both kinds share the same dispatch loop; specific call sites\n * cast back to the correct typed handler.\n */\ntype StoredHandler = (\n message: { payload: unknown; headers: unknown },\n rawMessage: ConsumeMessage,\n) => Future<Result<unknown, HandlerError>>;\n\nexport type ConsumerOptions = AmqpClientConsumerOptions;\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 * defaultConsumerOptions: {\n * prefetch: 5,\n * },\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 `consumers` and `rpcs` entry in the contract.\n *\n * - Regular consumers return `Future<Result<void, HandlerError>>`.\n * - RPC handlers return `Future<Result<TResponse, HandlerError>>` where\n * `TResponse` is inferred from the RPC's response message schema.\n *\n * Use `defineHandler` / `defineHandlers` to create handlers with full type\n * inference.\n */\n handlers: WorkerInferHandlers<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 * Optional default consumer options applied to all consumer handlers.\n * Handler-specific options provided in tuple form override these defaults.\n */\n defaultConsumerOptions?: ConsumerOptions | undefined;\n /**\n * Maximum time in ms to wait for the AMQP connection to become ready before\n * `create()` resolves to `Result.Error<TechnicalError>`. Without this option,\n * `create()` waits forever — the underlying amqp-connection-manager retries\n * indefinitely.\n */\n connectTimeoutMs?: number | 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 { defineQueue, defineMessage, defineContract, defineConsumer } from '@amqp-contract/contract';\n * import { z } from 'zod';\n *\n * const orderQueue = defineQueue('order-processing');\n * const orderMessage = defineMessage(z.object({\n * orderId: z.string(),\n * amount: z.number()\n * }));\n *\n * const contract = defineContract({\n * consumers: {\n * processOrder: defineConsumer(orderQueue, orderMessage)\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. Keyed by handler name (consumer or RPC); the\n * stored function signature is widened so the dispatch loop can call it\n * uniformly. The actual handler is type-checked at the worker's public API\n * boundary via `WorkerInferHandlers<TContract>`.\n */\n private readonly actualHandlers: Partial<Record<HandlerName<TContract>, StoredHandler>>;\n private readonly consumerOptions: Partial<Record<HandlerName<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: WorkerInferHandlers<TContract>,\n private readonly defaultConsumerOptions: ConsumerOptions,\n private readonly logger?: Logger,\n telemetry?: TelemetryProvider,\n ) {\n this.telemetry = telemetry ?? defaultTelemetryProvider;\n\n this.actualHandlers = {};\n this.consumerOptions = {};\n\n const handlersRecord = handlers as Record<string, unknown>;\n\n for (const handlerName of Object.keys(handlersRecord)) {\n const handlerEntry = handlersRecord[handlerName];\n const typedName = handlerName as HandlerName<TContract>;\n\n if (isHandlerTuple(handlerEntry)) {\n const [handler, options] = handlerEntry;\n this.actualHandlers[typedName] = handler as StoredHandler;\n this.consumerOptions[typedName] = {\n ...this.defaultConsumerOptions,\n ...options,\n };\n } else {\n this.actualHandlers[typedName] = handlerEntry as StoredHandler;\n this.consumerOptions[typedName] = this.defaultConsumerOptions;\n }\n }\n }\n\n /**\n * Build a `ConsumerDefinition`-shaped view for a handler name, regardless\n * of whether it came from `contract.consumers` or `contract.rpcs`. The\n * dispatch path treats both uniformly; the returned `isRpc` flag (and the\n * accompanying `responseSchema`) tells `processMessage` whether to validate\n * the handler return value and publish a reply.\n */\n private resolveConsumerView(name: HandlerName<TContract>): {\n consumer: ConsumerDefinition;\n isRpc: boolean;\n responseSchema?: StandardSchemaV1;\n } {\n // Use `Object.hasOwn` rather than `key in rpcs` so prototype properties\n // (e.g. \"toString\") on a plain object are not misclassified as RPC names.\n const rpcs = this.contract.rpcs;\n if (rpcs && Object.hasOwn(rpcs, name as string)) {\n const rpc = rpcs[name as string]!;\n return {\n consumer: { queue: rpc.queue, message: rpc.request },\n isRpc: true,\n responseSchema: rpc.response.payload,\n };\n }\n const consumerEntry = this.contract.consumers![name as string]!;\n return {\n consumer: extractConsumer(consumerEntry),\n isRpc: false,\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 defaultConsumerOptions,\n logger,\n telemetry,\n connectTimeoutMs,\n }: CreateWorkerOptions<TContract>): Future<Result<TypedAmqpWorker<TContract>, TechnicalError>> {\n const worker = new TypedAmqpWorker(\n contract,\n new AmqpClient(contract, {\n urls,\n connectionOptions,\n connectTimeoutMs,\n }),\n handlers,\n defaultConsumerOptions ?? {},\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 .flatMap((result) =>\n result.match({\n Ok: () => Future.value(Result.Ok<TypedAmqpWorker<TContract>, TechnicalError>(worker)),\n // Release the AmqpClient's connection ref-count and cancel any consumers\n // that registered before the failure, so a failed create() does not leak.\n Error: (error) =>\n worker\n .close()\n .tapError((closeError) => {\n logger?.warn(\"Failed to close worker after setup failure\", {\n error: closeError,\n });\n })\n .map(() => Result.Error<TypedAmqpWorker<TContract>, TechnicalError>(error)),\n }),\n );\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 * Start consuming for every entry in `contract.consumers` and `contract.rpcs`.\n */\n private consumeAll(): Future<Result<void, TechnicalError>> {\n const consumerNames = Object.keys(\n this.contract.consumers ?? {},\n ) as InferConsumerNames<TContract>[];\n const rpcNames = Object.keys(this.contract.rpcs ?? {}) as InferRpcNames<TContract>[];\n const allNames = [...consumerNames, ...rpcNames] as HandlerName<TContract>[];\n\n return Future.all(allNames.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 handler — either a `consumers`\n * entry (regular event/command consumer) or an `rpcs` entry (RPC server).\n */\n private consume(name: HandlerName<TContract>): Future<Result<void, TechnicalError>> {\n const view = this.resolveConsumerView(name);\n // Non-null assertion safe: `WorkerInferHandlers<TContract>` requires every\n // consumers / rpcs key to have a handler, so by the time we reach this\n // dispatch path the entry exists in `actualHandlers`. Enforced by the type\n // system at the public API boundary, not by a runtime check.\n const handler = this.actualHandlers[name]!;\n\n return this.consumeSingle(name, view, handler);\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(\n msg: ConsumeMessage,\n consumer: ConsumerDefinition,\n consumerName: HandlerName<TContract>,\n ): Future<Result<{ payload: unknown; headers: unknown }, TechnicalError>> {\n const queue = extractQueue(consumer.queue);\n const context = {\n consumerName: String(consumerName),\n queueName: 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<{ payload: unknown; headers: unknown }, TechnicalError>>;\n }\n\n /**\n * Validate an RPC handler's response and publish it back to the caller's reply\n * queue with the same `correlationId`. Published via the AMQP default exchange\n * with `routingKey = msg.properties.replyTo`, which works for both\n * `amq.rabbitmq.reply-to` and any anonymous queue declared by the caller.\n *\n * Validation errors are surfaced as NonRetryableError (handler returned the\n * wrong shape — retrying the same input will not fix it). Publish errors are\n * surfaced as RetryableError so the worker's existing retry logic applies.\n */\n private publishRpcResponse(\n msg: ConsumeMessage,\n queueName: string,\n rpcName: HandlerName<TContract>,\n responseSchema: StandardSchemaV1,\n response: unknown,\n ): Future<Result<void, HandlerError>> {\n const replyTo = msg.properties.replyTo;\n const correlationId = msg.properties.correlationId;\n if (typeof replyTo !== \"string\" || replyTo.length === 0) {\n this.logger?.warn(\n \"RPC handler returned a response but the incoming message has no replyTo; dropping response\",\n { rpcName: String(rpcName), queueName },\n );\n return Future.value(Result.Ok(undefined));\n }\n if (typeof correlationId !== \"string\" || correlationId.length === 0) {\n // Without a correlationId the client cannot match the reply to its\n // pending call — publishing anyway would guarantee a client-side timeout.\n this.logger?.warn(\n \"RPC handler returned a response but the incoming message has no correlationId; dropping response\",\n { rpcName: String(rpcName), queueName, replyTo },\n );\n return Future.value(Result.Ok(undefined));\n }\n\n // Wrap the call to `validate` itself in try/catch — a Standard Schema\n // implementation may throw synchronously (not via a rejected Promise), and\n // we don't want that to crash the consume callback.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = responseSchema[\"~standard\"].validate(response);\n } catch (error: unknown) {\n return Future.value(\n Result.Error<void, HandlerError>(\n new NonRetryableError(\"RPC response schema validation threw\", error),\n ),\n );\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n\n return Future.fromPromise(validationPromise)\n .mapError(\n (error: unknown) =>\n new NonRetryableError(\"RPC response schema validation threw\", error) as HandlerError,\n )\n .mapOkToResult((validation) => {\n if (validation.issues) {\n return Result.Error<unknown, HandlerError>(\n new NonRetryableError(\n `RPC response for \"${String(rpcName)}\" failed schema validation`,\n new MessageValidationError(String(rpcName), validation.issues),\n ),\n );\n }\n return Result.Ok<unknown, HandlerError>(validation.value);\n })\n .flatMapOk((validatedResponse) =>\n this.amqpClient\n .publish(\"\", replyTo, validatedResponse, {\n correlationId,\n contentType: \"application/json\",\n })\n .mapErrorToResult((error: TechnicalError) =>\n Result.Error<void, HandlerError>(\n new RetryableError(\"Failed to publish RPC response\", error),\n ),\n )\n .mapOkToResult((published) =>\n published\n ? Result.Ok<void, HandlerError>(undefined)\n : Result.Error<void, HandlerError>(\n new RetryableError(\"Failed to publish RPC response: channel buffer full\"),\n ),\n ),\n );\n }\n\n /**\n * Process a single consumed message: validate, invoke handler, optionally\n * publish the RPC response, record telemetry, and handle errors.\n */\n private processMessage(\n msg: ConsumeMessage,\n view: { consumer: ConsumerDefinition; isRpc: boolean; responseSchema?: StandardSchemaV1 },\n name: HandlerName<TContract>,\n handler: StoredHandler,\n ): Future<Result<void, TechnicalError>> {\n const { consumer, isRpc, responseSchema } = view;\n const queueName = extractQueue(consumer.queue).name;\n const startTime = Date.now();\n const span = startConsumeSpan(this.telemetry, queueName, String(name), {\n \"messaging.rabbitmq.message.delivery_tag\": msg.fields.deliveryTag,\n });\n\n let messageHandled = false;\n let firstError: Error | undefined;\n\n return this.parseAndValidateMessage(msg, consumer, name)\n .flatMapOk((validatedMessage) =>\n handler(validatedMessage, msg)\n .flatMapOk((handlerResponse) => {\n if (isRpc && responseSchema) {\n return this.publishRpcResponse(\n msg,\n queueName,\n name,\n responseSchema,\n handlerResponse,\n ).flatMapOk(() => {\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(name),\n queueName,\n });\n this.amqpClient.ack(msg);\n messageHandled = true;\n return Future.value(Result.Ok<void, HandlerError>(undefined));\n });\n }\n\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(name),\n queueName,\n });\n this.amqpClient.ack(msg);\n messageHandled = true;\n\n return Future.value(Result.Ok<void, HandlerError>(undefined));\n })\n .flatMapError((handlerError: HandlerError) => {\n this.logger?.error(\"Error processing message\", {\n consumerName: String(name),\n queueName,\n errorType: handlerError.name,\n error: handlerError.message,\n });\n firstError = handlerError;\n\n return handleError(\n { amqpClient: this.amqpClient, logger: this.logger },\n handlerError,\n msg,\n String(name),\n consumer,\n );\n }),\n )\n .map((result) => {\n const durationMs = Date.now() - startTime;\n if (messageHandled) {\n endSpanSuccess(span);\n recordConsumeMetric(this.telemetry, queueName, String(name), true, durationMs);\n } else {\n const error = result.isError()\n ? result.error\n : (firstError ?? new Error(\"Unknown error\"));\n endSpanError(span, error);\n recordConsumeMetric(this.telemetry, queueName, String(name), false, durationMs);\n }\n return result;\n });\n }\n\n /**\n * Consume messages one at a time.\n */\n private consumeSingle(\n name: HandlerName<TContract>,\n view: { consumer: ConsumerDefinition; isRpc: boolean; responseSchema?: StandardSchemaV1 },\n handler: StoredHandler,\n ): Future<Result<void, TechnicalError>> {\n const queueName = extractQueue(view.consumer.queue).name;\n\n return this.amqpClient\n .consume(\n queueName,\n async (msg) => {\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(name),\n queueName,\n });\n return;\n }\n await this.processMessage(msg, view, name, handler).toPromise();\n },\n this.consumerOptions[name],\n )\n .tapOk((consumerTag) => {\n this.consumerTags.add(consumerTag);\n })\n .mapError(\n (error) => new TechnicalError(`Failed to start consuming for \"${String(name)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n}\n","import type { ContractDefinition, InferConsumerNames } from \"@amqp-contract/contract\";\nimport type {\n WorkerInferConsumerHandler,\n WorkerInferConsumerHandlerEntry,\n WorkerInferConsumerHandlers,\n} from \"./types.js\";\nimport { ConsumerOptions } from \"./worker.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: object,\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// Handler Definitions\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: WorkerInferConsumerHandler<TContract, TName>,\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n options: ConsumerOptions,\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n options?: ConsumerOptions,\n): WorkerInferConsumerHandlerEntry<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: WorkerInferConsumerHandlers<TContract>,\n): WorkerInferConsumerHandlers<TContract> {\n validateHandlers(contract, handlers);\n return handlers;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/decompression.ts","../src/errors.ts","../src/retry.ts","../src/worker.ts","../src/handlers.ts"],"sourcesContent":["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","export { MessageValidationError } from \"@amqp-contract/core\";\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\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Type guard to check if an error is a RetryableError.\n *\n * Use this to check error types in catch blocks or error handlers.\n *\n * @param error - The error to check\n * @returns True if the error is a RetryableError\n *\n * @example\n * ```typescript\n * import { isRetryableError } from '@amqp-contract/worker';\n *\n * try {\n * await processMessage();\n * } catch (error) {\n * if (isRetryableError(error)) {\n * console.log('Will retry:', error.message);\n * } else {\n * console.log('Permanent failure:', error);\n * }\n * }\n * ```\n */\nexport function isRetryableError(error: unknown): error is RetryableError {\n return error instanceof RetryableError;\n}\n\n/**\n * Type guard to check if an error is a NonRetryableError.\n *\n * Use this to check error types in catch blocks or error handlers.\n *\n * @param error - The error to check\n * @returns True if the error is a NonRetryableError\n *\n * @example\n * ```typescript\n * import { isNonRetryableError } from '@amqp-contract/worker';\n *\n * try {\n * await processMessage();\n * } catch (error) {\n * if (isNonRetryableError(error)) {\n * console.log('Will not retry:', error.message);\n * }\n * }\n * ```\n */\nexport function isNonRetryableError(error: unknown): error is NonRetryableError {\n return error instanceof NonRetryableError;\n}\n\n/**\n * Type guard to check if an error is any HandlerError (RetryableError or NonRetryableError).\n *\n * @param error - The error to check\n * @returns True if the error is a HandlerError\n *\n * @example\n * ```typescript\n * import { isHandlerError } from '@amqp-contract/worker';\n *\n * function handleError(error: unknown) {\n * if (isHandlerError(error)) {\n * // error is RetryableError | NonRetryableError\n * console.log('Handler error:', error.name, error.message);\n * }\n * }\n * ```\n */\nexport function isHandlerError(error: unknown): error is HandlerError {\n return isRetryableError(error) || isNonRetryableError(error);\n}\n\n// =============================================================================\n// Factory Functions\n// =============================================================================\n\n/**\n * Create a RetryableError with less verbosity.\n *\n * This is a shorthand factory function for creating RetryableError instances.\n * Use it for cleaner error creation in handlers.\n *\n * @param message - Error message describing the failure\n * @param cause - Optional underlying error that caused this failure\n * @returns A new RetryableError instance\n *\n * @example\n * ```typescript\n * import { retryable } from '@amqp-contract/worker';\n * import { Future, Result } from '@swan-io/boxed';\n *\n * const handler = ({ payload }) =>\n * Future.fromPromise(processPayment(payload))\n * .mapOk(() => undefined)\n * .mapError((e) => retryable('Payment service unavailable', e));\n *\n * // Equivalent to:\n * // .mapError((e) => new RetryableError('Payment service unavailable', e));\n * ```\n */\nexport function retryable(message: string, cause?: unknown): RetryableError {\n return new RetryableError(message, cause);\n}\n\n/**\n * Create a NonRetryableError with less verbosity.\n *\n * This is a shorthand factory function for creating NonRetryableError instances.\n * Use it for cleaner error creation in handlers.\n *\n * @param message - Error message describing the failure\n * @param cause - Optional underlying error that caused this failure\n * @returns A new NonRetryableError instance\n *\n * @example\n * ```typescript\n * import { nonRetryable } from '@amqp-contract/worker';\n * import { Future, Result } from '@swan-io/boxed';\n *\n * const handler = ({ payload }) => {\n * if (!isValidPayload(payload)) {\n * return Future.value(Result.Error(nonRetryable('Invalid payload format')));\n * }\n * return Future.value(Result.Ok(undefined));\n * };\n *\n * // Equivalent to:\n * // return Future.value(Result.Error(new NonRetryableError('Invalid payload format')));\n * ```\n */\nexport function nonRetryable(message: string, cause?: unknown): NonRetryableError {\n return new NonRetryableError(message, cause);\n}\n","import {\n type ConsumerDefinition,\n type ResolvedImmediateRequeueRetryOptions,\n type ResolvedTtlBackoffRetryOptions,\n extractQueue,\n isQueueWithTtlBackoffInfrastructure,\n} from \"@amqp-contract/contract\";\nimport { type AmqpClient, type Logger, TechnicalError } from \"@amqp-contract/core\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport type { ConsumeMessage } from \"amqplib\";\nimport { NonRetryableError } from \"./errors.js\";\n\ntype RetryContext = {\n amqpClient: AmqpClient;\n logger?: Logger | undefined;\n};\n\n/**\n * Handle error in message processing with retry logic.\n *\n * Flow depends on retry mode:\n *\n * **immediate-requeue mode:**\n * 1. If NonRetryableError -> send directly to DLQ (no retry)\n * 2. If max retries exceeded -> send to DLQ\n * 3. Otherwise -> requeue immediately for retry\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 * **none mode (no retry config):**\n * 1. send directly to DLQ (no retry)\n */\nexport function handleError(\n ctx: RetryContext,\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 ctx.logger?.error(\"Non-retryable error, sending to DLQ immediately\", {\n consumerName,\n errorType: error.name,\n error: error.message,\n });\n sendToDLQ(ctx, 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 = extractQueue(consumer.queue).retry;\n\n // Immediate-requeue mode: requeue the message immediately\n if (config.mode === \"immediate-requeue\") {\n return handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, config);\n }\n\n // TTL-backoff mode: use wait queue with exponential backoff\n if (config.mode === \"ttl-backoff\") {\n return handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config);\n }\n\n // None mode: no retry, send directly to DLQ or reject\n ctx.logger?.warn(\"Retry disabled (none mode), sending to DLQ\", {\n consumerName,\n error: error.message,\n });\n sendToDLQ(ctx, msg, consumer);\n return Future.value(Result.Ok(undefined));\n}\n\n/**\n * Handle error by requeuing immediately.\n *\n * For quorum queues, messages are requeued with `nack(requeue=true)`, and the worker tracks delivery count via the native RabbitMQ `x-delivery-count` header.\n * For classic queues, messages are re-published on the same queue, and the worker tracks delivery count via a custom `x-retry-count` header.\n * When the count exceeds `maxRetries`, the message is automatically dead-lettered (if DLX is configured) or dropped.\n *\n * This is simpler than TTL-based retry but provides immediate retries only.\n */\nfunction handleErrorImmediateRequeue(\n ctx: RetryContext,\n error: Error,\n msg: ConsumeMessage,\n consumerName: string,\n consumer: ConsumerDefinition,\n config: ResolvedImmediateRequeueRetryOptions,\n): Future<Result<void, TechnicalError>> {\n const queue = extractQueue(consumer.queue);\n const queueName = queue.name;\n\n // Get retry count from headers\n // For quorum queues, the header x-delivery-count is automatically incremented on each delivery attempt\n // For classic queues, the header x-retry-count is manually incremented by the worker when re-publishing messages\n const retryCount =\n queue.type === \"quorum\"\n ? ((msg.properties.headers?.[\"x-delivery-count\"] as number) ?? 0)\n : ((msg.properties.headers?.[\"x-retry-count\"] as number) ?? 0);\n\n // Max retries exceeded -> DLQ\n if (retryCount >= config.maxRetries) {\n ctx.logger?.error(\"Max retries exceeded, sending to DLQ (immediate-requeue mode)\", {\n consumerName,\n queueName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n sendToDLQ(ctx, msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n ctx.logger?.warn(\"Retrying message (immediate-requeue mode)\", {\n consumerName,\n queueName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n\n if (queue.type === \"quorum\") {\n // For quorum queues, nack with requeue=true to trigger native retry mechanism\n ctx.amqpClient.nack(msg, false, true);\n return Future.value(Result.Ok(undefined));\n } else {\n // For classic queues, re-publish the message to the same exchange / routing key immediately with an incremented x-retry-count header\n return publishForRetry(ctx, {\n msg,\n exchange: msg.fields.exchange,\n routingKey: msg.fields.routingKey,\n queueName,\n error,\n });\n }\n}\n\n/**\n * Handle error using TTL + wait queue pattern for exponential backoff.\n *\n * ┌─────────────────────────────────────────────────────────────────┐\n * │ Retry Flow (Native RabbitMQ TTL + Wait queue pattern) │\n * ├─────────────────────────────────────────────────────────────────┤\n * │ │\n * │ 1. Handler throws any Error │\n * │ ↓ │\n * │ 2. Worker publishes to wait exchange |\n * | (with header `x-wait-queue` set to the wait queue name) │\n * │ ↓ │\n * │ 3. Wait exchange routes to wait queue │\n * │ (with expiration: calculated backoff delay) │\n * │ ↓ │\n * │ 4. Message waits in queue until TTL expires │\n * │ ↓ │\n * │ 5. Expired message dead-lettered to retry exchange |\n * | (with header `x-retry-queue` set to the main queue name) │\n * │ ↓ │\n * │ 6. Retry exchange routes back to main queue → RETRY │\n * │ ↓ │\n * │ 7. If retries exhausted: nack without requeue → DLQ │\n * │ │\n * └─────────────────────────────────────────────────────────────────┘\n */\nfunction handleErrorTtlBackoff(\n ctx: RetryContext,\n error: Error,\n msg: ConsumeMessage,\n consumerName: string,\n consumer: ConsumerDefinition,\n config: ResolvedTtlBackoffRetryOptions,\n): Future<Result<void, TechnicalError>> {\n if (!isQueueWithTtlBackoffInfrastructure(consumer.queue)) {\n ctx.logger?.error(\"Queue does not have TTL-backoff infrastructure\", {\n consumerName,\n queueName: consumer.queue.name,\n });\n return Future.value(\n Result.Error(new TechnicalError(\"Queue does not have TTL-backoff infrastructure\")),\n );\n }\n\n const queueEntry = consumer.queue;\n const queue = extractQueue(queueEntry);\n const queueName = queue.name;\n\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 ctx.logger?.error(\"Max retries exceeded, sending to DLQ (ttl-backoff mode)\", {\n consumerName,\n queueName,\n retryCount,\n maxRetries: config.maxRetries,\n error: error.message,\n });\n sendToDLQ(ctx, msg, consumer);\n return Future.value(Result.Ok(undefined));\n }\n\n // Retry with exponential backoff\n const delayMs = calculateRetryDelay(retryCount, config);\n ctx.logger?.warn(\"Retrying message (ttl-backoff mode)\", {\n consumerName,\n queueName,\n retryCount: retryCount + 1,\n maxRetries: config.maxRetries,\n delayMs,\n error: error.message,\n });\n\n // Re-publish the message to the wait exchange with TTL and incremented x-retry-count header\n return publishForRetry(ctx, {\n msg,\n exchange: queueEntry.waitExchange.name,\n routingKey: msg.fields.routingKey, // Preserve original routing key\n waitQueueName: queueEntry.waitQueue.name,\n queueName,\n delayMs,\n error,\n });\n}\n\n/**\n * Calculate retry delay with exponential backoff and optional jitter.\n */\nfunction 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 *\n * The channel is configured with `json: true`, so values published as plain\n * objects are encoded once at publish time. Re-publishing the raw `Buffer`\n * would then trigger a *second* JSON.stringify (turning the bytes into a\n * stringified base64 blob), so for JSON payloads we must round-trip back to\n * the parsed value. For any other content type — or when the message is\n * compressed — we pass the bytes through untouched, since re-parsing would\n * either fail or silently corrupt binary data.\n */\nfunction parseMessageContentForRetry(\n ctx: RetryContext,\n msg: ConsumeMessage,\n queueName: string,\n): Buffer | unknown {\n if (msg.properties.contentEncoding) {\n // Compressed (gzip, brotli, …) — opaque to us; keep the buffer as-is so\n // the consumer's decompressor sees the same bytes the producer sent.\n return msg.content;\n }\n\n const contentType = msg.properties.contentType;\n const isJson =\n contentType === undefined ||\n contentType === \"application/json\" ||\n contentType.startsWith(\"application/json;\") ||\n contentType.endsWith(\"+json\");\n\n if (!isJson) {\n // Binary or other text payload — preserve bytes exactly.\n return msg.content;\n }\n\n try {\n return JSON.parse(msg.content.toString());\n } catch (err) {\n ctx.logger?.warn(\"Failed to parse JSON message for retry, using original buffer\", {\n queueName,\n error: err,\n });\n return msg.content;\n }\n}\n\n/**\n * Publish message with an incremented x-retry-count header and optional TTL.\n */\nfunction publishForRetry(\n ctx: RetryContext,\n {\n msg,\n exchange,\n routingKey,\n queueName,\n waitQueueName,\n delayMs,\n error,\n }: {\n msg: ConsumeMessage;\n exchange: string;\n routingKey: string;\n queueName: string;\n waitQueueName?: string;\n delayMs?: number;\n error: Error;\n },\n): Future<Result<void, TechnicalError>> {\n // Get retry count from headers\n const retryCount = (msg.properties.headers?.[\"x-retry-count\"] as number) ?? 0;\n const newRetryCount = retryCount + 1;\n\n // Acknowledge original message\n ctx.amqpClient.ack(msg);\n\n const content = parseMessageContentForRetry(ctx, msg, queueName);\n\n // Publish message with incremented x-retry-count header and original error info\n return ctx.amqpClient\n .publish(exchange, routingKey, content, {\n ...msg.properties,\n ...(delayMs !== undefined ? { 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 ...(waitQueueName !== undefined\n ? {\n \"x-wait-queue\": waitQueueName, // For wait exchange routing\n \"x-retry-queue\": queueName, // For retry exchange routing\n }\n : {}),\n },\n })\n .mapOkToResult((published) => {\n if (!published) {\n ctx.logger?.error(\"Failed to publish message for retry (write buffer full)\", {\n queueName,\n retryCount: newRetryCount,\n ...(delayMs !== undefined ? { delayMs } : {}),\n });\n return Result.Error(\n new TechnicalError(\"Failed to publish message for retry (write buffer full)\"),\n );\n }\n\n ctx.logger?.info(\"Message published for retry\", {\n queueName,\n retryCount: newRetryCount,\n ...(delayMs !== undefined ? { 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 */\nfunction sendToDLQ(ctx: RetryContext, msg: ConsumeMessage, consumer: ConsumerDefinition): void {\n const queue = extractQueue(consumer.queue);\n const queueName = queue.name;\n const hasDeadLetter = queue.deadLetter !== undefined;\n\n if (!hasDeadLetter) {\n ctx.logger?.warn(\"Queue does not have DLX configured - message will be lost on nack\", {\n queueName,\n });\n }\n\n ctx.logger?.info(\"Sending message to DLQ\", {\n queueName,\n deliveryTag: msg.fields.deliveryTag,\n });\n\n // Nack without requeue - relies on DLX configuration\n ctx.amqpClient.nack(msg, false, false);\n}\n","import {\n type ConsumerDefinition,\n type ContractDefinition,\n type InferConsumerNames,\n type InferRpcNames,\n extractConsumer,\n extractQueue,\n} from \"@amqp-contract/contract\";\nimport {\n AmqpClient,\n ConsumerOptions as AmqpClientConsumerOptions,\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 { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { Future, Result } from \"@swan-io/boxed\";\nimport type { AmqpConnectionManagerOptions, ConnectionUrl } from \"amqp-connection-manager\";\nimport type { ConsumeMessage } from \"amqplib\";\nimport { decompressBuffer } from \"./decompression.js\";\nimport type { HandlerError } from \"./errors.js\";\nimport { MessageValidationError, NonRetryableError } from \"./errors.js\";\nimport { handleError } from \"./retry.js\";\nimport type { WorkerInferHandlers } from \"./types.js\";\n\n/**\n * Either a regular consumer name or an RPC name from the contract.\n */\ntype HandlerName<TContract extends ContractDefinition> =\n | InferConsumerNames<TContract>\n | InferRpcNames<TContract>;\n\n/**\n * Resolved handler entry stored on the worker, regardless of whether the\n * source is a `consumers` or `rpcs` slot. The handler signature is widened\n * here because both kinds share the same dispatch loop; specific call sites\n * cast back to the correct typed handler.\n */\ntype StoredHandler = (\n message: { payload: unknown; headers: unknown },\n rawMessage: ConsumeMessage,\n) => Future<Result<unknown, HandlerError>>;\n\nexport type ConsumerOptions = AmqpClientConsumerOptions;\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 * defaultConsumerOptions: {\n * prefetch: 5,\n * },\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 `consumers` and `rpcs` entry in the contract.\n *\n * - Regular consumers return `Future<Result<void, HandlerError>>`.\n * - RPC handlers return `Future<Result<TResponse, HandlerError>>` where\n * `TResponse` is inferred from the RPC's response message schema.\n *\n * Use `defineHandler` / `defineHandlers` to create handlers with full type\n * inference.\n */\n handlers: WorkerInferHandlers<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 * Optional default consumer options applied to all consumer handlers.\n * Handler-specific options provided in tuple form override these defaults.\n */\n defaultConsumerOptions?: ConsumerOptions | undefined;\n /**\n * Maximum time in ms to wait for the AMQP connection to become ready before\n * `create()` resolves to `Result.Error<TechnicalError>`. Defaults to 30s\n * (the {@link AmqpClient}'s `DEFAULT_CONNECT_TIMEOUT_MS`). Pass `null` to\n * disable the timeout and let amqp-connection-manager retry indefinitely.\n */\n connectTimeoutMs?: number | null | 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 { defineQueue, defineMessage, defineContract, defineConsumer } from '@amqp-contract/contract';\n * import { z } from 'zod';\n *\n * const orderQueue = defineQueue('order-processing');\n * const orderMessage = defineMessage(z.object({\n * orderId: z.string(),\n * amount: z.number()\n * }));\n *\n * const contract = defineContract({\n * consumers: {\n * processOrder: defineConsumer(orderQueue, orderMessage)\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. Keyed by handler name (consumer or RPC); the\n * stored function signature is widened so the dispatch loop can call it\n * uniformly. The actual handler is type-checked at the worker's public API\n * boundary via `WorkerInferHandlers<TContract>`.\n */\n private readonly actualHandlers: Partial<Record<HandlerName<TContract>, StoredHandler>>;\n private readonly consumerOptions: Partial<Record<HandlerName<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: WorkerInferHandlers<TContract>,\n private readonly defaultConsumerOptions: ConsumerOptions,\n private readonly logger?: Logger,\n telemetry?: TelemetryProvider,\n ) {\n this.telemetry = telemetry ?? defaultTelemetryProvider;\n\n this.actualHandlers = {};\n this.consumerOptions = {};\n\n const handlersRecord = handlers as Record<string, unknown>;\n\n for (const handlerName of Object.keys(handlersRecord)) {\n const handlerEntry = handlersRecord[handlerName];\n const typedName = handlerName as HandlerName<TContract>;\n\n if (isHandlerTuple(handlerEntry)) {\n const [handler, options] = handlerEntry;\n this.actualHandlers[typedName] = handler as StoredHandler;\n this.consumerOptions[typedName] = {\n ...this.defaultConsumerOptions,\n ...options,\n };\n } else {\n this.actualHandlers[typedName] = handlerEntry as StoredHandler;\n this.consumerOptions[typedName] = this.defaultConsumerOptions;\n }\n }\n }\n\n /**\n * Build a `ConsumerDefinition`-shaped view for a handler name, regardless\n * of whether it came from `contract.consumers` or `contract.rpcs`. The\n * dispatch path treats both uniformly; the returned `isRpc` flag (and the\n * accompanying `responseSchema`) tells `processMessage` whether to validate\n * the handler return value and publish a reply.\n */\n private resolveConsumerView(name: HandlerName<TContract>): {\n consumer: ConsumerDefinition;\n isRpc: boolean;\n responseSchema?: StandardSchemaV1;\n } {\n // Use `Object.hasOwn` rather than `key in rpcs` so prototype properties\n // (e.g. \"toString\") on a plain object are not misclassified as RPC names.\n const rpcs = this.contract.rpcs;\n if (rpcs && Object.hasOwn(rpcs, name as string)) {\n const rpc = rpcs[name as string]!;\n return {\n consumer: { queue: rpc.queue, message: rpc.request },\n isRpc: true,\n responseSchema: rpc.response.payload,\n };\n }\n const consumerEntry = this.contract.consumers![name as string]!;\n return {\n consumer: extractConsumer(consumerEntry),\n isRpc: false,\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 defaultConsumerOptions,\n logger,\n telemetry,\n connectTimeoutMs,\n }: CreateWorkerOptions<TContract>): Future<Result<TypedAmqpWorker<TContract>, TechnicalError>> {\n const worker = new TypedAmqpWorker(\n contract,\n new AmqpClient(contract, {\n urls,\n connectionOptions,\n connectTimeoutMs,\n }),\n handlers,\n defaultConsumerOptions ?? {},\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 .flatMap((result) =>\n result.match({\n Ok: () => Future.value(Result.Ok<TypedAmqpWorker<TContract>, TechnicalError>(worker)),\n // Release the AmqpClient's connection ref-count and cancel any consumers\n // that registered before the failure, so a failed create() does not leak.\n Error: (error) =>\n worker\n .close()\n .tapError((closeError) => {\n logger?.warn(\"Failed to close worker after setup failure\", {\n error: closeError,\n });\n })\n .map(() => Result.Error<TypedAmqpWorker<TContract>, TechnicalError>(error)),\n }),\n );\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 * Start consuming for every entry in `contract.consumers` and `contract.rpcs`.\n */\n private consumeAll(): Future<Result<void, TechnicalError>> {\n const consumerNames = Object.keys(\n this.contract.consumers ?? {},\n ) as InferConsumerNames<TContract>[];\n const rpcNames = Object.keys(this.contract.rpcs ?? {}) as InferRpcNames<TContract>[];\n const allNames = [...consumerNames, ...rpcNames] as HandlerName<TContract>[];\n\n return Future.all(allNames.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 handler — either a `consumers`\n * entry (regular event/command consumer) or an `rpcs` entry (RPC server).\n */\n private consume(name: HandlerName<TContract>): Future<Result<void, TechnicalError>> {\n const view = this.resolveConsumerView(name);\n // Non-null assertion safe: `WorkerInferHandlers<TContract>` requires every\n // consumers / rpcs key to have a handler, so by the time we reach this\n // dispatch path the entry exists in `actualHandlers`. Enforced by the type\n // system at the public API boundary, not by a runtime check.\n const handler = this.actualHandlers[name]!;\n\n return this.consumeSingle(name, view, handler);\n }\n\n /**\n * Validate data against a Standard Schema. No side effects; the caller is\n * responsible for ack/nack based on the Result.\n */\n private validateSchema(\n schema: StandardSchemaV1,\n data: unknown,\n context: { consumerName: string; field: string },\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 }\n\n /**\n * Parse and validate a message from AMQP. Pure: returns the validated payload\n * and headers, or an error. The dispatch path in {@link processMessage} routes\n * validation/parse errors directly to the DLQ (single nack) — they never enter\n * the retry pipeline because retrying an unparseable or schema-violating\n * payload cannot succeed.\n */\n private parseAndValidateMessage(\n msg: ConsumeMessage,\n consumer: ConsumerDefinition,\n consumerName: HandlerName<TContract>,\n ): Future<Result<{ payload: unknown; headers: unknown }, TechnicalError>> {\n const context = { consumerName: String(consumerName) };\n\n const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding)\n .mapErrorToResult((error) =>\n Result.Error(new TechnicalError(\"Failed to decompress message\", error)),\n )\n .mapOkToResult((buffer) =>\n Result.fromExecution(() => JSON.parse(buffer.toString()) as unknown).mapError(\n (error) => new TechnicalError(\"Failed to parse JSON\", error),\n ),\n )\n .flatMapOk((parsed) =>\n this.validateSchema(consumer.message.payload as StandardSchemaV1, parsed, {\n ...context,\n field: \"payload\",\n }),\n );\n\n const parseHeaders = consumer.message.headers\n ? this.validateSchema(\n consumer.message.headers as StandardSchemaV1,\n msg.properties.headers ?? {},\n {\n ...context,\n field: \"headers\",\n },\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<{ payload: unknown; headers: unknown }, TechnicalError>>;\n }\n\n /**\n * Validate an RPC handler's response and publish it back to the caller's reply\n * queue with the same `correlationId`. Published via the AMQP default exchange\n * with `routingKey = msg.properties.replyTo`, which works for both\n * `amq.rabbitmq.reply-to` and any anonymous queue declared by the caller.\n *\n * Failure semantics:\n * - **Missing replyTo / correlationId**: NonRetryableError. The caller is\n * already lost; retrying the original message cannot recover the reply\n * path. The poison message lands in DLQ for inspection rather than being\n * silently ack'd (which would mask a contract violation).\n * - **Schema validation failure**: NonRetryableError — the handler returned\n * the wrong shape; retrying the same input will not fix it.\n * - **Publish failure**: NonRetryableError. The caller has already timed out\n * (or will shortly), so retrying the message wastes the queue's retry\n * budget on a reply that no one is waiting for. The message is logged and\n * DLQ'd; the original work is treated as completed for the purpose of the\n * inbox.\n */\n private publishRpcResponse(\n msg: ConsumeMessage,\n queueName: string,\n rpcName: HandlerName<TContract>,\n responseSchema: StandardSchemaV1,\n response: unknown,\n ): Future<Result<void, HandlerError>> {\n const replyTo = msg.properties.replyTo;\n const correlationId = msg.properties.correlationId;\n if (typeof replyTo !== \"string\" || replyTo.length === 0) {\n this.logger?.error(\n \"RPC handler returned a response but the incoming message has no replyTo\",\n { rpcName: String(rpcName), queueName },\n );\n return Future.value(\n Result.Error<void, HandlerError>(\n new NonRetryableError(\n `RPC \"${String(rpcName)}\" received a message without replyTo; cannot deliver response`,\n ),\n ),\n );\n }\n if (typeof correlationId !== \"string\" || correlationId.length === 0) {\n // Without a correlationId the client cannot match the reply to its\n // pending call — publishing anyway would guarantee a client-side timeout.\n this.logger?.error(\n \"RPC handler returned a response but the incoming message has no correlationId\",\n { rpcName: String(rpcName), queueName, replyTo },\n );\n return Future.value(\n Result.Error<void, HandlerError>(\n new NonRetryableError(\n `RPC \"${String(rpcName)}\" received a message without correlationId; cannot deliver response`,\n ),\n ),\n );\n }\n\n // Wrap the call to `validate` itself in try/catch — a Standard Schema\n // implementation may throw synchronously (not via a rejected Promise), and\n // we don't want that to crash the consume callback.\n let rawValidation: ReturnType<StandardSchemaV1[\"~standard\"][\"validate\"]>;\n try {\n rawValidation = responseSchema[\"~standard\"].validate(response);\n } catch (error: unknown) {\n return Future.value(\n Result.Error<void, HandlerError>(\n new NonRetryableError(\"RPC response schema validation threw\", error),\n ),\n );\n }\n const validationPromise =\n rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);\n\n return Future.fromPromise(validationPromise)\n .mapError(\n (error: unknown) =>\n new NonRetryableError(\"RPC response schema validation threw\", error) as HandlerError,\n )\n .mapOkToResult((validation) => {\n if (validation.issues) {\n return Result.Error<unknown, HandlerError>(\n new NonRetryableError(\n `RPC response for \"${String(rpcName)}\" failed schema validation`,\n new MessageValidationError(String(rpcName), validation.issues),\n ),\n );\n }\n return Result.Ok<unknown, HandlerError>(validation.value);\n })\n .flatMapOk((validatedResponse) =>\n this.amqpClient\n .publish(\"\", replyTo, validatedResponse, {\n correlationId,\n contentType: \"application/json\",\n })\n // Reply-side failures are not retryable from the inbox: by the time\n // the broker can't deliver the reply, the caller's RPC future has\n // already (or will soon) time out. Retrying the original message\n // re-runs the handler against a stale caller. Send to DLQ instead so\n // the failure is visible without churning the queue.\n .mapErrorToResult((error: TechnicalError) =>\n Result.Error<void, HandlerError>(\n new NonRetryableError(\"Failed to publish RPC response\", error),\n ),\n )\n .mapOkToResult((published) =>\n published\n ? Result.Ok<void, HandlerError>(undefined)\n : Result.Error<void, HandlerError>(\n new NonRetryableError(\"Failed to publish RPC response: channel buffer full\"),\n ),\n ),\n );\n }\n\n /**\n * Process a single consumed message: validate, invoke handler, optionally\n * publish the RPC response, record telemetry, and handle errors.\n */\n private processMessage(\n msg: ConsumeMessage,\n view: { consumer: ConsumerDefinition; isRpc: boolean; responseSchema?: StandardSchemaV1 },\n name: HandlerName<TContract>,\n handler: StoredHandler,\n ): Future<Result<void, TechnicalError>> {\n const { consumer, isRpc, responseSchema } = view;\n const queueName = extractQueue(consumer.queue).name;\n const startTime = Date.now();\n const span = startConsumeSpan(this.telemetry, queueName, String(name), {\n \"messaging.rabbitmq.message.delivery_tag\": msg.fields.deliveryTag,\n });\n\n let messageHandled = false;\n let firstError: Error | undefined;\n\n return this.parseAndValidateMessage(msg, consumer, name)\n .flatMap((parseResult) =>\n parseResult.match({\n Ok: (validatedMessage) =>\n handler(validatedMessage, msg)\n .flatMapOk((handlerResponse) => {\n if (isRpc && responseSchema) {\n return this.publishRpcResponse(\n msg,\n queueName,\n name,\n responseSchema,\n handlerResponse,\n ).flatMapOk(() => {\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(name),\n queueName,\n });\n this.amqpClient.ack(msg);\n messageHandled = true;\n return Future.value(Result.Ok<void, HandlerError>(undefined));\n });\n }\n\n this.logger?.info(\"Message consumed successfully\", {\n consumerName: String(name),\n queueName,\n });\n this.amqpClient.ack(msg);\n messageHandled = true;\n\n return Future.value(Result.Ok<void, HandlerError>(undefined));\n })\n .flatMapError((handlerError: HandlerError) => {\n this.logger?.error(\"Error processing message\", {\n consumerName: String(name),\n queueName,\n errorType: handlerError.name,\n error: handlerError.message,\n });\n firstError = handlerError;\n\n return handleError(\n { amqpClient: this.amqpClient, logger: this.logger },\n handlerError,\n msg,\n String(name),\n consumer,\n );\n }),\n // Parse / validation failure path: nack once with requeue=false so the\n // queue's DLX (if configured) receives the poison message. We bypass\n // handleError() because a malformed payload is deterministic — retrying\n // it would burn the queue's retry budget on a guaranteed failure.\n Error: (parseError) => {\n firstError = parseError;\n this.logger?.error(\"Failed to parse/validate message; sending to DLQ\", {\n consumerName: String(name),\n queueName,\n error: parseError,\n });\n this.amqpClient.nack(msg, false, false);\n return Future.value(Result.Error<void, TechnicalError>(parseError));\n },\n }),\n )\n .map((result) => {\n const durationMs = Date.now() - startTime;\n if (messageHandled) {\n endSpanSuccess(span);\n recordConsumeMetric(this.telemetry, queueName, String(name), true, durationMs);\n } else {\n const error = result.isError()\n ? result.error\n : (firstError ?? new Error(\"Unknown error\"));\n endSpanError(span, error);\n recordConsumeMetric(this.telemetry, queueName, String(name), false, durationMs);\n }\n return result;\n });\n }\n\n /**\n * Consume messages one at a time.\n */\n private consumeSingle(\n name: HandlerName<TContract>,\n view: { consumer: ConsumerDefinition; isRpc: boolean; responseSchema?: StandardSchemaV1 },\n handler: StoredHandler,\n ): Future<Result<void, TechnicalError>> {\n const queueName = extractQueue(view.consumer.queue).name;\n\n return this.amqpClient\n .consume(\n queueName,\n async (msg) => {\n if (msg === null) {\n this.logger?.warn(\"Consumer cancelled by server\", {\n consumerName: String(name),\n queueName,\n });\n return;\n }\n // The dispatch path is built on `Future<Result<…>>` so handler\n // failures are values, not exceptions. Defensively guard the\n // boundary anyway: a handler that violates the contract by throwing\n // synchronously (or any unexpected fault inside processMessage)\n // would otherwise leave the message neither acked nor nacked, and\n // amqp-connection-manager would not redeliver it until the channel\n // closes. nack(requeue=false) routes it via DLX if configured.\n try {\n await this.processMessage(msg, view, name, handler).toPromise();\n } catch (error: unknown) {\n this.logger?.error(\"Uncaught error in consume callback; nacking message\", {\n consumerName: String(name),\n queueName,\n error,\n });\n this.amqpClient.nack(msg, false, false);\n }\n },\n this.consumerOptions[name],\n )\n .tapOk((consumerTag) => {\n this.consumerTags.add(consumerTag);\n })\n .mapError(\n (error) => new TechnicalError(`Failed to start consuming for \"${String(name)}\"`, error),\n )\n .mapOk(() => undefined);\n }\n}\n","import type { ContractDefinition, InferConsumerNames } from \"@amqp-contract/contract\";\nimport type {\n WorkerInferConsumerHandler,\n WorkerInferConsumerHandlerEntry,\n WorkerInferConsumerHandlers,\n} from \"./types.js\";\nimport { ConsumerOptions } from \"./worker.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: object,\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// Handler Definitions\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: WorkerInferConsumerHandler<TContract, TName>,\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n options: ConsumerOptions,\n): WorkerInferConsumerHandlerEntry<TContract, TName>;\nexport function defineHandler<\n TContract extends ContractDefinition,\n TName extends InferConsumerNames<TContract>,\n>(\n contract: TContract,\n consumerName: TName,\n handler: WorkerInferConsumerHandler<TContract, TName>,\n options?: ConsumerOptions,\n): WorkerInferConsumerHandlerEntry<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: WorkerInferConsumerHandlers<TContract>,\n): WorkerInferConsumerHandlers<TContract> {\n validateHandlers(contract, handlers);\n return handlers;\n}\n"],"mappings":"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"}