@amqp-contract/worker 0.23.0 → 0.24.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,11 +1,11 @@
1
1
  # @amqp-contract/worker
2
2
 
3
- **Type-safe AMQP worker for consuming messages using amqp-contract with Future/Result error handling.**
3
+ **Type-safe AMQP worker for consuming messages using amqp-contract with ResultAsync/Result error handling.**
4
4
 
5
5
  [![CI](https://github.com/btravers/amqp-contract/actions/workflows/ci.yml/badge.svg)](https://github.com/btravers/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
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?logo=typescript)](https://www.typescriptlang.org/)
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
11
  📖 **[Full documentation →](https://btravers.github.io/amqp-contract/api/worker)**
@@ -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 { Future } from "@swan-io/boxed";
34
+ import { ResultAsync } from "neverthrow";
35
35
  import { contract } from "./contract";
36
36
 
37
37
  // Optional: Create a logger implementation
@@ -43,21 +43,24 @@ const logger: Logger = {
43
43
  };
44
44
 
45
45
  // Create worker from contract with handlers (automatically connects and starts consuming)
46
- const worker = await TypedAmqpWorker.create({
47
- contract,
48
- handlers: {
49
- processOrder: ({ payload }) => {
50
- console.log("Processing order:", payload.orderId);
51
-
52
- // Your business logic here
53
- return Future.fromPromise(Promise.all([processPayment(payload), updateInventory(payload)]))
54
- .mapOk(() => undefined)
55
- .mapError((error) => new RetryableError("Order processing failed", error));
46
+ const worker = (
47
+ await TypedAmqpWorker.create({
48
+ contract,
49
+ handlers: {
50
+ processOrder: ({ payload }) => {
51
+ console.log("Processing order:", payload.orderId);
52
+
53
+ // Your business logic here
54
+ return ResultAsync.fromPromise(
55
+ Promise.all([processPayment(payload), updateInventory(payload)]),
56
+ (error) => new RetryableError("Order processing failed", error),
57
+ ).map(() => undefined);
58
+ },
56
59
  },
57
- },
58
- urls: ["amqp://localhost"],
59
- logger, // Optional: logs message consumption and errors
60
- });
60
+ urls: ["amqp://localhost"],
61
+ logger, // Optional: logs message consumption and errors
62
+ })
63
+ )._unsafeUnwrap();
61
64
 
62
65
  // Worker is already consuming messages
63
66
 
@@ -96,19 +99,22 @@ Then use `RetryableError` in your handlers:
96
99
 
97
100
  ```typescript
98
101
  import { TypedAmqpWorker, RetryableError } from "@amqp-contract/worker";
99
- import { Future } from "@swan-io/boxed";
100
-
101
- const worker = await TypedAmqpWorker.create({
102
- contract,
103
- handlers: {
104
- processOrder: ({ payload }) =>
105
- // If this fails with RetryableError, message is automatically retried
106
- Future.fromPromise(processPayment(payload))
107
- .mapOk(() => undefined)
108
- .mapError((error) => new RetryableError("Payment failed", error)),
109
- },
110
- urls: ["amqp://localhost"],
111
- });
102
+ import { ResultAsync } from "neverthrow";
103
+
104
+ const worker = (
105
+ await TypedAmqpWorker.create({
106
+ contract,
107
+ handlers: {
108
+ processOrder: ({ payload }) =>
109
+ // If this fails with RetryableError, message is automatically retried
110
+ ResultAsync.fromPromise(
111
+ processPayment(payload),
112
+ (error) => new RetryableError("Payment failed", error),
113
+ ).map(() => undefined),
114
+ },
115
+ urls: ["amqp://localhost"],
116
+ })
117
+ )._unsafeUnwrap();
112
118
  ```
113
119
 
114
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.
@@ -119,23 +125,22 @@ You can define handlers outside of the worker creation using `defineHandler` and
119
125
 
120
126
  ## Error Handling
121
127
 
122
- Worker handlers return `Future<Result<void, HandlerError>>` for explicit error handling:
128
+ Worker handlers return `ResultAsync<void, HandlerError>` for explicit error handling:
123
129
 
124
130
  ```typescript
125
131
  import { RetryableError, NonRetryableError } from "@amqp-contract/worker";
126
- import { Future, Result } from "@swan-io/boxed";
132
+ import { errAsync, ResultAsync } from "neverthrow";
127
133
 
128
134
  handlers: {
129
135
  processOrder: ({ payload }) => {
130
136
  // Validation errors - non-retryable
131
137
  if (payload.amount <= 0) {
132
- return Future.value(Result.Error(new NonRetryableError("Invalid amount")));
138
+ return errAsync(new NonRetryableError("Invalid amount"));
133
139
  }
134
140
 
135
141
  // Transient errors - retryable
136
- return Future.fromPromise(process(payload))
137
- .mapOk(() => undefined)
138
- .mapError((error) => new RetryableError("Processing failed", error));
142
+ return ResultAsync.fromPromise(process(payload), (error) => new RetryableError("Processing failed", error))
143
+ .map(() => undefined);
139
144
  },
140
145
  }
141
146
  ```
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 _swan_io_boxed = require("@swan-io/boxed");
4
+ let neverthrow = require("neverthrow");
5
5
  let node_zlib = require("node:zlib");
6
6
  let node_util = require("node:util");
7
7
  //#region src/decompression.ts
@@ -22,17 +22,17 @@ 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 Future with the decompressed buffer or a TechnicalError
25
+ * @returns A ResultAsync 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 _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(buffer));
30
+ if (!contentEncoding) return (0, neverthrow.okAsync)(buffer);
31
31
  const normalizedEncoding = contentEncoding.toLowerCase();
32
- if (!isSupportedEncoding(normalizedEncoding)) return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(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, neverthrow.errAsync)(new _amqp_contract_core.TechnicalError(`Unsupported content-encoding: "${contentEncoding}". Supported encodings are: ${SUPPORTED_ENCODINGS.join(", ")}. Please check your publisher configuration.`));
33
33
  switch (normalizedEncoding) {
34
- case "gzip": return _swan_io_boxed.Future.fromPromise(gunzipAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to decompress gzip", error));
35
- case "deflate": return _swan_io_boxed.Future.fromPromise(inflateAsync(buffer)).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to decompress deflate", error));
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));
36
36
  }
37
37
  }
