@amqp-contract/worker 0.20.0 → 0.21.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,9 +1,40 @@
1
+ import { extractConsumer, extractQueue, isQueueWithTtlBackoffInfrastructure } from "@amqp-contract/contract";
1
2
  import { AmqpClient, MessageValidationError, TechnicalError, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, startConsumeSpan } from "@amqp-contract/core";
2
- import { extractConsumer } from "@amqp-contract/contract";
3
3
  import { Future, Result } from "@swan-io/boxed";
4
4
  import { gunzip, inflate } from "node:zlib";
5
5
  import { promisify } from "node:util";
6
-
6
+ //#region src/decompression.ts
7
+ const gunzipAsync = promisify(gunzip);
8
+ const inflateAsync = promisify(inflate);
9
+ /**
10
+ * Supported content encodings for message decompression.
11
+ */
12
+ const SUPPORTED_ENCODINGS = ["gzip", "deflate"];
13
+ /**
14
+ * Type guard to check if a string is a supported encoding.
15
+ */
16
+ function isSupportedEncoding(encoding) {
17
+ return SUPPORTED_ENCODINGS.includes(encoding.toLowerCase());
18
+ }
19
+ /**
20
+ * Decompress a buffer based on the content-encoding header.
21
+ *
22
+ * @param buffer - The buffer to decompress
23
+ * @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')
24
+ * @returns A Future with the decompressed buffer or a TechnicalError
25
+ *
26
+ * @internal
27
+ */
28
+ function decompressBuffer(buffer, contentEncoding) {
29
+ if (!contentEncoding) return Future.value(Result.Ok(buffer));
30
+ const normalizedEncoding = contentEncoding.toLowerCase();
31
+ if (!isSupportedEncoding(normalizedEncoding)) return Future.value(Result.Error(new TechnicalError(`Unsupported content-encoding: "${contentEncoding}". Supported encodings are: ${SUPPORTED_ENCODINGS.join(", ")}. Please check your publisher configuration.`)));
32
+ switch (normalizedEncoding) {
33
+ case "gzip": return Future.fromPromise(gunzipAsync(buffer)).mapError((error) => new TechnicalError("Failed to decompress gzip", error));
34
+ case "deflate": return Future.fromPromise(inflateAsync(buffer)).mapError((error) => new TechnicalError("Failed to decompress deflate", error));
35
+ }
36
+ }
37
+ //#endregion
7
38
  //#region src/errors.ts
8
39
  /**
9
40
  * Retryable errors - transient failures that may succeed on retry
@@ -164,40 +195,6 @@ function retryable(message, cause) {
164
195
  function nonRetryable(message, cause) {
165
196
  return new NonRetryableError(message, cause);
166
197
  }
167
-
168
- //#endregion
169
- //#region src/decompression.ts
170
- const gunzipAsync = promisify(gunzip);
171
- const inflateAsync = promisify(inflate);
172
- /**
173
- * Supported content encodings for message decompression.
174
- */
175
- const SUPPORTED_ENCODINGS = ["gzip", "deflate"];
176
- /**
177
- * Type guard to check if a string is a supported encoding.
178
- */
179
- function isSupportedEncoding(encoding) {
180
- return SUPPORTED_ENCODINGS.includes(encoding.toLowerCase());
181
- }
182
- /**
183
- * Decompress a buffer based on the content-encoding header.
184
- *
185
- * @param buffer - The buffer to decompress
186
- * @param contentEncoding - The content-encoding header value (e.g., 'gzip', 'deflate')
187
- * @returns A Future with the decompressed buffer or a TechnicalError
188
- *
189
- * @internal
190
- */
191
- function decompressBuffer(buffer, contentEncoding) {
192
- if (!contentEncoding) return Future.value(Result.Ok(buffer));
193
- const normalizedEncoding = contentEncoding.toLowerCase();
194
- if (!isSupportedEncoding(normalizedEncoding)) return Future.value(Result.Error(new TechnicalError(`Unsupported content-encoding: "${contentEncoding}". Supported encodings are: ${SUPPORTED_ENCODINGS.join(", ")}. Please check your publisher configuration.`)));
195
- switch (normalizedEncoding) {
196
- case "gzip": return Future.fromPromise(gunzipAsync(buffer)).mapError((error) => new TechnicalError("Failed to decompress gzip", error));
197
- case "deflate": return Future.fromPromise(inflateAsync(buffer)).mapError((error) => new TechnicalError("Failed to decompress deflate", error));
198
- }
199
- }
200
-
201
198
  //#endregion
