@amqp-contract/worker 0.25.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { extractConsumer, extractQueue, isQueueWithTtlBackoffInfrastructure } from "@amqp-contract/contract";
2
2
  import { AmqpClient, MessageValidationError, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, safeJsonParse, startConsumeSpan } from "@amqp-contract/core";
3
- import { ResultAsync, err, errAsync, ok, okAsync } from "neverthrow";
3
+ import { Err, Ok, TaggedError, allAsync, fromPromise, fromSafePromise } from "unthrown";
4
4
  import { gunzip, inflate } from "node:zlib";
5
5
  import { promisify } from "node:util";
6
6
  //#region src/decompression.ts
@@ -21,55 +21,57 @@ function isSupportedEncoding(encoding) {
21
21
  *
22
22
  * @param buffer - The buffer to decompress
23
23
  * @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')
24
- * @returns A ResultAsync resolving to the decompressed buffer or a TechnicalError
24
+ * @returns An AsyncResult resolving to the decompressed buffer or a TechnicalError
25
25
  *
26
26
  * @internal
27
27
  */
28
28
  function decompressBuffer(buffer, contentEncoding) {
29
- if (!contentEncoding) return okAsync(buffer);
29
+ if (!contentEncoding) return Ok(buffer).toAsync();
30
30
  const normalizedEncoding = contentEncoding.toLowerCase();
31
- if (!isSupportedEncoding(normalizedEncoding)) return errAsync(new TechnicalError(`Unsupported content-encoding: "${contentEncoding}". Supported encodings are: ${SUPPORTED_ENCODINGS.join(", ")}. Please check your publisher configuration.`));
31
+ if (!isSupportedEncoding(normalizedEncoding)) return Err(new TechnicalError(`Unsupported content-encoding: "${contentEncoding}". Supported encodings are: ${SUPPORTED_ENCODINGS.join(", ")}. Please check your publisher configuration.`)).toAsync();
32
32
  switch (normalizedEncoding) {
33
- case "gzip": return ResultAsync.fromPromise(gunzipAsync(buffer), (error) => new TechnicalError("Failed to decompress gzip", error));
34
- case "deflate": return ResultAsync.fromPromise(inflateAsync(buffer), (error) => new TechnicalError("Failed to decompress deflate", error));
33
+ case "gzip": return fromPromise(gunzipAsync(buffer), (error) => new TechnicalError("Failed to decompress gzip", error));
34
+ case "deflate": return fromPromise(inflateAsync(buffer), (error) => new TechnicalError("Failed to decompress deflate", error));
35
35
  }
36
36
  }
37
37
  //#endregion
38
38
  //#region src/errors.ts
39
39
  /**
40
- * Abstract base class for all handler-signalled errors.
41
- *
42
- * Concrete subclasses (`RetryableError`, `NonRetryableError`) discriminate on
43
- * the `name` property so exhaustive narrowing in user code keeps working.
44
- * `error instanceof HandlerError` is true for any handler error.
45
- */
46
- var HandlerError = class extends Error {
47
- constructor(message, cause) {
48
- super(message);
49
- this.cause = cause;
50
- const ErrorConstructor = Error;
51
- if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
52
- }
53
- };
54
- /**
55
40
  * Retryable errors - transient failures that may succeed on retry
56
41
  * Examples: network timeouts, rate limiting, temporary service unavailability
57
42
  *
58
43
  * Use this error type when the operation might succeed if retried.
59
44
  * The worker will apply exponential backoff and retry the message.
45
+ *
46
+ * Built on unthrown's {@link TaggedError}, so it carries a namespaced `_tag` of
47
+ * `"@amqp-contract/RetryableError"` (to avoid colliding with other libraries'
48
+ * tags in a shared `matchTags`) for exhaustive dispatch; the `Error.name` is
49
+ * kept bare (`"RetryableError"`).
60
50
  */
61
- var RetryableError = class extends HandlerError {
62
- name = "RetryableError";
51
+ var RetryableError = class extends TaggedError("@amqp-contract/RetryableError", { name: "RetryableError" }) {
52
+ constructor(message, cause) {
53
+ super({
54
+ message,
55
+ cause
56
+ });
57
+ }
63
58
  };
64
59
  /**
65
60
  * Non-retryable errors - permanent failures that should not be retried
66
61
  * Examples: invalid data, business rule violations, permanent external failures
67
62
  *
68
63
  * Use this error type when retrying would not help - the message will be
69
- * immediately sent to the dead letter queue (DLQ) if configured.
64
+ * immediately sent to the dead letter queue (DLQ) if configured. Carries a
65
+ * namespaced `_tag` of `"@amqp-contract/NonRetryableError"`; the `Error.name` is
66
+ * kept bare (`"NonRetryableError"`).
70
67
  */
71
- var NonRetryableError = class extends HandlerError {
72
- name = "NonRetryableError";
68
+ var NonRetryableError = class extends TaggedError("@amqp-contract/NonRetryableError", { name: "NonRetryableError" }) {
69
+ constructor(message, cause) {
70
+ super({
71
+ message,
72
+ cause
73
+ });
74
+ }
73
75
  };
74
76
  /**
75
77
  * Type guard to check if an error is a RetryableError.
@@ -140,7 +142,7 @@ function isNonRetryableError(error) {
140
142
  * ```
141
143
  */
142
144
  function isHandlerError(error) {
143
- return error instanceof HandlerError;
145
+ return error instanceof RetryableError || error instanceof NonRetryableError;
144
146
  }
145
147
  /**
146
148
  * Create a RetryableError with less verbosity.
@@ -155,16 +157,16 @@ function isHandlerError(error) {
155
157
  * @example
156
158
  * ```typescript
157
159
  * import { retryable } from '@amqp-contract/worker';
158
- * import { ResultAsync } from 'neverthrow';
160
+ * import { fromPromise } from 'unthrown';
159
161
  *
160
162
  * const handler = ({ payload }) =>
161
- * ResultAsync.fromPromise(
163
+ * fromPromise(
162
164
  * processPayment(payload),
163
165
  * (e) => retryable('Payment service unavailable', e),
164
166
  * ).map(() => undefined);
165
167
  *
166
168
  * // Equivalent to:
167
- * // ResultAsync.fromPromise(processPayment(payload), (e) => new RetryableError('...', e))
169
+ * // fromPromise(processPayment(payload), (e) => new RetryableError('...', e))
168
170
  * ```
169
171
  */
170
172
  function retryable(message, cause) {
@@ -183,17 +185,17 @@ function retryable(message, cause) {
183
185
  * @example
184
186
  * ```typescript
185
187
  * import { nonRetryable } from '@amqp-contract/worker';
186
- * import { errAsync, okAsync } from 'neverthrow';
188
+ * import { Err, Ok } from 'unthrown';
187
189
  *
188
190
  * const handler = ({ payload }) => {
189
191
  * if (!isValidPayload(payload)) {
190
- * return errAsync(nonRetryable('Invalid payload format'));
192
+ * return Err(nonRetryable('Invalid payload format')).toAsync();
191
193
  * }
192
- * return okAsync(undefined);
194
+ * return Ok(undefined).toAsync();
193
195
  * };
194
196
  *
195
197
  * // Equivalent to:
196
- * // return errAsync(new NonRetryableError('Invalid payload format'));
198
+ * // return Err(new NonRetryableError('Invalid payload format')).toAsync();
197
199
  * ```
198
200
  */
199
201
  function nonRetryable(message, cause) {
@@ -222,7 +224,7 @@ function nonRetryable(message, cause) {
222
224
  function handleError(ctx, error, msg, consumerName, consumer) {
223
225
  if (error instanceof NonRetryableError) {
224
226
  sendToDLQ(ctx, msg, consumer);
225
- return okAsync(void 0);
227
+ return Ok(void 0).toAsync();
226
228
  }
227
229
  const config = extractQueue(consumer.queue).retry;
228
230
  if (config.mode === "immediate-requeue") return handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, config);
@@ -232,7 +234,7 @@ function handleError(ctx, error, msg, consumerName, consumer) {
232
234
  queueName: extractQueue(consumer.queue).name
233
235
  });
234
236
  sendToDLQ(ctx, msg, consumer);
235
- return okAsync(void 0);
237
+ return Ok(void 0).toAsync();
236
238
  }
237
239
  /**
238
240
  * Handle error by requeuing immediately.
@@ -255,7 +257,7 @@ function handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, co
255
257
  maxRetries: config.maxRetries
256
258
  });
257
259
  sendToDLQ(ctx, msg, consumer);
258
- return okAsync(void 0);
260
+ return Ok(void 0).toAsync();
259
261
  }
260
262
  ctx.logger?.info("Retrying message (immediate-requeue mode)", {
261
263
  consumerName,
@@ -265,7 +267,7 @@ function handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, co
265
267
  });
266
268
  if (queue.type === "quorum") {
267
269
  ctx.amqpClient.nack(msg, false, true);
268
- return okAsync(void 0);
270
+ return Ok(void 0).toAsync();
269
271
  } else return publishForRetry(ctx, {
270
272
  msg,
271
273
  exchange: msg.fields.exchange,
@@ -306,7 +308,7 @@ function handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config)
306
308
  consumerName,
307
309
  queueName: consumer.queue.name
308
310
  });
309
- return errAsync(new TechnicalError("Queue does not have TTL-backoff infrastructure"));
311
+ return Err(new TechnicalError("Queue does not have TTL-backoff infrastructure")).toAsync();
310
312
  }
311
313
  const queueEntry = consumer.queue;
312
314
  const queueName = extractQueue(queueEntry).name;
@@ -319,7 +321,7 @@ function handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config)
319
321
  maxRetries: config.maxRetries
320
322
  });
321
323
  sendToDLQ(ctx, msg, consumer);
322
- return okAsync(void 0);
324
+ return Ok(void 0).toAsync();
323
325
  }
324
326
  const delayMs = calculateRetryDelay(retryCount, config);
325
327
  ctx.logger?.info("Retrying message (ttl-backoff mode)", {
@@ -392,14 +394,14 @@ function publishForRetry(ctx, { msg, exchange, routingKey, queueName, waitQueueN
392
394
  "x-retry-queue": queueName
393
395
  } : {}
394
396
  }
395
- }).andThen((published) => {
397
+ }).flatMap((published) => {
396
398
  if (!published) {
397
399
  ctx.logger?.error("Failed to publish message for retry (write buffer full)", {
398
400
  queueName,
399
401
  retryCount: newRetryCount,
400
402
  ...delayMs !== void 0 ? { delayMs } : {}
401
403
  });
402
- return err(new TechnicalError("Failed to publish message for retry (write buffer full)"));
404
+ return Err(new TechnicalError("Failed to publish message for retry (write buffer full)"));
403
405
  }
404
406
  ctx.amqpClient.ack(msg);
405
407
  ctx.logger?.info("Message published for retry", {
@@ -407,7 +409,7 @@ function publishForRetry(ctx, { msg, exchange, routingKey, queueName, waitQueueN
407
409
  retryCount: newRetryCount,
408
410
  ...delayMs !== void 0 ? { delayMs } : {}
409
411
  });
410
- return ok(void 0);
412
+ return Ok(void 0);
411
413
  }).orElse((publishError) => {
412
414
  ctx.logger?.error("Publish for retry failed; leaving original un-ack'd for redelivery", {
413
415
  queueName,
@@ -415,7 +417,7 @@ function publishForRetry(ctx, { msg, exchange, routingKey, queueName, waitQueueN
415
417
  ...delayMs !== void 0 ? { delayMs } : {},
416
418
  error: publishError
417
419
  });
418
- return err(publishError);
420
+ return Err(publishError);
419
421
  });
420
422
  }
421
423
  /**
@@ -452,7 +454,7 @@ function isHandlerTuple(entry) {
452
454
  * ```typescript
453
455
  * import { TypedAmqpWorker } from '@amqp-contract/worker';
454
456
  * import { defineQueue, defineMessage, defineContract, defineConsumer } from '@amqp-contract/contract';
455
- * import { okAsync } from 'neverthrow';
457
+ * import { Ok } from 'unthrown';
456
458
  * import { z } from 'zod';
457
459
  *
458
460
  * const orderQueue = defineQueue('order-processing');
@@ -472,20 +474,23 @@ function isHandlerTuple(entry) {
472
474
  * handlers: {
473
475
  * processOrder: ({ payload }) => {
474
476
  * console.log('Processing order', payload.orderId);
475
- * return okAsync(undefined);
477
+ * return Ok(undefined).toAsync();
476
478
  * },
477
479
  * },
478
480
  * urls: ['amqp://localhost'],
479
481
  * });
480
482
  *
481
- * if (result.isErr()) throw result.error;
482
- * const worker = result.value;
483
+ * const worker = result.unwrap();
483
484
  *
484
485
  * // Close when done
485
486
  * await worker.close();
486
487
  * ```
487
488
  */
488
489
  var TypedAmqpWorker = class TypedAmqpWorker {
490
+ contract;
491
+ amqpClient;
492
+ defaultConsumerOptions;
493
+ logger;
489
494
  /**
490
495
  * Internal handler storage. Keyed by handler name (consumer or RPC); the
491
496
  * stored function signature is widened so the dispatch loop can call it
@@ -558,14 +563,14 @@ var TypedAmqpWorker = class TypedAmqpWorker {
558
563
  * Connections are automatically shared across clients and workers with the same
559
564
  * URLs and connection options, following RabbitMQ best practices.
560
565
  *
561
- * @returns A ResultAsync that resolves to the worker or a TechnicalError.
566
+ * @returns A AsyncResult that resolves to the worker or a TechnicalError.
562
567
  *
563
568
  * @example
564
569
  * ```typescript
565
570
  * const result = await TypedAmqpWorker.create({
566
571
  * contract: myContract,
567
572
  * handlers: {
568
- * processOrder: ({ payload }) => okAsync(undefined),
573
+ * processOrder: ({ payload }) => Ok(undefined).toAsync(),
569
574
  * },
570
575
  * urls: ['amqp://localhost'],
571
576
  * });
@@ -577,14 +582,15 @@ var TypedAmqpWorker = class TypedAmqpWorker {
577
582
  connectionOptions,
578
583
  connectTimeoutMs
579
584
  }), handlers, defaultConsumerOptions ?? {}, logger, telemetry);
580
- const setup = worker.waitForConnectionReady().andThen(() => worker.consumeAll());
581
- return new ResultAsync((async () => {
585
+ const setup = worker.waitForConnectionReady().flatMap(() => worker.consumeAll());
586
+ return fromSafePromise((async () => {
582
587
  const setupResult = await setup;
583
- if (setupResult.isOk()) return ok(worker);
584
- const closeResult = await worker.close();
585
- if (closeResult.isErr()) logger?.warn("Failed to close worker after setup failure", { error: closeResult.error });
586
- return err(setupResult.error);
587
- })());
588
+ if (!setupResult.isOk()) {
589
+ const closeResult = await worker.close();
590
+ if (closeResult.isErr()) logger?.warn("Failed to close worker after setup failure", { error: closeResult.error });
591
+ }
592
+ return setupResult.map(() => worker);
593
+ })()).flatMap((result) => result);
588
594
  }
589
595
  /**
590
596
  * Close the AMQP channel and connection.
@@ -601,16 +607,15 @@ var TypedAmqpWorker = class TypedAmqpWorker {
601
607
  * ```
602
608
  */
603
609
  close() {
604
- const cancellations = Array.from(this.consumerTags).map((consumerTag) => this.amqpClient.cancel(consumerTag).orElse((error) => {
610
+ return allAsync(Array.from(this.consumerTags).map((consumerTag) => this.amqpClient.cancel(consumerTag).orElse((error) => {
605
611
  this.logger?.warn("Failed to cancel consumer during close", {
606
612
  consumerTag,
607
613
  error
608
614
  });
609
- return ok(void 0);
610
- }));
611
- return ResultAsync.combine(cancellations).andTee(() => {
615
+ return Ok(void 0);
616
+ }))).tap(() => {
612
617
  this.consumerTags.clear();
613
- }).andThen(() => this.amqpClient.close()).map(() => void 0);
618
+ }).flatMap(() => this.amqpClient.close()).map(() => void 0);
614
619
  }
615
620
  /**
616
621
  * Start consuming for every entry in `contract.consumers` and `contract.rpcs`.
@@ -618,8 +623,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
618
623
  consumeAll() {
619
624
  const consumerNames = Object.keys(this.contract.consumers ?? {});
620
625
  const rpcNames = Object.keys(this.contract.rpcs ?? {});
621
- const allNames = [...consumerNames, ...rpcNames];
622
- return ResultAsync.combine(allNames.map((name) => this.consume(name))).map(() => void 0);
626
+ return allAsync([...consumerNames, ...rpcNames].map((name) => this.consume(name))).map(() => void 0);
623
627
  }
624
628
  waitForConnectionReady() {
625
629
  return this.amqpClient.waitForConnect();
@@ -639,10 +643,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
639
643
  */
640
644
  validateSchema(schema, data, context) {
641
645
  const rawValidation = schema["~standard"].validate(data);
642
- const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
643
- return ResultAsync.fromPromise(validationPromise, (error) => new TechnicalError(`Error validating ${context.field}`, error)).andThen((result) => {
644
- if (result.issues) return err(new TechnicalError(`${context.field} validation failed`, new MessageValidationError(context.consumerName, result.issues)));
645
- return ok(result.value);
646
+ return fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation), (error) => new TechnicalError(`Error validating ${context.field}`, error)).flatMap((result) => {
647
+ if (result.issues) return Err(new TechnicalError(`${context.field} validation failed`, new MessageValidationError(context.consumerName, result.issues)));
648
+ return Ok(result.value);
646
649
  });
647
650
  }
648
651
  /**
@@ -654,15 +657,13 @@ var TypedAmqpWorker = class TypedAmqpWorker {
654
657
  */
655
658
  parseAndValidateMessage(msg, consumer, consumerName) {
656
659
  const context = { consumerName: String(consumerName) };
657
- const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding).andThen((buffer) => safeJsonParse(buffer, (error) => new TechnicalError("Failed to parse JSON", error))).andThen((parsed) => this.validateSchema(consumer.message.payload, parsed, {
660
+ return allAsync([decompressBuffer(msg.content, msg.properties.contentEncoding).flatMap((buffer) => safeJsonParse(buffer, (error) => new TechnicalError("Failed to parse JSON", error))).flatMap((parsed) => this.validateSchema(consumer.message.payload, parsed, {
658
661
  ...context,
659
662
  field: "payload"
660
- }));
661
- const parseHeaders = consumer.message.headers ? this.validateSchema(consumer.message.headers, msg.properties.headers ?? {}, {
663
+ })), consumer.message.headers ? this.validateSchema(consumer.message.headers, msg.properties.headers ?? {}, {
662
664
  ...context,
663
665
  field: "headers"
664
- }) : okAsync(void 0);
665
- return ResultAsync.combine([parsePayload, parseHeaders]).map(([payload, headers]) => ({
666
+ }) : Ok(void 0).toAsync()]).map(([payload, headers]) => ({
666
667
  payload,
667
668
  headers
668
669
  }));
@@ -694,7 +695,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
694
695
  rpcName: String(rpcName),
695
696
  queueName
696
697
  });
697
- return errAsync(new NonRetryableError(`RPC "${String(rpcName)}" received a message without replyTo; cannot deliver response`));
698
+ return Err(new NonRetryableError(`RPC "${String(rpcName)}" received a message without replyTo; cannot deliver response`)).toAsync();
698
699
  }
699
700
  if (typeof correlationId !== "string" || correlationId.length === 0) {
700
701
  this.logger?.error("RPC handler returned a response but the incoming message has no correlationId", {
@@ -702,22 +703,21 @@ var TypedAmqpWorker = class TypedAmqpWorker {
702
703
  queueName,
703
704
  replyTo
704
705
  });
705
- return errAsync(new NonRetryableError(`RPC "${String(rpcName)}" received a message without correlationId; cannot deliver response`));
706
+ return Err(new NonRetryableError(`RPC "${String(rpcName)}" received a message without correlationId; cannot deliver response`)).toAsync();
706
707
  }
707
708
  let rawValidation;
708
709
  try {
709
710
  rawValidation = responseSchema["~standard"].validate(response);
710
711
  } catch (error) {
711
- return errAsync(new NonRetryableError("RPC response schema validation threw", error));
712
+ return Err(new NonRetryableError("RPC response schema validation threw", error)).toAsync();
712
713
  }
713
- const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
714
- return ResultAsync.fromPromise(validationPromise, (error) => new NonRetryableError("RPC response schema validation threw", error)).andThen((validation) => {
715
- if (validation.issues) return err(new NonRetryableError(`RPC response for "${String(rpcName)}" failed schema validation`, new MessageValidationError(String(rpcName), validation.issues)));
716
- return ok(validation.value);
717
- }).andThen((validatedResponse) => this.amqpClient.publish("", replyTo, validatedResponse, {
714
+ return fromPromise(rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation), (error) => new NonRetryableError("RPC response schema validation threw", error)).flatMap((validation) => {
715
+ if (validation.issues) return Err(new NonRetryableError(`RPC response for "${String(rpcName)}" failed schema validation`, new MessageValidationError(String(rpcName), validation.issues)));
716
+ return Ok(validation.value);
717
+ }).flatMap((validatedResponse) => this.amqpClient.publish("", replyTo, validatedResponse, {
718
718
  correlationId,
719
719
  contentType: "application/json"
720
- }).mapErr((error) => new NonRetryableError("Failed to publish RPC response", error)).andThen((published) => published ? ok(void 0) : err(new NonRetryableError("Failed to publish RPC response: channel buffer full"))));
720
+ }).mapErr((error) => new NonRetryableError("Failed to publish RPC response", error)).flatMap((published) => published ? Ok(void 0) : Err(new NonRetryableError("Failed to publish RPC response: channel buffer full"))));
721
721
  }
722
722
  /**
723
723
  * Parse and validate the message; on failure, nack(requeue=false) so the
@@ -728,7 +728,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
728
728
  parseAndValidateOrNack(msg, consumer, name) {
729
729
  return this.parseAndValidateMessage(msg, consumer, name).orElse((parseError) => {
730
730
  this.amqpClient.nack(msg, false, false);
731
- return errAsync(parseError);
731
+ return Err(parseError).toAsync();
732
732
  });
733
733
  }
734
734
  /**
@@ -743,10 +743,10 @@ var TypedAmqpWorker = class TypedAmqpWorker {
743
743
  /**
744
744
  * For RPC handlers, validate and publish the reply on the caller's
745
745
  * `replyTo` / `correlationId`. For non-RPC consumers, this is a no-op that
746
- * resolves to `okAsync(undefined)`.
746
+ * resolves to `Ok(undefined).toAsync()`.
747
747
  */
748
748
  publishReplyIfRpc(msg, view, name, handlerResponse) {
749
- if (!view.isRpc || !view.responseSchema) return okAsync(void 0);
749
+ if (!view.isRpc || !view.responseSchema) return Ok(void 0).toAsync();
750
750
  const queueName = extractQueue(view.consumer.queue).name;
751
751
  return this.publishRpcResponse(msg, queueName, name, view.responseSchema, handlerResponse);
752
752
  }
@@ -759,7 +759,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
759
759
  * is still needed (see {@link consumeSingle}).
760
760
  *
761
761
  * Success-vs-failure telemetry is data-driven: the chain resolves to
762
- * `ok(undefined)` only on handler success (and reply-publish success for
762
+ * `Ok(undefined)` only on handler success (and reply-publish success for
763
763
  * RPC). Handler failures — even when {@link handleError} routes them
764
764
  * successfully to retry/DLQ — are classified as failures for metrics by
765
765
  * re-failing the chain with a `TechnicalError` whose `cause` is the
@@ -772,14 +772,14 @@ var TypedAmqpWorker = class TypedAmqpWorker {
772
772
  const queueName = extractQueue(consumer.queue).name;
773
773
  const startTime = Date.now();
774
774
  const span = startConsumeSpan(this.telemetry, queueName, String(name), { "messaging.rabbitmq.message.delivery_tag": msg.fields.deliveryTag });
775
- return this.parseAndValidateOrNack(msg, consumer, name).orTee((parseError) => {
775
+ return this.parseAndValidateOrNack(msg, consumer, name).tapErr((parseError) => {
776
776
  this.logger?.error("Failed to parse/validate message; sending to DLQ", {
777
777
  consumerName: String(name),
778
778
  queueName,
779
779
  error: parseError
780
780
  });
781
781
  state.messageHandled = true;
782
- }).andThen((validatedMessage) => this.runHandler(handler, validatedMessage, msg).andThen((handlerResponse) => this.publishReplyIfRpc(msg, view, name, handlerResponse).andTee(() => {
782
+ }).flatMap((validatedMessage) => this.runHandler(handler, validatedMessage, msg).flatMap((handlerResponse) => this.publishReplyIfRpc(msg, view, name, handlerResponse).tap(() => {
783
783
  this.logger?.info("Message consumed successfully", {
784
784
  consumerName: String(name),
785
785
  queueName
@@ -797,10 +797,10 @@ var TypedAmqpWorker = class TypedAmqpWorker {
797
797
  return handleError({
798
798
  amqpClient: this.amqpClient,
799
799
  logger: this.logger
800
- }, handlerError, msg, String(name), consumer).andTee(() => {
800
+ }, handlerError, msg, String(name), consumer).tap(() => {
801
801
  state.messageHandled = true;
802
- }).andThen(() => errAsync(new TechnicalError(`Handler "${String(name)}" failed: ${handlerError.message}`, handlerError)));
803
- })).andTee(() => {
802
+ }).flatMap(() => Err(new TechnicalError(`Handler "${String(name)}" failed: ${handlerError.message}`, handlerError)).toAsync());
803
+ })).tap(() => {
804
804
  try {
805
805
  endSpanSuccess(span);
806
806
  recordConsumeMetric(this.telemetry, queueName, String(name), true, Date.now() - startTime);
@@ -811,7 +811,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
811
811
  error: telemetryError
812
812
  });
813
813
  }
814
- }).orTee((error) => {
814
+ }).tapErr((error) => {
815
815
  const reportedError = error.cause instanceof Error ? error.cause : error;
816
816
  try {
817
817
  endSpanError(span, reportedError);
@@ -857,7 +857,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
857
857
  });
858
858
  this.amqpClient.nack(msg, false, false);
859
859
  }
860
- }, this.consumerOptions[name]).andTee((consumerTag) => {
860
+ }, this.consumerOptions[name]).tap((consumerTag) => {
861
861
  this.consumerTags.add(consumerTag);
862
862
  }).map(() => void 0).mapErr((error) => new TechnicalError(`Failed to start consuming for "${String(name)}"`, error));
863
863
  }
@@ -883,7 +883,9 @@ function formatAvailable(names) {
883
883
  function validateHandlerTargetExists(contract, name) {
884
884
  const consumers = contract.consumers;
885
885
  const rpcs = contract.rpcs;
886
- if (!(!!consumers && Object.hasOwn(consumers, name)) && !(!!rpcs && Object.hasOwn(rpcs, name))) {
886
+ const isConsumer = !!consumers && Object.hasOwn(consumers, name);
887
+ const isRpc = !!rpcs && Object.hasOwn(rpcs, name);
888
+ if (!isConsumer && !isRpc) {
887
889
  const available = formatAvailable(availableHandlerNames(contract));
888
890
  throw new Error(`Handler target "${name}" not found in contract. Available consumers and RPCs: ${available}`);
889
891
  }
@@ -904,8 +906,8 @@ function defineHandler(contract, name, handler, options) {
904
906
  * Define multiple type-safe handlers for consumers and RPCs in a contract.
905
907
  *
906
908
  * **Recommended:** This function creates handlers that return
907
- * `ResultAsync<void, HandlerError>` (consumers) or
908
- * `ResultAsync<TResponse, HandlerError>` (RPCs), providing explicit error
909
+ * `AsyncResult<void, HandlerError>` (consumers) or
910
+ * `AsyncResult<TResponse, HandlerError>` (RPCs), providing explicit error
909
911
  * handling and better control over retry behavior.
910
912
  *
911
913
  * The handlers object must contain exactly one entry per `consumers` and
@@ -919,15 +921,15 @@ function defineHandler(contract, name, handler, options) {
919
921
  * @example
920
922
  * ```typescript
921
923
  * import { defineHandlers, RetryableError } from '@amqp-contract/worker';
922
- * import { okAsync, ResultAsync } from 'neverthrow';
924
+ * import { fromPromise, Ok } from 'unthrown';
923
925
  *
924
926
  * const handlers = defineHandlers(orderContract, {
925
927
  * processOrder: ({ payload }) =>
926
- * ResultAsync.fromPromise(
928
+ * fromPromise(
927
929
  * processPayment(payload),
928
930
  * (error) => new RetryableError('Payment failed', error),
929
931
  * ).map(() => undefined),
930
- * calculate: ({ payload }) => okAsync({ sum: payload.a + payload.b }),
932
+ * calculate: ({ payload }) => Ok({ sum: payload.a + payload.b }).toAsync(),
931
933
  * });
932
934
  * ```
933
935
  */
@@ -936,6 +938,6 @@ function defineHandlers(contract, handlers) {
936
938
  return handlers;
937
939
  }
938
940
  //#endregion
939
- export { HandlerError, MessageValidationError, NonRetryableError, RetryableError, TypedAmqpWorker, defineHandler, defineHandlers, isHandlerError, isNonRetryableError, isRetryableError, nonRetryable, retryable };
941
+ export { MessageValidationError, NonRetryableError, RetryableError, TypedAmqpWorker, defineHandler, defineHandlers, isHandlerError, isNonRetryableError, isRetryableError, nonRetryable, retryable };
940
942
 
941
943
  //# sourceMappingURL=index.mjs.map