38
38
  //#endregion
@@ -153,15 +153,16 @@ function isHandlerError(error) {
153
153
  * @example
154
154
  * ```typescript
155
155
  * import { retryable } from '@amqp-contract/worker';
156
- * import { Future, Result } from '@swan-io/boxed';
156
+ * import { ResultAsync } from 'neverthrow';
157
157
  *
158
158
  * const handler = ({ payload }) =>
159
- * Future.fromPromise(processPayment(payload))
160
- * .mapOk(() => undefined)
161
- * .mapError((e) => retryable('Payment service unavailable', e));
159
+ * ResultAsync.fromPromise(
160
+ * processPayment(payload),
161
+ * (e) => retryable('Payment service unavailable', e),
162
+ * ).map(() => undefined);
162
163
  *
163
164
  * // Equivalent to:
164
- * // .mapError((e) => new RetryableError('Payment service unavailable', e));
165
+ * // ResultAsync.fromPromise(processPayment(payload), (e) => new RetryableError('...', e))
165
166
  * ```
166
167
  */
167
168
  function retryable(message, cause) {
@@ -180,17 +181,17 @@ function retryable(message, cause) {
180
181
  * @example
181
182
  * ```typescript
182
183
  * import { nonRetryable } from '@amqp-contract/worker';
183
- * import { Future, Result } from '@swan-io/boxed';
184
+ * import { errAsync, okAsync } from 'neverthrow';
184
185
  *
185
186
  * const handler = ({ payload }) => {
186
187
  * if (!isValidPayload(payload)) {
187
- * return Future.value(Result.Error(nonRetryable('Invalid payload format')));
188
+ * return errAsync(nonRetryable('Invalid payload format'));
188
189
  * }
189
- * return Future.value(Result.Ok(undefined));
190
+ * return okAsync(undefined);
190
191
  * };
191
192
  *
192
193
  * // Equivalent to:
193
- * // return Future.value(Result.Error(new NonRetryableError('Invalid payload format')));
194
+ * // return errAsync(new NonRetryableError('Invalid payload format'));
194
195
  * ```
195
196
  */
196
197
  function nonRetryable(message, cause) {
@@ -224,7 +225,7 @@ function handleError(ctx, error, msg, consumerName, consumer) {
224
225
  error: error.message
225
226
  });
226
227
  sendToDLQ(ctx, msg, consumer);
227
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
228
+ return (0, neverthrow.okAsync)(void 0);
228
229
  }
229
230
  const config = (0, _amqp_contract_contract.extractQueue)(consumer.queue).retry;
230
231
  if (config.mode === "immediate-requeue") return handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, config);
@@ -234,7 +235,7 @@ function handleError(ctx, error, msg, consumerName, consumer) {
234
235
  error: error.message
235
236
  });