202
199
  //#region src/worker.ts
203
200
  /**
@@ -220,7 +217,7 @@ function isHandlerTuple(entry) {
220
217
  * import { defineQueue, defineMessage, defineContract, defineConsumer } from '@amqp-contract/contract';
221
218
  * import { z } from 'zod';
222
219
  *
223
- * const orderQueue = defineQueue('order-processing', { durable: true });
220
+ * const orderQueue = defineQueue('order-processing');
224
221
  * const orderMessage = defineMessage(z.object({
225
222
  * orderId: z.string(),
226
223
  * amount: z.number()
@@ -255,9 +252,10 @@ var TypedAmqpWorker = class TypedAmqpWorker {
255
252
  consumerOptions;
256
253
  consumerTags = /* @__PURE__ */ new Set();
257
254
  telemetry;
258
- constructor(contract, amqpClient, handlers, logger, telemetry) {
255
+ constructor(contract, amqpClient, handlers, defaultConsumerOptions, logger, telemetry) {
259
256
  this.contract = contract;
260
257
  this.amqpClient = amqpClient;
258
+ this.defaultConsumerOptions = defaultConsumerOptions;
261
259
  this.logger = logger;
262
260
  this.telemetry = telemetry ?? defaultTelemetryProvider;
263
261
  this.actualHandlers = {};
@@ -269,8 +267,14 @@ var TypedAmqpWorker = class TypedAmqpWorker {
269
267
  if (isHandlerTuple(handlerEntry)) {
270
268
  const [handler, options] = handlerEntry;
271
269
  this.actualHandlers[typedConsumerName] = handler;
272
- this.consumerOptions[typedConsumerName] = options;
273
- } else this.actualHandlers[typedConsumerName] = handlerEntry;
270
+ this.consumerOptions[typedConsumerName] = {
271
+ ...this.defaultConsumerOptions,
272
+ ...options
273
+ };
274
+ } else {
275
+ this.actualHandlers[typedConsumerName] = handlerEntry;
276
+ this.consumerOptions[typedConsumerName] = this.defaultConsumerOptions;
277
+ }
274
278
  }
275
279
  }
276
280
  /**
@@ -298,11 +302,11 @@ var TypedAmqpWorker = class TypedAmqpWorker {
298
302
  * }).resultToPromise();
299
303
  * ```
300
304
  */
301
- static create({ contract, handlers, urls, connectionOptions, logger, telemetry }) {
305
+ static create({ contract, handlers, urls, connectionOptions, defaultConsumerOptions, logger, telemetry }) {
302
306
  const worker = new TypedAmqpWorker(contract, new AmqpClient(contract, {
303
307
  urls,
304
308
  connectionOptions
305
- }), handlers, logger, telemetry);
309
+ }), handlers, defaultConsumerOptions ?? {}, logger, telemetry);
306
310
  return worker.waitForConnectionReady().flatMapOk(() => worker.consumeAll()).mapOk(() => worker);
307
311
  }
308
312
  /**
@@ -337,7 +341,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
337
341
  * Defaults are applied in the contract's defineQueue, so we just return the config.
338
342
  */
339
343
  getRetryConfigForConsumer(consumer) {
340
- return consumer.queue.retry;
344
+ return extractQueue(consumer.queue).retry;
341
345
  }
342
346
  /**
343
347
  * Start consuming messages for all consumers.
@@ -346,13 +350,6 @@ var TypedAmqpWorker = class TypedAmqpWorker {
346
350
  consumeAll() {
347
351
  const consumers = this.contract.consumers;
348
352
  const consumerNames = Object.keys(consumers);
349
- const maxPrefetch = consumerNames.reduce((max, name) => {
350
- const prefetch = this.consumerOptions[name]?.prefetch;
351
- return prefetch ? Math.max(max, prefetch) : max;
352
- }, 0);
353
- if (maxPrefetch > 0) this.amqpClient.addSetup(async (channel) => {
354
- await channel.prefetch(maxPrefetch);
355
- });
356
353
  return Future.all(consumerNames.map((name) => this.consume(name))).map(Result.all).mapOk(() => void 0);
357
354
  }
358
355
  waitForConnectionReady() {
@@ -391,9 +388,10 @@ var TypedAmqpWorker = class TypedAmqpWorker {
391
388
  * @returns Ok with validated message (payload + headers), or Error (message already nacked)
392
389
  */
