@amqp-contract/core 0.24.0 → 1.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Core utilities for AMQP setup and management in amqp-contract.**
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/core.svg?logo=npm)](https://www.npmjs.com/package/@amqp-contract/core)
7
7
  [![npm downloads](https://img.shields.io/npm/dm/@amqp-contract/core.svg)](https://www.npmjs.com/package/@amqp-contract/core)
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue?logo=typescript)](https://www.typescriptlang.org/)
@@ -10,7 +10,7 @@
10
10
 
11
11
  This package provides centralized functionality for establishing AMQP topology (exchanges, queues, and bindings) from contract definitions, and defines the `Logger` interface used across amqp-contract packages.
12
12
 
13
- 📖 **[Full documentation →](https://btravers.github.io/amqp-contract)**
13
+ 📖 **[Full documentation →](https://btravstack.github.io/amqp-contract)**
14
14
 
15
15
  ## Installation
16
16
 
@@ -68,7 +68,7 @@ const amqpClient = new AmqpClient(contract, {
68
68
  await amqpClient.close();
69
69
  ```
70
70
 
71
- For advanced channel configuration options (custom setup, prefetch, publisher confirms), see the [Channel Configuration Guide](https://btravers.github.io/amqp-contract/guide/channel-configuration).
71
+ For advanced channel configuration options (custom setup, prefetch, publisher confirms), see the [Channel Configuration Guide](https://btravstack.github.io/amqp-contract/guide/channel-configuration).
72
72
 
73
73
  ### Logger Interface
74
74
 
@@ -93,16 +93,16 @@ const client = (
93
93
  urls: ["amqp://localhost"],
94
94
  logger, // Optional: logs published messages
95
95
  })
96
- )._unsafeUnwrap();
96
+ ).unwrap();
97
97
  ```
98
98
 
99
99
  ## API
100
100
 
101
- For complete API documentation, see the [@amqp-contract/core API Reference](https://btravers.github.io/amqp-contract/api/core).
101
+ For complete API documentation, see the [@amqp-contract/core API Reference](https://btravstack.github.io/amqp-contract/api/core).
102
102
 
103
103
  ## Documentation
104
104
 
105
- 📖 **[Read the full documentation →](https://btravers.github.io/amqp-contract)**
105
+ 📖 **[Read the full documentation →](https://btravstack.github.io/amqp-contract)**
106
106
 
107
107
  ## License
108
108
 
package/dist/index.cjs CHANGED
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- let neverthrow = require("neverthrow");
24
+ let unthrown = require("unthrown");
25
25
  let amqp_connection_manager = require("amqp-connection-manager");
26
26
  amqp_connection_manager = __toESM(amqp_connection_manager, 1);
27
27
  let _amqp_contract_contract = require("@amqp-contract/contract");
@@ -184,33 +184,40 @@ function _resetConnectionsForTesting() {
184
184
  *
185
185
  * This includes AMQP connection failures, channel issues, validation failures,
186
186
  * and other runtime errors. This error is shared across core, worker, and client packages.
187
+ *
188
+ * Built on unthrown's {@link TaggedError}, so it carries a `_tag` of
189
+ * `"@amqp-contract/TechnicalError"` for exhaustive dispatch via `matchTags`. The
190
+ * tag is namespaced to avoid colliding with other libraries' tags in a shared
191
+ * `matchTags`; the human-facing `Error.name` is kept bare (`"TechnicalError"`).
192
+ * Remains a real `Error` (and a *modeled* error — it lives in the `E` channel of
193
+ * a `Result`, never the `Defect` channel).
187
194
  */
188
- var TechnicalError = class extends Error {
195
+ var TechnicalError = class extends (0, unthrown.TaggedError)("@amqp-contract/TechnicalError", { name: "TechnicalError" }) {
189
196
  constructor(message, cause) {
190
- super(message);
191
- this.cause = cause;
192
- this.name = "TechnicalError";
193
- const ErrorConstructor = Error;
194
- if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
197
+ super({
198
+ message,
199
+ cause
200
+ });
195
201
  }
196
202
  };
197
203
  /**
198
204
  * Error thrown when message validation fails (payload or headers).
199
205
  *
200
206
  * Used by both the client (publish-time payload validation) and the worker
201
- * (consume-time payload and headers validation).
207
+ * (consume-time payload and headers validation). Carries a `_tag` of
208
+ * `"@amqp-contract/MessageValidationError"` (namespaced to avoid collisions);
209
+ * the `Error.name` is kept bare (`"MessageValidationError"`).
202
210
  *
203
211
  * @param source - The name of the publisher or consumer that triggered the validation
204
212
  * @param issues - The validation issues from the Standard Schema validation
205
213
  */
206
- var MessageValidationError = class extends Error {
214
+ var MessageValidationError = class extends (0, unthrown.TaggedError)("@amqp-contract/MessageValidationError", { name: "MessageValidationError" }) {
207
215
  constructor(source, issues) {
208
- super(`Message validation failed for "${source}"`);
209
- this.source = source;
210
- this.issues = issues;
211
- this.name = "MessageValidationError";
212
- const ErrorConstructor = Error;
213
- if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
216
+ super({
217
+ message: `Message validation failed for "${source}"`,
218
+ source,
219
+ issues
220
+ });
214
221
  }
215
222
  };
216
223
  //#endregion
@@ -238,10 +245,10 @@ var MessageValidationError = class extends Error {
238
245
  async function setupAmqpTopology(channel, contract) {
239
246
  const exchanges = Object.values(contract.exchanges ?? {}).filter((e) => e.name !== "");
240
247
  const exchangeErrors = (await Promise.allSettled(exchanges.map((exchange) => channel.assertExchange(exchange.name, exchange.type, {
241
- durable: exchange.durable,
242
- autoDelete: exchange.autoDelete,
243
- internal: exchange.internal,
244
- arguments: exchange.arguments
248
+ ...exchange.durable !== void 0 && { durable: exchange.durable },
249
+ ...exchange.autoDelete !== void 0 && { autoDelete: exchange.autoDelete },
250
+ ...exchange.internal !== void 0 && { internal: exchange.internal },
251
+ ...exchange.arguments !== void 0 && { arguments: exchange.arguments }
245
252
  })))).map((result, i) => ({
246
253
  result,
247
254
  name: exchanges[i].name
@@ -272,9 +279,9 @@ async function setupAmqpTopology(channel, contract) {
272
279
  });
273
280
  if (queue.maxPriority !== void 0) queueArguments["x-max-priority"] = queue.maxPriority;
274
281
  return channel.assertQueue(queue.name, {
275
- durable: queue.durable,
276
- exclusive: queue.exclusive,
277
- autoDelete: queue.autoDelete,
282
+ ...queue.durable !== void 0 && { durable: queue.durable },
283
+ ...queue.exclusive !== void 0 && { exclusive: queue.exclusive },
284
+ ...queue.autoDelete !== void 0 && { autoDelete: queue.autoDelete },
278
285
  arguments: queueArguments
279
286
  });
280
287
  }))).map((result, i) => ({
@@ -348,7 +355,7 @@ function resolveConnectTimeoutMs(input) {
348
355
  * - Automatic AMQP topology setup (exchanges, queues, bindings) from contract
349
356
  * - Channel creation with JSON serialization enabled by default
350
357
  *
351
- * All operations return `ResultAsync<T, TechnicalError>` for consistent error handling.
358
+ * All operations return `AsyncResult<T, TechnicalError>` for consistent error handling.
352
359
  *
353
360
  * @example
354
361
  * ```typescript
@@ -357,7 +364,7 @@ function resolveConnectTimeoutMs(input) {
357
364
  * connectionOptions: { heartbeatIntervalInSeconds: 30 }
358
365
  * });
359
366
  *
360
- * // Wait for connection (ResultAsync is thenable)
367
+ * // Wait for connection (AsyncResult is thenable)
361
368
  * await client.waitForConnect();
362
369
  *
363
370
  * // Publish a message
@@ -368,6 +375,7 @@ function resolveConnectTimeoutMs(input) {
368
375
  * ```
369
376
  */
370
377
  var AmqpClient = class {
378
+ contract;
371
379
  connection;
372
380
  channelWrapper;
373
381
  urls;
@@ -375,6 +383,15 @@ var AmqpClient = class {
375
383
  /** Resolved timeout in ms; `null` means "wait forever". */
376
384
  connectTimeoutMs;
377
385
  /**
386
+ * Per-consumer prefetch setup functions registered via `addSetup` so they
387
+ * can be removed in {@link cancel} once the consumer is gone — otherwise
388
+ * the channel wrapper would replay the cancelled consumer's QoS on every
389
+ * reconnect and silently apply it to subsequent consumers.
390
+ *
391
+ * @internal
392
+ */
393
+ prefetchSetups = /* @__PURE__ */ new Map();
394
+ /**
378
395
  * Create a new AMQP client instance.
379
396
  *
380
397
  * The client will automatically:
@@ -422,7 +439,7 @@ var AmqpClient = class {
422
439
  * Wait for the channel to be connected and ready.
423
440
  *
424
441
  * If `connectTimeoutMs` was provided in the constructor options, the returned
425
- * ResultAsync resolves to `err(TechnicalError)` once the timeout elapses.
442
+ * AsyncResult resolves to `err(TechnicalError)` once the timeout elapses.
426
443
  * Without a timeout, this waits forever — amqp-connection-manager retries
427
444
  * connections indefinitely and never errors on its own.
428
445
  *
@@ -435,7 +452,7 @@ var AmqpClient = class {
435
452
  waitForConnect() {
436
453
  const connectPromise = this.channelWrapper.waitForConnect();
437
454
  const timeoutMs = this.connectTimeoutMs;
438
- const racedPromise = timeoutMs === null ? connectPromise : new Promise((resolve, reject) => {
455
+ return (0, unthrown.fromPromise)(timeoutMs === null ? connectPromise : new Promise((resolve, reject) => {
439
456
  const handle = setTimeout(() => {
440
457
  reject(/* @__PURE__ */ new Error(`Timed out waiting for AMQP connection after ${timeoutMs}ms`));
441
458
  }, timeoutMs);
@@ -446,38 +463,75 @@ var AmqpClient = class {
446
463
  clearTimeout(handle);
447
464
  reject(error);
448
465
  });
449
- });
450
- return neverthrow.ResultAsync.fromPromise(racedPromise, (error) => new TechnicalError("Failed to connect to AMQP broker", error));
466
+ }), (error) => new TechnicalError("Failed to connect to AMQP broker", error));
451
467
  }
452
468
  /**
453
469
  * Publish a message to an exchange.
454
470
  *
455
- * @returns ResultAsync resolving to `true` if the message was sent, `false` if the channel buffer is full.
471
+ * @returns AsyncResult resolving to `true` if the message was sent, `false` if the channel buffer is full.
456
472
  */
457
473
  publish(exchange, routingKey, content, options) {
458
- return neverthrow.ResultAsync.fromPromise(this.channelWrapper.publish(exchange, routingKey, content, options), (error) => new TechnicalError("Failed to publish message", error));
474
+ return (0, unthrown.fromPromise)(this.channelWrapper.publish(exchange, routingKey, content, options), (error) => new TechnicalError("Failed to publish message", error));
459
475
  }
460
476
  /**
461
477
  * Publish a message directly to a queue.
462
478
  *
463
- * @returns ResultAsync resolving to `true` if the message was sent, `false` if the channel buffer is full.
479
+ * @returns AsyncResult resolving to `true` if the message was sent, `false` if the channel buffer is full.
464
480
  */
465
481
  sendToQueue(queue, content, options) {
466
- return neverthrow.ResultAsync.fromPromise(this.channelWrapper.sendToQueue(queue, content, options), (error) => new TechnicalError("Failed to publish message to queue", error));
482
+ return (0, unthrown.fromPromise)(this.channelWrapper.sendToQueue(queue, content, options), (error) => new TechnicalError("Failed to publish message to queue", error));
467
483
  }
468
484
  /**
469
485
  * Start consuming messages from a queue.
470
486
  *
471
- * @returns ResultAsync resolving to the consumer tag.
487
+ * If `options.prefetch` is set, a per-consumer prefetch count is applied via
488
+ * `channel.prefetch(count, false)` registered as a setup function on the
489
+ * channel wrapper *before* the underlying `consume` call. Registering it via
490
+ * `addSetup` ensures the prefetch is reapplied automatically on channel
491
+ * reconnect; using `global=false` scopes it to subsequent consumers on the
492
+ * channel (RabbitMQ semantics — opposite of intuition: `false` is per-
493
+ * consumer, `true` is channel-wide).
494
+ *
495
+ * `prefetch` is stripped from the options handed to `channelWrapper.consume`
496
+ * because it is not a valid `amqplib` `Options.Consume` field — leaving it
497
+ * in would just travel as a no-op key-value pair on the consume frame.
498
+ *
499
+ * @returns AsyncResult resolving to the consumer tag.
472
500
  */
473
501
  consume(queue, callback, options) {
474
- return neverthrow.ResultAsync.fromPromise(this.channelWrapper.consume(queue, callback, options), (error) => new TechnicalError("Failed to start consuming messages", error)).map((reply) => reply.consumerTag);
502
+ const { prefetch, ...consumeOptions } = options ?? {};
503
+ if (prefetch !== void 0) {
504
+ if (!Number.isInteger(prefetch) || prefetch < 0 || prefetch > 65535) return (0, unthrown.err)(new TechnicalError(`Invalid prefetch: expected a non-negative integer ≤ 65535, got ${String(prefetch)}`)).toAsync();
505
+ }
506
+ const prefetchSetup = typeof prefetch === "number" ? async (channel) => {
507
+ await channel.prefetch(prefetch, false);
508
+ } : void 0;
509
+ return (0, unthrown.fromPromise)((async () => {
510
+ if (prefetchSetup) await this.channelWrapper.addSetup(prefetchSetup);
511
+ let reply;
512
+ try {
513
+ reply = await this.channelWrapper.consume(queue, callback, consumeOptions);
514
+ } catch (error) {
515
+ if (prefetchSetup) await this.channelWrapper.removeSetup(prefetchSetup).catch(() => {});
516
+ throw error;
517
+ }
518
+ if (prefetchSetup) this.prefetchSetups.set(reply.consumerTag, prefetchSetup);
519
+ return reply;
520
+ })(), (error) => new TechnicalError("Failed to start consuming messages", error)).map((reply) => reply.consumerTag);
475
521
  }
476
522
  /**
477
523
  * Cancel a consumer by its consumer tag.
478
524
  */
479
525
  cancel(consumerTag) {
480
- return neverthrow.ResultAsync.fromPromise(this.channelWrapper.cancel(consumerTag), (error) => new TechnicalError("Failed to cancel consumer", error)).map(() => void 0);
526
+ return (0, unthrown.fromPromise)((async () => {
527
+ const setup = this.prefetchSetups.get(consumerTag);
528
+ this.prefetchSetups.delete(consumerTag);
529
+ try {
530
+ await this.channelWrapper.cancel(consumerTag);
531
+ } finally {
532
+ if (setup !== void 0) await this.channelWrapper.removeSetup(setup).catch(() => {});
533
+ }
534
+ })(), (error) => new TechnicalError("Failed to cancel consumer", error)).map(() => void 0);
481
535
  }
482
536
  /**
483
537
  * Acknowledge a message.
@@ -534,14 +588,14 @@ var AmqpClient = class {
534
588
  * errors are wrapped in an AggregateError.
535
589
  */
536
590
  close() {
537
- return new neverthrow.ResultAsync((async () => {
538
- const channelResult = await neverthrow.ResultAsync.fromPromise(this.channelWrapper.close(), (error) => new TechnicalError("Failed to close channel", error));
539
- const releaseResult = await neverthrow.ResultAsync.fromPromise(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions), (error) => new TechnicalError("Failed to release connection", error));
540
- if (channelResult.isErr() && releaseResult.isErr()) return (0, neverthrow.err)(new TechnicalError("Failed to close channel and release connection", new AggregateError([channelResult.error, releaseResult.error], "Failed to close channel and release connection")));
591
+ return (0, unthrown.fromSafePromise)((async () => {
592
+ const channelResult = await (0, unthrown.fromPromise)(this.channelWrapper.close(), (error) => new TechnicalError("Failed to close channel", error));
593
+ const releaseResult = await (0, unthrown.fromPromise)(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions), (error) => new TechnicalError("Failed to release connection", error));
594
+ if (channelResult.isErr() && releaseResult.isErr()) return (0, unthrown.err)(new TechnicalError("Failed to close channel and release connection", new AggregateError([channelResult.error, releaseResult.error], "Failed to close channel and release connection")));
541
595
  if (channelResult.isErr()) return channelResult;
542
596
  if (releaseResult.isErr()) return releaseResult;
543
- return (0, neverthrow.ok)(void 0);
544
- })());
597
+ return (0, unthrown.ok)(void 0);
598
+ })()).flatMap((result) => result);
545
599
  }
546
600
  /**
547
601
  * Reset connection singleton cache (for testing only)
@@ -552,6 +606,32 @@ var AmqpClient = class {
552
606
  }
553
607
  };
554
608
  //#endregion
609
+ //#region src/parsing.ts
610
+ /**
611
+ * Parse a `Buffer` as JSON, mapping any `JSON.parse` exception to the
612
+ * caller-supplied error type.
613
+ *
614
+ * Use this in consume / reply paths where a parse failure is a typed value,
615
+ * not a thrown exception — the caller decides how to translate the raw error
616
+ * into a domain-level error (e.g. {@link TechnicalError}).
617
+ *
618
+ * @typeParam E - The error type produced by `errorFn`.
619
+ * @param buffer - The raw message body to parse.
620
+ * @param errorFn - Callback invoked with the underlying `JSON.parse` error.
621
+ * @returns A `Result` containing the parsed `unknown` value or the mapped error.
622
+ *
623
+ * @example
624
+ * ```typescript
625
+ * const parsed = safeJsonParse(
626
+ * msg.content,
627
+ * (error) => new TechnicalError("Failed to parse JSON", error),
628
+ * );
629
+ * ```
630
+ */
631
+ function safeJsonParse(buffer, errorFn) {
632
+ return (0, unthrown.fromThrowable)(() => JSON.parse(buffer.toString()), errorFn)();
633
+ }
634
+ //#endregion
555
635
  //#region src/telemetry.ts
556
636
  /**
557
637
  * SpanKind values from OpenTelemetry.
@@ -830,6 +910,7 @@ exports.endSpanSuccess = endSpanSuccess;
830
910
  exports.recordConsumeMetric = recordConsumeMetric;
831
911
  exports.recordLateRpcReply = recordLateRpcReply;
832
912
  exports.recordPublishMetric = recordPublishMetric;
913
+ exports.safeJsonParse = safeJsonParse;
833
914
  exports.setupAmqpTopology = setupAmqpTopology;
834
915
  exports.startConsumeSpan = startConsumeSpan;
835
916
  exports.startPublishSpan = startPublishSpan;
package/dist/index.d.cts CHANGED
@@ -1,32 +1,47 @@
1
1
  import { ContractDefinition } from "@amqp-contract/contract";
2
2
  import { AmqpConnectionManager, AmqpConnectionManagerOptions, ConnectionUrl, CreateChannelOpts } from "amqp-connection-manager";
3
3
  import { Channel, ConsumeMessage, Options } from "amqplib";
4
- import { ResultAsync } from "neverthrow";
4
+ import { AsyncResult, Result } from "unthrown";
5
5
  import { Attributes, Counter, Histogram, Span, Tracer } from "@opentelemetry/api";
6
6
 
7
7
  //#region src/errors.d.ts
8
+ declare const TechnicalError_base: import("unthrown").TaggedErrorConstructor<"@amqp-contract/TechnicalError">;
8
9
  /**
9
10
  * Error for technical/runtime failures that cannot be prevented by TypeScript.
10
11
  *
11
12
  * This includes AMQP connection failures, channel issues, validation failures,
12
13
  * and other runtime errors. This error is shared across core, worker, and client packages.
14
+ *
15
+ * Built on unthrown's {@link TaggedError}, so it carries a `_tag` of
16
+ * `"@amqp-contract/TechnicalError"` for exhaustive dispatch via `matchTags`. The
17
+ * tag is namespaced to avoid colliding with other libraries' tags in a shared
18
+ * `matchTags`; the human-facing `Error.name` is kept bare (`"TechnicalError"`).
19
+ * Remains a real `Error` (and a *modeled* error — it lives in the `E` channel of
20
+ * a `Result`, never the `Defect` channel).
13
21
  */
14
- declare class TechnicalError extends Error {
15
- readonly cause?: unknown | undefined;
16
- constructor(message: string, cause?: unknown | undefined);
22
+ declare class TechnicalError extends TechnicalError_base<{
23
+ message: string;
24
+ cause?: unknown;
25
+ }> {
26
+ constructor(message: string, cause?: unknown);
17
27
  }
28
+ declare const MessageValidationError_base: import("unthrown").TaggedErrorConstructor<"@amqp-contract/MessageValidationError">;
18
29
  /**
19
30
  * Error thrown when message validation fails (payload or headers).
20
31
  *
21
32
  * Used by both the client (publish-time payload validation) and the worker
22
- * (consume-time payload and headers validation).
33
+ * (consume-time payload and headers validation). Carries a `_tag` of
34
+ * `"@amqp-contract/MessageValidationError"` (namespaced to avoid collisions);
35
+ * the `Error.name` is kept bare (`"MessageValidationError"`).
23
36
  *
24
37
  * @param source - The name of the publisher or consumer that triggered the validation
25
38
  * @param issues - The validation issues from the Standard Schema validation
26
39
  */
27
- declare class MessageValidationError extends Error {
28
- readonly source: string;
29
- readonly issues: unknown;
40
+ declare class MessageValidationError extends MessageValidationError_base<{
41
+ message: string;
42
+ source: string;
43
+ issues: unknown;
44
+ }> {
30
45
  constructor(source: string, issues: unknown);
31
46
  }
32
47
  //#endregion
@@ -63,16 +78,29 @@ type AmqpClientOptions = {
63
78
  */
64
79
  type ConsumeCallback = (msg: ConsumeMessage | null) => void | Promise<void>;
65
80
  /**
66
- * Publish options that extend amqplib's Options.Publish with optional timeout support.
81
+ * Publish options for `AmqpClient.publish` / `AmqpClient.sendToQueue`.
82
+ *
83
+ * Currently a re-export of amqplib's `Options.Publish`. A previous version of
84
+ * this type also exposed a `timeout` field, but that field never had a
85
+ * meaningful AMQP-level effect in this codebase and has been removed to avoid
86
+ * suggesting behaviour we do not provide. (`amqp-connection-manager`'s own
87
+ * `publishTimeout` channel option is unrelated and is configured at channel
88
+ * creation, not per-publish.)
67
89
  */
68
- type PublishOptions = Options.Publish & {
69
- /** Message will be rejected after timeout ms */timeout?: number;
70
- };
90
+ type PublishOptions = Options.Publish;
71
91
  /**
72
- * Consume options that extend amqplib's Options.Consume with optional prefetch support.
92
+ * Consume options that extend amqplib's `Options.Consume` with an optional
93
+ * per-consumer prefetch count.
94
+ *
95
+ * `prefetch` is intercepted by {@link AmqpClient.consume}: it is stripped from
96
+ * the options handed to the underlying `channelWrapper.consume(...)` call
97
+ * (since amqplib's `Options.Consume` does not include it) and applied via
98
+ * `channel.prefetch(count, false)` registered through `addSetup` *before* the
99
+ * consume so the value is in effect when the consumer starts and is reapplied
100
+ * automatically on channel reconnect.
73
101
  */
74
102
  type ConsumerOptions = Options.Consume & {
75
- /** Number of messages to prefetch */prefetch?: number;
103
+ /** Per-consumer prefetch count. Applied before `channel.consume(...)`. */prefetch?: number;
76
104
  };
77
105
  /**
78
106
  * AMQP client that manages connections and channels with automatic topology setup.
@@ -83,7 +111,7 @@ type ConsumerOptions = Options.Consume & {
83
111
  * - Automatic AMQP topology setup (exchanges, queues, bindings) from contract
84
112
  * - Channel creation with JSON serialization enabled by default
85
113
  *
86
- * All operations return `ResultAsync<T, TechnicalError>` for consistent error handling.
114
+ * All operations return `AsyncResult<T, TechnicalError>` for consistent error handling.
87
115
  *
88
116
  * @example
89
117
  * ```typescript
@@ -92,7 +120,7 @@ type ConsumerOptions = Options.Consume & {
92
120
  * connectionOptions: { heartbeatIntervalInSeconds: 30 }
93
121
  * });
94
122
  *
95
- * // Wait for connection (ResultAsync is thenable)
123
+ * // Wait for connection (AsyncResult is thenable)
96
124
  * await client.waitForConnect();
97
125
  *
98
126
  * // Publish a message
@@ -110,6 +138,15 @@ declare class AmqpClient {
110
138
  private readonly connectionOptions?;
111
139
  /** Resolved timeout in ms; `null` means "wait forever". */
112
140
  private readonly connectTimeoutMs;
141
+ /**
142
+ * Per-consumer prefetch setup functions registered via `addSetup` so they
143
+ * can be removed in {@link cancel} once the consumer is gone — otherwise
144
+ * the channel wrapper would replay the cancelled consumer's QoS on every
145
+ * reconnect and silently apply it to subsequent consumers.
146
+ *
147
+ * @internal
148
+ */
149
+ private readonly prefetchSetups;
113
150
  /**
114
151
  * Create a new AMQP client instance.
115
152
  *
@@ -136,7 +173,7 @@ declare class AmqpClient {
136
173
  * Wait for the channel to be connected and ready.
137
174
  *
138
175
  * If `connectTimeoutMs` was provided in the constructor options, the returned
139
- * ResultAsync resolves to `err(TechnicalError)` once the timeout elapses.
176
+ * AsyncResult resolves to `err(TechnicalError)` once the timeout elapses.
140
177
  * Without a timeout, this waits forever — amqp-connection-manager retries
141
178
  * connections indefinitely and never errors on its own.
142
179
  *
@@ -146,29 +183,41 @@ declare class AmqpClient {
146
183
  * path to release the connection — `waitForConnect` does not do this
147
184
  * automatically. The typed factories handle this cleanup for you.
148
185
  */
149
- waitForConnect(): ResultAsync<void, TechnicalError>;
186
+ waitForConnect(): AsyncResult<void, TechnicalError>;
150
187
  /**
151
188
  * Publish a message to an exchange.
152
189
  *
153
- * @returns ResultAsync resolving to `true` if the message was sent, `false` if the channel buffer is full.
190
+ * @returns AsyncResult resolving to `true` if the message was sent, `false` if the channel buffer is full.
154
191
  */
155
- publish(exchange: string, routingKey: string, content: Buffer | unknown, options?: PublishOptions): ResultAsync<boolean, TechnicalError>;
192
+ publish(exchange: string, routingKey: string, content: Buffer | unknown, options?: PublishOptions): AsyncResult<boolean, TechnicalError>;
156
193
  /**
157
194
  * Publish a message directly to a queue.
158
195
  *
159
- * @returns ResultAsync resolving to `true` if the message was sent, `false` if the channel buffer is full.
196
+ * @returns AsyncResult resolving to `true` if the message was sent, `false` if the channel buffer is full.
160
197
  */
161
- sendToQueue(queue: string, content: Buffer | unknown, options?: PublishOptions): ResultAsync<boolean, TechnicalError>;
198
+ sendToQueue(queue: string, content: Buffer | unknown, options?: PublishOptions): AsyncResult<boolean, TechnicalError>;
162
199
  /**
163
200
  * Start consuming messages from a queue.
164
201
  *
165
- * @returns ResultAsync resolving to the consumer tag.
202
+ * If `options.prefetch` is set, a per-consumer prefetch count is applied via
203
+ * `channel.prefetch(count, false)` registered as a setup function on the
204
+ * channel wrapper *before* the underlying `consume` call. Registering it via
205
+ * `addSetup` ensures the prefetch is reapplied automatically on channel
206
+ * reconnect; using `global=false` scopes it to subsequent consumers on the
207
+ * channel (RabbitMQ semantics — opposite of intuition: `false` is per-
208
+ * consumer, `true` is channel-wide).
209
+ *
210
+ * `prefetch` is stripped from the options handed to `channelWrapper.consume`
211
+ * because it is not a valid `amqplib` `Options.Consume` field — leaving it
212
+ * in would just travel as a no-op key-value pair on the consume frame.
213
+ *
214
+ * @returns AsyncResult resolving to the consumer tag.
166
215
  */
167
- consume(queue: string, callback: ConsumeCallback, options?: ConsumerOptions): ResultAsync<string, TechnicalError>;
216
+ consume(queue: string, callback: ConsumeCallback, options?: ConsumerOptions): AsyncResult<string, TechnicalError>;
168
217
  /**
169
218
  * Cancel a consumer by its consumer tag.
170
219
  */
171
- cancel(consumerTag: string): ResultAsync<void, TechnicalError>;
220
+ cancel(consumerTag: string): AsyncResult<void, TechnicalError>;
172
221
  /**
173
222
  * Acknowledge a message.
174
223
  *
@@ -215,7 +264,7 @@ declare class AmqpClient {
215
264
  * Both steps run regardless of each other's outcome; if both fail, the
216
265
  * errors are wrapped in an AggregateError.
217
266
  */
218
- close(): ResultAsync<void, TechnicalError>;
267
+ close(): AsyncResult<void, TechnicalError>;
219
268
  /**
220
269
  * Reset connection singleton cache (for testing only)
221
270
  * @internal
@@ -296,6 +345,30 @@ type Logger = {
296
345
  error(message: string, context?: LoggerContext): void;
297
346
  };
298
347
  //#endregion
348
+ //#region src/parsing.d.ts
349
+ /**
350
+ * Parse a `Buffer` as JSON, mapping any `JSON.parse` exception to the
351
+ * caller-supplied error type.
352
+ *
353
+ * Use this in consume / reply paths where a parse failure is a typed value,
354
+ * not a thrown exception — the caller decides how to translate the raw error
355
+ * into a domain-level error (e.g. {@link TechnicalError}).
356
+ *
357
+ * @typeParam E - The error type produced by `errorFn`.
358
+ * @param buffer - The raw message body to parse.
359
+ * @param errorFn - Callback invoked with the underlying `JSON.parse` error.
360
+ * @returns A `Result` containing the parsed `unknown` value or the mapped error.
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * const parsed = safeJsonParse(
365
+ * msg.content,
366
+ * (error) => new TechnicalError("Failed to parse JSON", error),
367
+ * );
368
+ * ```
369
+ */
370
+ declare function safeJsonParse<E>(buffer: Buffer, errorFn: (raw: unknown) => E): Result<unknown, E>;
371
+ //#endregion
299
372
  //#region src/setup.d.ts
300
373
  /**
301
374
  * Setup AMQP topology (exchanges, queues, and bindings) from a contract definition.
@@ -423,5 +496,5 @@ declare function recordLateRpcReply(provider: TelemetryProvider, reason: "unknow
423
496
  */
424
497
  declare function _resetTelemetryCacheForTesting(): void;
425
498
  //#endregion
426
- export { AmqpClient, type AmqpClientOptions, type ConsumeCallback, type ConsumerOptions, DEFAULT_CONNECT_TIMEOUT_MS, type Logger, type LoggerContext, MessageValidationError, MessagingSemanticConventions, type PublishOptions, TechnicalError, type TelemetryProvider, _getConnectionCountForTesting, _resetConnectionsForTesting, _resetTelemetryCacheForTesting, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, recordLateRpcReply, recordPublishMetric, setupAmqpTopology, startConsumeSpan, startPublishSpan };
499
+ export { AmqpClient, type AmqpClientOptions, type ConsumeCallback, type ConsumerOptions, DEFAULT_CONNECT_TIMEOUT_MS, type Logger, type LoggerContext, MessageValidationError, MessagingSemanticConventions, type PublishOptions, TechnicalError, type TelemetryProvider, _getConnectionCountForTesting, _resetConnectionsForTesting, _resetTelemetryCacheForTesting, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, recordLateRpcReply, recordPublishMetric, safeJsonParse, setupAmqpTopology, startConsumeSpan, startPublishSpan };
427
500
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts","../src/amqp-client.ts","../src/connection-manager.ts","../src/logger.ts","../src/setup.ts","../src/telemetry.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/errors.ts","../src/amqp-client.ts","../src/connection-manager.ts","../src/logger.ts","../src/parsing.ts","../src/setup.ts","../src/telemetry.ts"],"mappings":"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"}