236
237
  sendToDLQ(ctx, msg, consumer);
237
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
238
+ return (0, neverthrow.okAsync)(void 0);
238
239
  }
239
240
  /**
240
241
  * Handle error by requeuing immediately.
@@ -258,7 +259,7 @@ function handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, co
258
259
  error: error.message
259
260
  });
260
261
  sendToDLQ(ctx, msg, consumer);
261
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
262
+ return (0, neverthrow.okAsync)(void 0);
262
263
  }
263
264
  ctx.logger?.warn("Retrying message (immediate-requeue mode)", {
264
265
  consumerName,
@@ -269,7 +270,7 @@ function handleErrorImmediateRequeue(ctx, error, msg, consumerName, consumer, co
269
270
  });
270
271
  if (queue.type === "quorum") {
271
272
  ctx.amqpClient.nack(msg, false, true);
272
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
273
+ return (0, neverthrow.okAsync)(void 0);
273
274
  } else return publishForRetry(ctx, {
274
275
  msg,
275
276
  exchange: msg.fields.exchange,
@@ -310,7 +311,7 @@ function handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config)
310
311
  consumerName,
311
312
  queueName: consumer.queue.name
312
313
  });
313
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError("Queue does not have TTL-backoff infrastructure")));
314
+ return (0, neverthrow.errAsync)(new _amqp_contract_core.TechnicalError("Queue does not have TTL-backoff infrastructure"));
314
315
  }
315
316
  const queueEntry = consumer.queue;
316
317
  const queueName = (0, _amqp_contract_contract.extractQueue)(queueEntry).name;
@@ -324,7 +325,7 @@ function handleErrorTtlBackoff(ctx, error, msg, consumerName, consumer, config)
324
325
  error: error.message
325
326
  });
326
327
  sendToDLQ(ctx, msg, consumer);
327
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
328
+ return (0, neverthrow.okAsync)(void 0);
328
329
  }
329
330
  const delayMs = calculateRetryDelay(retryCount, config);
330
331
  ctx.logger?.warn("Retrying message (ttl-backoff mode)", {
@@ -371,10 +372,10 @@ function parseMessageContentForRetry(ctx, msg, queueName) {
371
372
  if (!(contentType === void 0 || contentType === "application/json" || contentType.startsWith("application/json;") || contentType.endsWith("+json"))) return msg.content;
372
373
  try {
373
374
  return JSON.parse(msg.content.toString());
374
- } catch (err) {
375
+ } catch (parseErr) {
375
376
  ctx.logger?.warn("Failed to parse JSON message for retry, using original buffer", {
376
377
  queueName,
377
- error: err
378
+ error: parseErr
378
379
  });
379
380
  return msg.content;
380
381
  }
@@ -399,21 +400,21 @@ function publishForRetry(ctx, { msg, exchange, routingKey, queueName, waitQueueN
399
400
  "x-retry-queue": queueName
400
401
  } : {}
401
402
  }
402
- }).mapOkToResult((published) => {
403
+ }).andThen((published) => {
403
404
  if (!published) {
404
405
  ctx.logger?.error("Failed to publish message for retry (write buffer full)", {
405
406
  queueName,
406
407
  retryCount: newRetryCount,
407
408
  ...delayMs !== void 0 ? { delayMs } : {}
408
409
  });
409
- return _swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError("Failed to publish message for retry (write buffer full)"));
410
+ return (0, neverthrow.err)(new _amqp_contract_core.TechnicalError("Failed to publish message for retry (write buffer full)"));
410
411
  }
411
412
  ctx.logger?.info("Message published for retry", {
412
413
  queueName,
413
414
  retryCount: newRetryCount,
414
415
  ...delayMs !== void 0 ? { delayMs } : {}
415
416
  });
416
- return _swan_io_boxed.Result.Ok(void 0);
417
+ return (0, neverthrow.ok)(void 0);
417
418
  });
418
419
  }
419
420
  /**
@@ -450,6 +451,7 @@ function isHandlerTuple(entry) {
450
451
  * ```typescript
451
452
  * import { TypedAmqpWorker } from '@amqp-contract/worker';
452
453
  * import { defineQueue, defineMessage, defineContract, defineConsumer } from '@amqp-contract/contract';
454
+ * import { okAsync } from 'neverthrow';
453
455
  * import { z } from 'zod';
454
456
  *
455
457
  * const orderQueue = defineQueue('order-processing');
@@ -464,19 +466,22 @@ function isHandlerTuple(entry) {
464
466
  * }
465
467
  * });
466
468
  *
467
- * const worker = await TypedAmqpWorker.create({
469
+ * const result = await TypedAmqpWorker.create({
468
470
  * contract,
469
471
  * handlers: {
470
- * processOrder: async (message) => {
471
- * console.log('Processing order', message.orderId);
472
- * // Process the order...
473
- * }
472
+ * processOrder: ({ payload }) => {
473
+ * console.log('Processing order', payload.orderId);
474
+ * return okAsync(undefined);
475
+ * },
474
476
  * },
475
- * urls: ['amqp://localhost']
476
- * }).resultToPromise();
477
+ * urls: ['amqp://localhost'],
478
+ * });
479
+ *
480
+ * if (result.isErr()) throw result.error;
481
+ * const worker = result.value;
477
482
  *
478
483
  * // Close when done
479
- * await worker.close().resultToPromise();
484
+ * await worker.close();
480
485
  * ```
481
486
  */