393
390
  parseAndValidateMessage(msg, consumer, consumerName) {
391
+ const queue = extractQueue(consumer.queue);
394
392
  const context = {
395
393
  consumerName: String(consumerName),
396
- queueName: consumer.queue.name
394
+ queueName: queue.name
397
395
  };
398
396
  const nackAndError = (message, error) => {
399
397
  this.logger?.error(message, {
@@ -426,7 +424,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
426
424
  * Consume messages one at a time
427
425
  */
428
426
  consumeSingle(consumerName, consumer, handler) {
429
- const queueName = consumer.queue.name;
427
+ const queueName = extractQueue(consumer.queue).name;
430
428
  return this.amqpClient.consume(queueName, async (msg) => {
431
429
  if (msg === null) {
432
430
  this.logger?.warn("Consumer cancelled by server", {
@@ -463,7 +461,7 @@ var TypedAmqpWorker = class TypedAmqpWorker {
463
461
  endSpanError(span, /* @__PURE__ */ new Error("Message validation failed"));
464
462
  recordConsumeMetric(this.telemetry, queueName, String(consumerName), false, durationMs);
465
463
  }).toPromise();
466
- }).tapOk((consumerTag) => {
464
+ }, this.consumerOptions[consumerName]).tapOk((consumerTag) => {
467
465
  this.consumerTags.add(consumerTag);
468
466
  }).mapError((error) => new TechnicalError(`Failed to start consuming for "${String(consumerName)}"`, error)).mapOk(() => void 0);
469
467
  }
@@ -472,17 +470,18 @@ var TypedAmqpWorker = class TypedAmqpWorker {
472
470
  *
473
471
  * Flow depends on retry mode:
474
472
  *
475
- * **quorum-native mode:**
473
+ * **immediate-requeue mode:**
476
474
  * 1. If NonRetryableError -> send directly to DLQ (no retry)
477
- * 2. Otherwise -> nack with requeue=true (RabbitMQ handles delivery count)
475
+ * 2. If max retries exceeded -> send to DLQ
476
+ * 3. Otherwise -> requeue immediately for retry
478
477
  *
479
478
  * **ttl-backoff mode:**
480
479
  * 1. If NonRetryableError -> send directly to DLQ (no retry)
481
480
  * 2. If max retries exceeded -> send to DLQ
482
481
  * 3. Otherwise -> publish to wait queue with TTL for retry
483
482
  *
484
- * **Legacy mode (no retry config):**
485
- * 1. nack with requeue=true (immediate requeue)
483
+ * **none mode (no retry config):**
484
+ * 1. send directly to DLQ (no retry)
486
485
  */
487
486
  handleError(error, msg, consumerName, consumer) {
488
487
  if (error instanceof NonRetryableError) {
@@ -495,52 +494,98 @@ var TypedAmqpWorker = class TypedAmqpWorker {
495
494
  return Future.value(Result.Ok(void 0));
496
495
  }
497
496
  const config = this.getRetryConfigForConsumer(consumer);
498
- if (config.mode === "quorum-native") return this.handleErrorQuorumNative(error, msg, consumerName, consumer);
499
- return this.handleErrorTtlBackoff(error, msg, consumerName, consumer, config);
497
+ if (config.mode === "immediate-requeue") return this.handleErrorImmediateRequeue(error, msg, consumerName, consumer, config);
498
+ if (config.mode === "ttl-backoff") return this.handleErrorTtlBackoff(error, msg, consumerName, consumer, config);
499
+ this.logger?.warn("Retry disabled (none mode), sending to DLQ", {
500
+ consumerName,
501
+ error: error.message
502
+ });
503
+ this.sendToDLQ(msg, consumer);
504
+ return Future.value(Result.Ok(void 0));
500
505
  }
501
506
  /**
502
- * Handle error using quorum queue's native delivery limit feature.
507
+ * Handle error by requeuing immediately.
503
508
  *
504
- * Simply requeues the message with nack(requeue=true). RabbitMQ automatically:
505
- * - Increments x-delivery-count header
506
- * - Dead-letters the message when count exceeds x-delivery-limit
509
+ * For quorum queues, messages are requeued with `nack(requeue=true)`, and the worker tracks delivery count via the native RabbitMQ `x-delivery-count` header.
510
+ * For classic queues, messages are re-published on the same queue, and the worker tracks delivery count via a custom `x-retry-count` header.
511
+ * When the count exceeds `maxRetries`, the message is automatically dead-lettered (if DLX is configured) or dropped.
507
512
  *
508
513
  * This is simpler than TTL-based retry but provides immediate retries only.
509
514
  */
510
- handleErrorQuorumNative(error, msg, consumerName, consumer) {
511
- const queue = consumer.queue;
515
+ handleErrorImmediateRequeue(error, msg, consumerName, consumer, config) {
516
+ const queue = extractQueue(consumer.queue);
512
517
  const queueName = queue.name;
513
- const deliveryCount = msg.properties.headers?.["x-delivery-count"] ?? 0;
514
- const deliveryLimit = queue.type === "quorum" ? queue.deliveryLimit : void 0;
515
- const attemptsBeforeDeadLetter = deliveryLimit !== void 0 ? Math.max(0, deliveryLimit - deliveryCount - 1) : "unknown";
516
- if (deliveryLimit !== void 0 && deliveryCount >= deliveryLimit - 1) this.logger?.warn("Message at final delivery attempt (quorum-native mode)", {
518
+ const retryCount = queue.type === "quorum" ? msg.properties.headers?.["x-delivery-count"] ?? 0 : msg.properties.headers?.["x-retry-count"] ?? 0;
519
+ if (retryCount >= config.maxRetries) {
520
+ this.logger?.error("Max retries exceeded, sending to DLQ (immediate-requeue mode)", {
521
+ consumerName,
522
+ queueName,
523
+ retryCount,
524
+ maxRetries: config.maxRetries,
525
+ error: error.message
526
+ });
527
+ this.sendToDLQ(msg, consumer);
528
+ return Future.value(Result.Ok(void 0));
529
+ }
530
+ this.logger?.warn("Retrying message (immediate-requeue mode)", {
517
531
  consumerName,
518
532
  queueName,
519
- deliveryCount,
520
- deliveryLimit,
521
- willDeadLetterOnNextFailure: deliveryCount === deliveryLimit - 1,
522
- alreadyExceededLimit: deliveryCount >= deliveryLimit,
533
+ retryCount,
534
+ maxRetries: config.maxRetries,
523
535
  error: error.message
524
536
  });
525
- else this.logger?.warn("Retrying message (quorum-native mode)", {
526
- consumerName,
537
+ if (queue.type === "quorum") {
538
+ this.amqpClient.nack(msg, false, true);
539
+ return Future.value(Result.Ok(void 0));
540
+ } else return this.publishForRetry({
541
+ msg,
542
+ exchange: msg.fields.exchange,
543
+ routingKey: msg.fields.routingKey,
527
544
  queueName,
528
- deliveryCount,
529
- deliveryLimit,
530
- attemptsBeforeDeadLetter,
531
- error: error.message
545
+ error
532
546
  });
533
- this.amqpClient.nack(msg, false, true);
534
- return Future.value(Result.Ok(void 0));
535
547
  }
536
548
  /**
537
549
  * Handle error using TTL + wait queue pattern for exponential backoff.
550
+ *
551
+ * ┌─────────────────────────────────────────────────────────────────┐
552
+ * │ Retry Flow (Native RabbitMQ TTL + Wait queue pattern) │
553
+ * ├─────────────────────────────────────────────────────────────────┤
554
+ * │ │
555
+ * │ 1. Handler throws any Error │
556
+ * │ ↓ │
557
+ * │ 2. Worker publishes to wait exchange |
558
+ * | (with header `x-wait-queue` set to the wait queue name) │
559
+ * │ ↓ │
560
+ * │ 3. Wait exchange routes to wait queue │
561
+ * │ (with expiration: calculated backoff delay) │
562
+ * │ ↓ │
563
+ * │ 4. Message waits in queue until TTL expires │
564
+ * │ ↓ │
565
+ * │ 5. Expired message dead-lettered to retry exchange |
566
+ * | (with header `x-retry-queue` set to the main queue name) │
567
+ * │ ↓ │
568
+ * │ 6. Retry exchange routes back to main queue → RETRY │
569
+ * │ ↓ │
570
+ * │ 7. If retries exhausted: nack without requeue → DLQ │
571
+ * │ │
572
+ * └─────────────────────────────────────────────────────────────────┘
538
573
  */
539
574
  handleErrorTtlBackoff(error, msg, consumerName, consumer, config) {
575
+ if (!isQueueWithTtlBackoffInfrastructure(consumer.queue)) {
576
+ this.logger?.error("Queue does not have TTL-backoff infrastructure", {
577
+ consumerName,
578
+ queueName: consumer.queue.name
579
+ });
580
+ return Future.value(Result.Error(new TechnicalError("Queue does not have TTL-backoff infrastructure")));
581
+ }
582
+ const queueEntry = consumer.queue;
583
+ const queueName = extractQueue(queueEntry).name;
540
584
  const retryCount = msg.properties.headers?.["x-retry-count"] ?? 0;
541
585
  if (retryCount >= config.maxRetries) {
542
- this.logger?.error("Max retries exceeded, sending to DLQ", {
586
+ this.logger?.error("Max retries exceeded, sending to DLQ (ttl-backoff mode)", {
543
587
  consumerName,
588
+ queueName,
544
589
  retryCount,
545
590
  maxRetries: config.maxRetries,
546
591
  error: error.message
@@ -551,11 +596,21 @@ var TypedAmqpWorker = class TypedAmqpWorker {
551
596
  const delayMs = this.calculateRetryDelay(retryCount, config);
552
597
  this.logger?.warn("Retrying message (ttl-backoff mode)", {
553
598
  consumerName,
599
+ queueName,
554
600
  retryCount: retryCount + 1,
601
+ maxRetries: config.maxRetries,
555
602
  delayMs,
556
603
  error: error.message
557
604
  });
558
- return this.publishForRetry(msg, consumer, retryCount + 1, delayMs, error);
605
+ return this.publishForRetry({
606
+ msg,
607
+ exchange: queueEntry.waitExchange.name,
608
+ routingKey: msg.fields.routingKey,
609
+ waitQueueName: queueEntry.waitQueue.name,
610
+ queueName,
611
+ delayMs,
612
+ error
613
+ });
559
614
  }
560
615
  /**
561
616
  * Calculate retry delay with exponential backoff and optional jitter.
@@ -583,65 +638,38 @@ var TypedAmqpWorker = class TypedAmqpWorker {
583
638
  return content;
584
639
  }
585
640
  /**
586
- * Publish message to wait queue for retry after TTL expires.
587
- *
588
- * ┌─────────────────────────────────────────────────────────────────┐
589
- * │ Retry Flow (Native RabbitMQ TTL + DLX Pattern) │
590
- * ├─────────────────────────────────────────────────────────────────┤
591
- * │ │
592
- * │ 1. Handler throws any Error │
593
- * │ ↓ │
594
- * │ 2. Worker publishes to DLX with routing key: {queue}-wait │
595
- * │ ↓ │
596
- * │ 3. DLX routes to wait queue: {queue}-wait │
597
- * │ (with expiration: calculated backoff delay) │
598
- * │ ↓ │
599
- * │ 4. Message waits in queue until TTL expires │
600
- * │ ↓ │
601
- * │ 5. Expired message dead-lettered to DLX │
602
- * │ (with routing key: {queue}) │
603
- * │ ↓ │
604
- * │ 6. DLX routes back to main queue → RETRY │
605
- * │ ↓ │
606
- * │ 7. If retries exhausted: nack without requeue → DLQ │
607
- * │ │
608
- * └─────────────────────────────────────────────────────────────────┘
641
+ * Publish message with an incremented x-retry-count header and optional TTL.
609
642
  */
610
- publishForRetry(msg, consumer, newRetryCount, delayMs, error) {
611
- const queueName = consumer.queue.name;
612
- const deadLetter = consumer.queue.deadLetter;
613
- if (!deadLetter) {
614
- this.logger?.warn("Cannot retry: queue does not have DLX configured, falling back to nack with requeue", { queueName });
615
- this.amqpClient.nack(msg, false, true);
616
- return Future.value(Result.Ok(void 0));
617
- }
618
- const dlxName = deadLetter.exchange.name;
619
- const waitRoutingKey = `${queueName}-wait`;
643
+ publishForRetry({ msg, exchange, routingKey, queueName, waitQueueName, delayMs, error }) {
644
+ const newRetryCount = (msg.properties.headers?.["x-retry-count"] ?? 0) + 1;
620
645
  this.amqpClient.ack(msg);
621
646
  const content = this.parseMessageContentForRetry(msg, queueName);
622
- return this.amqpClient.publish(dlxName, waitRoutingKey, content, {
647
+ return this.amqpClient.publish(exchange, routingKey, content, {
623
648
  ...msg.properties,
624
- expiration: delayMs.toString(),
649
+ ...delayMs !== void 0 ? { expiration: delayMs.toString() } : {},
625
650
  headers: {
626
651
  ...msg.properties.headers,
627
652
  "x-retry-count": newRetryCount,
628
653
  "x-last-error": error.message,
629
- "x-first-failure-timestamp": msg.properties.headers?.["x-first-failure-timestamp"] ?? Date.now()
654
+ "x-first-failure-timestamp": msg.properties.headers?.["x-first-failure-timestamp"] ?? Date.now(),
655
+ ...waitQueueName !== void 0 ? {
656
+ "x-wait-queue": waitQueueName,
657
+ "x-retry-queue": queueName
658
+ } : {}
630
659
  }
631
660
  }).mapOkToResult((published) => {
632
661
  if (!published) {
633
662
  this.logger?.error("Failed to publish message for retry (write buffer full)", {
634
663
  queueName,
635
- waitRoutingKey,
636
- retryCount: newRetryCount
664
+ retryCount: newRetryCount,
665
+ ...delayMs !== void 0 ? { delayMs } : {}
637
666
  });
638
667
  return Result.Error(new TechnicalError("Failed to publish message for retry (write buffer full)"));
639
668
  }
640
669
  this.logger?.info("Message published for retry", {
641
670
  queueName,
642
- waitRoutingKey,
643
671
  retryCount: newRetryCount,
644
- delayMs
672
+ ...delayMs !== void 0 ? { delayMs } : {}
645
673
  });
646
674
  return Result.Ok(void 0);
647
675
  });
@@ -651,8 +679,9 @@ var TypedAmqpWorker = class TypedAmqpWorker {
651
679
  * Nacks the message without requeue, relying on DLX configuration.
652
680
  */
653
681
  sendToDLQ(msg, consumer) {
654
- const queueName = consumer.queue.name;
655
- if (!(consumer.queue.deadLetter !== void 0)) this.logger?.warn("Queue does not have DLX configured - message will be lost on nack", { queueName });
682
+ const queue = extractQueue(consumer.queue);
683
+ const queueName = queue.name;
684
+ if (!(queue.deadLetter !== void 0)) this.logger?.warn("Queue does not have DLX configured - message will be lost on nack", { queueName });
656
685
  this.logger?.info("Sending message to DLQ", {
657
686
  queueName,
658
687
  deliveryTag: msg.fields.deliveryTag
@@ -660,7 +689,6 @@ var TypedAmqpWorker = class TypedAmqpWorker {
660
689
  this.amqpClient.nack(msg, false, false);
661
690
  }
662
691
  };
663
-
664
692
  //#endregion
665
693
  //#region src/handlers.ts
666
694
  /**
@@ -721,7 +749,7 @@ function defineHandlers(contract, handlers) {
721
749
  validateHandlers(contract, handlers);
722
750
  return handlers;
723
751
  }
724
-
725
752
  //#endregion
726
753
  export { MessageValidationError, NonRetryableError, RetryableError, TypedAmqpWorker, defineHandler, defineHandlers, isHandlerError, isNonRetryableError, isRetryableError, nonRetryable, retryable };
754
+
727
755
  //# sourceMappingURL=index.mjs.map