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