482
487
  var TypedAmqpWorker = class TypedAmqpWorker {
@@ -552,18 +557,17 @@ var TypedAmqpWorker = class TypedAmqpWorker {
552
557
  * Connections are automatically shared across clients and workers with the same
553
558
  * URLs and connection options, following RabbitMQ best practices.
554
559
  *
555
- * @param options - Configuration options for the worker
556
- * @returns A Future that resolves to a Result containing the worker or an error
560
+ * @returns A ResultAsync that resolves to the worker or a TechnicalError.
557
561
  *
558
562
  * @example
559
563
  * ```typescript
560
- * const worker = await TypedAmqpWorker.create({
564
+ * const result = await TypedAmqpWorker.create({
561
565
  * contract: myContract,
562
566
  * handlers: {
563
- * processOrder: async ({ payload }) => console.log('Order:', payload.orderId)
567
+ * processOrder: ({ payload }) => okAsync(undefined),
564
568
  * },
565
- * urls: ['amqp://localhost']
566
- * }).resultToPromise();
569
+ * urls: ['amqp://localhost'],
570
+ * });
567
571
  * ```
568
572
  */
569
573
  static create({ contract, handlers, urls, connectionOptions, defaultConsumerOptions, logger, telemetry, connectTimeoutMs }) {
@@ -572,12 +576,14 @@ var TypedAmqpWorker = class TypedAmqpWorker {
572
576
  connectionOptions,
573
577
  connectTimeoutMs
574
578
  }), handlers, defaultConsumerOptions ?? {}, logger, telemetry);
575
- return worker.waitForConnectionReady().flatMapOk(() => worker.consumeAll()).flatMap((result) => result.match({
576
- Ok: () => _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(worker)),
577
- Error: (error) => worker.close().tapError((closeError) => {
578
- logger?.warn("Failed to close worker after setup failure", { error: closeError });
579
- }).map(() => _swan_io_boxed.Result.Error(error))
580
- }));
579
+ const setup = worker.waitForConnectionReady().andThen(() => worker.consumeAll());
580
+ return new neverthrow.ResultAsync((async () => {
581
+ const setupResult = await setup;
582
+ if (setupResult.isOk()) return (0, neverthrow.ok)(worker);
583
+ const closeResult = await worker.close();
584
+ if (closeResult.isErr()) logger?.warn("Failed to close worker after setup failure", { error: closeResult.error });
585
+ return (0, neverthrow.err)(setupResult.error);
586
+ })());
581
587
  }
582
588
  /**
583
589
  * Close the AMQP channel and connection.
@@ -585,26 +591,25 @@ var TypedAmqpWorker = class TypedAmqpWorker {
585
591
  * This gracefully closes the connection to the AMQP broker,
586
592
  * stopping all message consumption and cleaning up resources.
587
593
  *
588
- * @returns A Future that resolves to a Result indicating success or failure
589
- *
590
594
  * @example
591
595
  * ```typescript
592
- * const closeResult = await worker.close().resultToPromise();
596
+ * const closeResult = await worker.close();
593
597
  * if (closeResult.isOk()) {
594
598
  * console.log('Worker closed successfully');
595
599
  * }
596
600
  * ```
597
601
  */
598
602
  close() {
599
- return _swan_io_boxed.Future.all(Array.from(this.consumerTags).map((consumerTag) => this.amqpClient.cancel(consumerTag).mapErrorToResult((error) => {
603
+ const cancellations = Array.from(this.consumerTags).map((consumerTag) => this.amqpClient.cancel(consumerTag).orElse((error) => {
600
604
  this.logger?.warn("Failed to cancel consumer during close", {
601
605
  consumerTag,
602
606
  error
603
607
  });
604
- return _swan_io_boxed.Result.Ok(void 0);
605
- }))).map(_swan_io_boxed.Result.all).tapOk(() => {
608
+ return (0, neverthrow.ok)(void 0);
609
+ }));
610
+ return neverthrow.ResultAsync.combine(cancellations).andTee(() => {
606
611
  this.consumerTags.clear();
607
- }).flatMapOk(() => this.amqpClient.close()).mapOk(() => void 0);
612
+ }).andThen(() => this.amqpClient.close()).map(() => void 0);
608
613
  }
609
614
  /**
610
615
  * Start consuming for every entry in `contract.consumers` and `contract.rpcs`.
@@ -613,7 +618,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
613
618
  const consumerNames = Object.keys(this.contract.consumers ?? {});
614
619
  const rpcNames = Object.keys(this.contract.rpcs ?? {});
615
620
  const allNames = [...consumerNames, ...rpcNames];
616
- return _swan_io_boxed.Future.all(allNames.map((name) => this.consume(name))).map(_swan_io_boxed.Result.all).mapOk(() => void 0);
621
+ return neverthrow.ResultAsync.combine(allNames.map((name) => this.consume(name))).map(() => void 0);
617
622
  }
618
623
  waitForConnectionReady() {
619
624
  return this.amqpClient.waitForConnect();
@@ -634,9 +639,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
634
639
  validateSchema(schema, data, context) {
635
640
  const rawValidation = schema["~standard"].validate(data);
636
641
  const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
637
- return _swan_io_boxed.Future.fromPromise(validationPromise).mapError((error) => new _amqp_contract_core.TechnicalError(`Error validating ${context.field}`, error)).mapOkToResult((result) => {
638
- if (result.issues) return _swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError(`${context.field} validation failed`, new _amqp_contract_core.MessageValidationError(context.consumerName, result.issues)));
639
- return _swan_io_boxed.Result.Ok(result.value);
642
+ return neverthrow.ResultAsync.fromPromise(validationPromise, (error) => new _amqp_contract_core.TechnicalError(`Error validating ${context.field}`, error)).andThen((result) => {
643
+ 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)));
644
+ return (0, neverthrow.ok)(result.value);
640
645
  });
641
646
  }
642
647
  /**
@@ -648,18 +653,18 @@ var TypedAmqpWorker = class TypedAmqpWorker {
648
653
  */
649
654
  parseAndValidateMessage(msg, consumer, consumerName) {
650
655
  const context = { consumerName: String(consumerName) };
651
- const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding).mapErrorToResult((error) => _swan_io_boxed.Result.Error(new _amqp_contract_core.TechnicalError("Failed to decompress message", error))).mapOkToResult((buffer) => _swan_io_boxed.Result.fromExecution(() => JSON.parse(buffer.toString())).mapError((error) => new _amqp_contract_core.TechnicalError("Failed to parse JSON", error))).flatMapOk((parsed) => this.validateSchema(consumer.message.payload, parsed, {
656
+ const parsePayload = decompressBuffer(msg.content, msg.properties.contentEncoding).andThen((buffer) => neverthrow.Result.fromThrowable(() => JSON.parse(buffer.toString()), (error) => new _amqp_contract_core.TechnicalError("Failed to parse JSON", error))()).andThen((parsed) => this.validateSchema(consumer.message.payload, parsed, {
652
657
  ...context,
653
658
  field: "payload"
654
659
  }));
655
660
  const parseHeaders = consumer.message.headers ? this.validateSchema(consumer.message.headers, msg.properties.headers ?? {}, {
656
661
  ...context,
657
662
  field: "headers"
658
- }) : _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
659
- return _swan_io_boxed.Future.allFromDict({
660
- payload: parsePayload,
661
- headers: parseHeaders
662
- }).map(_swan_io_boxed.Result.allFromDict);
663
+ }) : (0, neverthrow.okAsync)(void 0);
664
+ return neverthrow.ResultAsync.combine([parsePayload, parseHeaders]).map(([payload, headers]) => ({
665
+ payload,
666
+ headers
667
+ }));
663
668
  }
664
669
  /**
665
670
  * Validate an RPC handler's response and publish it back to the caller's reply
@@ -688,7 +693,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
688
693
  rpcName: String(rpcName),
689
694
  queueName
690
695
  });
691
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new NonRetryableError(`RPC "${String(rpcName)}" received a message without replyTo; cannot deliver response`)));
696
+ return (0, neverthrow.errAsync)(new NonRetryableError(`RPC "${String(rpcName)}" received a message without replyTo; cannot deliver response`));
692
697
  }
693
698
  if (typeof correlationId !== "string" || correlationId.length === 0) {
694
699
  this.logger?.error("RPC handler returned a response but the incoming message has no correlationId", {
@@ -696,22 +701,22 @@ var TypedAmqpWorker = class TypedAmqpWorker {
696
701
  queueName,
697
702
  replyTo
698
703
  });
699
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new NonRetryableError(`RPC "${String(rpcName)}" received a message without correlationId; cannot deliver response`)));
704
+ return (0, neverthrow.errAsync)(new NonRetryableError(`RPC "${String(rpcName)}" received a message without correlationId; cannot deliver response`));
700
705
  }
701
706
  let rawValidation;
702
707
  try {
703
708
  rawValidation = responseSchema["~standard"].validate(response);
704
709
  } catch (error) {
705
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(new NonRetryableError("RPC response schema validation threw", error)));
710
+ return (0, neverthrow.errAsync)(new NonRetryableError("RPC response schema validation threw", error));
706
711
  }
707
712
  const validationPromise = rawValidation instanceof Promise ? rawValidation : Promise.resolve(rawValidation);
708
- return _swan_io_boxed.Future.fromPromise(validationPromise).mapError((error) => new NonRetryableError("RPC response schema validation threw", error)).mapOkToResult((validation) => {
709
- if (validation.issues) return _swan_io_boxed.Result.Error(new NonRetryableError(`RPC response for "${String(rpcName)}" failed schema validation`, new _amqp_contract_core.MessageValidationError(String(rpcName), validation.issues)));
710
- return _swan_io_boxed.Result.Ok(validation.value);
711
- }).flatMapOk((validatedResponse) => this.amqpClient.publish("", replyTo, validatedResponse, {
713
+ return neverthrow.ResultAsync.fromPromise(validationPromise, (error) => new NonRetryableError("RPC response schema validation threw", error)).andThen((validation) => {
714
+ 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)));
715
+ return (0, neverthrow.ok)(validation.value);
716
+ }).andThen((validatedResponse) => this.amqpClient.publish("", replyTo, validatedResponse, {
712
717
  correlationId,
713
718
  contentType: "application/json"
714
- }).mapErrorToResult((error) => _swan_io_boxed.Result.Error(new NonRetryableError("Failed to publish RPC response", error))).mapOkToResult((published) => published ? _swan_io_boxed.Result.Ok(void 0) : _swan_io_boxed.Result.Error(new NonRetryableError("Failed to publish RPC response: channel buffer full"))));
719
+ }).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"))));
715
720
  }
716
721
  /**
717
722
  * Process a single consumed message: validate, invoke handler, optionally
@@ -724,58 +729,56 @@ var TypedAmqpWorker = class TypedAmqpWorker {
724
729
  const span = (0, _amqp_contract_core.startConsumeSpan)(this.telemetry, queueName, String(name), { "messaging.rabbitmq.message.delivery_tag": msg.fields.deliveryTag });
725
730
  let messageHandled = false;
726
731
  let firstError;
727
- return this.parseAndValidateMessage(msg, consumer, name).flatMap((parseResult) => parseResult.match({
728
- Ok: (validatedMessage) => handler(validatedMessage, msg).flatMapOk((handlerResponse) => {
729
- if (isRpc && responseSchema) return this.publishRpcResponse(msg, queueName, name, responseSchema, handlerResponse).flatMapOk(() => {
730
- this.logger?.info("Message consumed successfully", {
731
- consumerName: String(name),
732
- queueName
733
- });
734
- this.amqpClient.ack(msg);
735
- messageHandled = true;
736
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
737
- });
732
+ const inner = this.parseAndValidateMessage(msg, consumer, name).orElse((parseError) => {
733
+ firstError = parseError;
734
+ this.logger?.error("Failed to parse/validate message; sending to DLQ", {
735
+ consumerName: String(name),
736
+ queueName,
737
+ error: parseError
738
+ });
739
+ this.amqpClient.nack(msg, false, false);
740
+ return (0, neverthrow.errAsync)(parseError);
741
+ }).andThen((validatedMessage) => handler(validatedMessage, msg).andThen((handlerResponse) => {
742
+ if (isRpc && responseSchema) return this.publishRpcResponse(msg, queueName, name, responseSchema, handlerResponse).map(() => {
738
743
  this.logger?.info("Message consumed successfully", {
739
744
  consumerName: String(name),
740
745
  queueName
741
746
  });
742
747
  this.amqpClient.ack(msg);
743
748
  messageHandled = true;
744
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Ok(void 0));
745
- }).flatMapError((handlerError) => {
746
- this.logger?.error("Error processing message", {
747
- consumerName: String(name),
748
- queueName,
749
- errorType: handlerError.name,
750
- error: handlerError.message
751
- });
752
- firstError = handlerError;
753
- return handleError({
754
- amqpClient: this.amqpClient,
755
- logger: this.logger
756
- }, handlerError, msg, String(name), consumer);
757
- }),
758
- Error: (parseError) => {
759
- firstError = parseError;
760
- this.logger?.error("Failed to parse/validate message; sending to DLQ", {
761
- consumerName: String(name),
762
- queueName,
763
- error: parseError
764
- });
765
- this.amqpClient.nack(msg, false, false);
766
- return _swan_io_boxed.Future.value(_swan_io_boxed.Result.Error(parseError));
767
- }
768
- })).map((result) => {
749
+ });
750
+ this.logger?.info("Message consumed successfully", {
751
+ consumerName: String(name),
752
+ queueName
753
+ });
754
+ this.amqpClient.ack(msg);
755
+ messageHandled = true;
756
+ return (0, neverthrow.okAsync)(void 0);
757
+ }).orElse((handlerError) => {
758
+ this.logger?.error("Error processing message", {
759
+ consumerName: String(name),
760
+ queueName,
761
+ errorType: handlerError.name,
762
+ error: handlerError.message
763
+ });
764
+ firstError = handlerError;
765
+ return handleError({
766
+ amqpClient: this.amqpClient,
767
+ logger: this.logger
768
+ }, handlerError, msg, String(name), consumer);
769
+ }));
770
+ return new neverthrow.ResultAsync((async () => {
771
+ const result = await inner;
769
772
  const durationMs = Date.now() - startTime;
770
773
  if (messageHandled) {
771
774
  (0, _amqp_contract_core.endSpanSuccess)(span);
772
775
  (0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(name), true, durationMs);
773
776
  } else {
774
- (0, _amqp_contract_core.endSpanError)(span, result.isError() ? result.error : firstError ?? /* @__PURE__ */ new Error("Unknown error"));
777
+ (0, _amqp_contract_core.endSpanError)(span, result.isErr() ? result.error : firstError ?? /* @__PURE__ */ new Error("Unknown error"));
775
778
  (0, _amqp_contract_core.recordConsumeMetric)(this.telemetry, queueName, String(name), false, durationMs);
776
779
  }
777
780
  return result;
778
- });
781
+ })());
779
782
  }
780
783
  /**
781
784
  * Consume messages one at a time.
@@ -791,7 +794,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
791
794
  return;
792
795
  }
793
796
  try {
794
- await this.processMessage(msg, view, name, handler).toPromise();
797
+ await this.processMessage(msg, view, name, handler);
795
798
  } catch (error) {
796
799
  this.logger?.error("Uncaught error in consume callback; nacking message", {
797
800
  consumerName: String(name),
@@ -800,9 +803,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
800
803
  });
801
804
  this.amqpClient.nack(msg, false, false);
802
805
  }
803
- }, this.consumerOptions[name]).tapOk((consumerTag) => {
806
+ }, this.consumerOptions[name]).andTee((consumerTag) => {
804
807
  this.consumerTags.add(consumerTag);
805
- }).mapError((error) => new _amqp_contract_core.TechnicalError(`Failed to start consuming for "${String(name)}"`, error)).mapOk(() => void 0);
808
+ }).map(() => void 0).mapErr((error) => new _amqp_contract_core.TechnicalError(`Failed to start consuming for "${String(name)}"`, error));
806
809
  }
807
810
  };
808
811
  //#endregion
@@ -835,7 +838,7 @@ function defineHandler(contract, consumerName, handler, options) {
835
838
  /**
836
839
  * Define multiple type-safe handlers for consumers in a contract.
837
840
  *
838
- * **Recommended:** This function creates handlers that return `Future<Result<void, HandlerError>>`,
841
+ * **Recommended:** This function creates handlers that return `ResultAsync<void, HandlerError>`,
839
842
  * providing explicit error handling and better control over retry behavior.
840
843
  *
841
844
  * @template TContract - The contract definition type
@@ -846,18 +849,20 @@ function defineHandler(contract, consumerName, handler, options) {
846
849
  * @example
847
850
  * ```typescript
848
851
  * import { defineHandlers, RetryableError } from '@amqp-contract/worker';
849
- * import { Future } from '@swan-io/boxed';
852
+ * import { ResultAsync } from 'neverthrow';
850
853
  * import { orderContract } from './contract';
851
854
  *
852
855
  * const handlers = defineHandlers(orderContract, {
853
856
  * processOrder: ({ payload }) =>
854
- * Future.fromPromise(processPayment(payload))
855
- * .mapOk(() => undefined)
856
- * .mapError((error) => new RetryableError('Payment failed', error)),
857
+ * ResultAsync.fromPromise(
858
+ * processPayment(payload),
859
+ * (error) => new RetryableError('Payment failed', error),
860
+ * ).map(() => undefined),
857
861
  * notifyOrder: ({ payload }) =>
858
- * Future.fromPromise(sendNotification(payload))
859
- * .mapOk(() => undefined)
860
- * .mapError((error) => new RetryableError('Notification failed', error)),
862
+ * ResultAsync.fromPromise(
863
+ * sendNotification(payload),
864
+ * (error) => new RetryableError('Notification failed', error),
865
+ * ).map(() => undefined),
861
866
  * });
862
867
  * ```
863
868
  */