@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/dist/index.d.mts CHANGED
@@ -1,32 +1,47 @@
1
- import { ResultAsync } from "neverthrow";
1
+ import { AsyncResult, Result } from "unthrown";
2
2
  import { AmqpConnectionManager, AmqpConnectionManagerOptions, ConnectionUrl, CreateChannelOpts } from "amqp-connection-manager";
3
3
  import { ContractDefinition } from "@amqp-contract/contract";
4
4
  import { Attributes, Counter, Histogram, Span, Tracer } from "@opentelemetry/api";
5
5
  import { Channel, ConsumeMessage, Options } from "amqplib";
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.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","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.mts","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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"}
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
- import { ResultAsync, err, ok } from "neverthrow";
2
+ import { TaggedError, err, fromPromise, fromSafePromise, fromThrowable, ok } from "unthrown";
3
3
  import amqp from "amqp-connection-manager";
4
4
  import { extractQueue } from "@amqp-contract/contract";
5
5
  //#region \0rolldown/runtime.js
6
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
6
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
7
7
  //#endregion
8
8
  //#region src/connection-manager.ts
9
9
  /**
@@ -163,33 +163,40 @@ function _resetConnectionsForTesting() {
163
163
  *
164
164
  * This includes AMQP connection failures, channel issues, validation failures,
165
165
  * and other runtime errors. This error is shared across core, worker, and client packages.
166
+ *
167
+ * Built on unthrown's {@link TaggedError}, so it carries a `_tag` of
168
+ * `"@amqp-contract/TechnicalError"` for exhaustive dispatch via `matchTags`. The
169
+ * tag is namespaced to avoid colliding with other libraries' tags in a shared
170
+ * `matchTags`; the human-facing `Error.name` is kept bare (`"TechnicalError"`).
171
+ * Remains a real `Error` (and a *modeled* error — it lives in the `E` channel of
172
+ * a `Result`, never the `Defect` channel).
166
173
  */
167
- var TechnicalError = class extends Error {
174
+ var TechnicalError = class extends TaggedError("@amqp-contract/TechnicalError", { name: "TechnicalError" }) {
168
175
  constructor(message, cause) {
169
- super(message);
170
- this.cause = cause;
171
- this.name = "TechnicalError";
172
- const ErrorConstructor = Error;
173
- if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
176
+ super({
177
+ message,
178
+ cause
179
+ });
174
180
  }
175
181
  };
176
182
  /**
177
183
  * Error thrown when message validation fails (payload or headers).
178
184
  *
179
185
  * Used by both the client (publish-time payload validation) and the worker
180
- * (consume-time payload and headers validation).
186
+ * (consume-time payload and headers validation). Carries a `_tag` of
187
+ * `"@amqp-contract/MessageValidationError"` (namespaced to avoid collisions);
188
+ * the `Error.name` is kept bare (`"MessageValidationError"`).
181
189
  *
182
190
  * @param source - The name of the publisher or consumer that triggered the validation
183
191
  * @param issues - The validation issues from the Standard Schema validation
184
192
  */
185
- var MessageValidationError = class extends Error {
193
+ var MessageValidationError = class extends TaggedError("@amqp-contract/MessageValidationError", { name: "MessageValidationError" }) {
186
194
  constructor(source, issues) {
187
- super(`Message validation failed for "${source}"`);
188
- this.source = source;
189
- this.issues = issues;
190
- this.name = "MessageValidationError";
191
- const ErrorConstructor = Error;
192
- if (typeof ErrorConstructor.captureStackTrace === "function") ErrorConstructor.captureStackTrace(this, this.constructor);
195
+ super({
196
+ message: `Message validation failed for "${source}"`,
197
+ source,
198
+ issues
199
+ });
193
200
  }
194
201
  };
195
202
  //#endregion
@@ -217,10 +224,10 @@ var MessageValidationError = class extends Error {
217
224
  async function setupAmqpTopology(channel, contract) {
218
225
  const exchanges = Object.values(contract.exchanges ?? {}).filter((e) => e.name !== "");
219
226
  const exchangeErrors = (await Promise.allSettled(exchanges.map((exchange) => channel.assertExchange(exchange.name, exchange.type, {
220
- durable: exchange.durable,
221
- autoDelete: exchange.autoDelete,
222
- internal: exchange.internal,
223
- arguments: exchange.arguments
227
+ ...exchange.durable !== void 0 && { durable: exchange.durable },
228
+ ...exchange.autoDelete !== void 0 && { autoDelete: exchange.autoDelete },
229
+ ...exchange.internal !== void 0 && { internal: exchange.internal },
230
+ ...exchange.arguments !== void 0 && { arguments: exchange.arguments }
224
231
  })))).map((result, i) => ({
225
232
  result,
226
233
  name: exchanges[i].name
@@ -251,9 +258,9 @@ async function setupAmqpTopology(channel, contract) {
251
258
  });
252
259
  if (queue.maxPriority !== void 0) queueArguments["x-max-priority"] = queue.maxPriority;
253
260
  return channel.assertQueue(queue.name, {
254
- durable: queue.durable,
255
- exclusive: queue.exclusive,
256
- autoDelete: queue.autoDelete,
261
+ ...queue.durable !== void 0 && { durable: queue.durable },
262
+ ...queue.exclusive !== void 0 && { exclusive: queue.exclusive },
263
+ ...queue.autoDelete !== void 0 && { autoDelete: queue.autoDelete },
257
264
  arguments: queueArguments
258
265
  });
259
266
  }))).map((result, i) => ({
@@ -327,7 +334,7 @@ function resolveConnectTimeoutMs(input) {
327
334
  * - Automatic AMQP topology setup (exchanges, queues, bindings) from contract
328
335
  * - Channel creation with JSON serialization enabled by default
329
336
  *
330
- * All operations return `ResultAsync<T, TechnicalError>` for consistent error handling.
337
+ * All operations return `AsyncResult<T, TechnicalError>` for consistent error handling.
331
338
  *
332
339
  * @example
333
340
  * ```typescript
@@ -336,7 +343,7 @@ function resolveConnectTimeoutMs(input) {
336
343
  * connectionOptions: { heartbeatIntervalInSeconds: 30 }
337
344
  * });
338
345
  *
339
- * // Wait for connection (ResultAsync is thenable)
346
+ * // Wait for connection (AsyncResult is thenable)
340
347
  * await client.waitForConnect();
341
348
  *
342
349
  * // Publish a message
@@ -347,6 +354,7 @@ function resolveConnectTimeoutMs(input) {
347
354
  * ```
348
355
  */
349
356
  var AmqpClient = class {
357
+ contract;
350
358
  connection;
351
359
  channelWrapper;
352
360
  urls;
@@ -354,6 +362,15 @@ var AmqpClient = class {
354
362
  /** Resolved timeout in ms; `null` means "wait forever". */
355
363
  connectTimeoutMs;
356
364
  /**
365
+ * Per-consumer prefetch setup functions registered via `addSetup` so they
366
+ * can be removed in {@link cancel} once the consumer is gone — otherwise
367
+ * the channel wrapper would replay the cancelled consumer's QoS on every
368
+ * reconnect and silently apply it to subsequent consumers.
369
+ *
370
+ * @internal
371
+ */
372
+ prefetchSetups = /* @__PURE__ */ new Map();
373
+ /**
357
374
  * Create a new AMQP client instance.
358
375
  *
359
376
  * The client will automatically:
@@ -401,7 +418,7 @@ var AmqpClient = class {
401
418
  * Wait for the channel to be connected and ready.
402
419
  *
403
420
  * If `connectTimeoutMs` was provided in the constructor options, the returned
404
- * ResultAsync resolves to `err(TechnicalError)` once the timeout elapses.
421
+ * AsyncResult resolves to `err(TechnicalError)` once the timeout elapses.
405
422
  * Without a timeout, this waits forever — amqp-connection-manager retries
406
423
  * connections indefinitely and never errors on its own.
407
424
  *
@@ -414,7 +431,7 @@ var AmqpClient = class {
414
431
  waitForConnect() {
415
432
  const connectPromise = this.channelWrapper.waitForConnect();
416
433
  const timeoutMs = this.connectTimeoutMs;
417
- const racedPromise = timeoutMs === null ? connectPromise : new Promise((resolve, reject) => {
434
+ return fromPromise(timeoutMs === null ? connectPromise : new Promise((resolve, reject) => {
418
435
  const handle = setTimeout(() => {
419
436
  reject(/* @__PURE__ */ new Error(`Timed out waiting for AMQP connection after ${timeoutMs}ms`));
420
437
  }, timeoutMs);
@@ -425,38 +442,75 @@ var AmqpClient = class {
425
442
  clearTimeout(handle);
426
443
  reject(error);
427
444
  });
428
- });
429
- return ResultAsync.fromPromise(racedPromise, (error) => new TechnicalError("Failed to connect to AMQP broker", error));
445
+ }), (error) => new TechnicalError("Failed to connect to AMQP broker", error));
430
446
  }
431
447
  /**
432
448
  * Publish a message to an exchange.
433
449
  *
434
- * @returns ResultAsync resolving to `true` if the message was sent, `false` if the channel buffer is full.
450
+ * @returns AsyncResult resolving to `true` if the message was sent, `false` if the channel buffer is full.
435
451
  */
436
452
  publish(exchange, routingKey, content, options) {
437
- return ResultAsync.fromPromise(this.channelWrapper.publish(exchange, routingKey, content, options), (error) => new TechnicalError("Failed to publish message", error));
453
+ return fromPromise(this.channelWrapper.publish(exchange, routingKey, content, options), (error) => new TechnicalError("Failed to publish message", error));
438
454
  }
439
455
  /**
440
456
  * Publish a message directly to a queue.
441
457
  *
442
- * @returns ResultAsync resolving to `true` if the message was sent, `false` if the channel buffer is full.
458
+ * @returns AsyncResult resolving to `true` if the message was sent, `false` if the channel buffer is full.
443
459
  */
444
460
  sendToQueue(queue, content, options) {
445
- return ResultAsync.fromPromise(this.channelWrapper.sendToQueue(queue, content, options), (error) => new TechnicalError("Failed to publish message to queue", error));
461
+ return fromPromise(this.channelWrapper.sendToQueue(queue, content, options), (error) => new TechnicalError("Failed to publish message to queue", error));
446
462
  }
447
463
  /**
448
464
  * Start consuming messages from a queue.
449
465
  *
450
- * @returns ResultAsync resolving to the consumer tag.
466
+ * If `options.prefetch` is set, a per-consumer prefetch count is applied via
467
+ * `channel.prefetch(count, false)` registered as a setup function on the
468
+ * channel wrapper *before* the underlying `consume` call. Registering it via
469
+ * `addSetup` ensures the prefetch is reapplied automatically on channel
470
+ * reconnect; using `global=false` scopes it to subsequent consumers on the
471
+ * channel (RabbitMQ semantics — opposite of intuition: `false` is per-
472
+ * consumer, `true` is channel-wide).
473
+ *
474
+ * `prefetch` is stripped from the options handed to `channelWrapper.consume`
475
+ * because it is not a valid `amqplib` `Options.Consume` field — leaving it
476
+ * in would just travel as a no-op key-value pair on the consume frame.
477
+ *
478
+ * @returns AsyncResult resolving to the consumer tag.
451
479
  */
452
480
  consume(queue, callback, options) {
453
- return ResultAsync.fromPromise(this.channelWrapper.consume(queue, callback, options), (error) => new TechnicalError("Failed to start consuming messages", error)).map((reply) => reply.consumerTag);
481
+ const { prefetch, ...consumeOptions } = options ?? {};
482
+ if (prefetch !== void 0) {
483
+ if (!Number.isInteger(prefetch) || prefetch < 0 || prefetch > 65535) return err(new TechnicalError(`Invalid prefetch: expected a non-negative integer ≤ 65535, got ${String(prefetch)}`)).toAsync();
484
+ }
485
+ const prefetchSetup = typeof prefetch === "number" ? async (channel) => {
486
+ await channel.prefetch(prefetch, false);
487
+ } : void 0;
488
+ return fromPromise((async () => {
489
+ if (prefetchSetup) await this.channelWrapper.addSetup(prefetchSetup);
490
+ let reply;
491
+ try {
492
+ reply = await this.channelWrapper.consume(queue, callback, consumeOptions);
493
+ } catch (error) {
494
+ if (prefetchSetup) await this.channelWrapper.removeSetup(prefetchSetup).catch(() => {});
495
+ throw error;
496
+ }
497
+ if (prefetchSetup) this.prefetchSetups.set(reply.consumerTag, prefetchSetup);
498
+ return reply;
499
+ })(), (error) => new TechnicalError("Failed to start consuming messages", error)).map((reply) => reply.consumerTag);
454
500
  }
455
501
  /**
456
502
  * Cancel a consumer by its consumer tag.
457
503
  */
458
504
  cancel(consumerTag) {
459
- return ResultAsync.fromPromise(this.channelWrapper.cancel(consumerTag), (error) => new TechnicalError("Failed to cancel consumer", error)).map(() => void 0);
505
+ return fromPromise((async () => {
506
+ const setup = this.prefetchSetups.get(consumerTag);
507
+ this.prefetchSetups.delete(consumerTag);
508
+ try {
509
+ await this.channelWrapper.cancel(consumerTag);
510
+ } finally {
511
+ if (setup !== void 0) await this.channelWrapper.removeSetup(setup).catch(() => {});
512
+ }
513
+ })(), (error) => new TechnicalError("Failed to cancel consumer", error)).map(() => void 0);
460
514
  }
461
515
  /**
462
516
  * Acknowledge a message.
@@ -513,14 +567,14 @@ var AmqpClient = class {
513
567
  * errors are wrapped in an AggregateError.
514
568
  */
515
569
  close() {
516
- return new ResultAsync((async () => {
517
- const channelResult = await ResultAsync.fromPromise(this.channelWrapper.close(), (error) => new TechnicalError("Failed to close channel", error));
518
- const releaseResult = await ResultAsync.fromPromise(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions), (error) => new TechnicalError("Failed to release connection", error));
570
+ return fromSafePromise((async () => {
571
+ const channelResult = await fromPromise(this.channelWrapper.close(), (error) => new TechnicalError("Failed to close channel", error));
572
+ const releaseResult = await fromPromise(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions), (error) => new TechnicalError("Failed to release connection", error));
519
573
  if (channelResult.isErr() && releaseResult.isErr()) return err(new TechnicalError("Failed to close channel and release connection", new AggregateError([channelResult.error, releaseResult.error], "Failed to close channel and release connection")));
520
574
  if (channelResult.isErr()) return channelResult;
521
575
  if (releaseResult.isErr()) return releaseResult;
522
576
  return ok(void 0);
523
- })());
577
+ })()).flatMap((result) => result);
524
578
  }
525
579
  /**
526
580
  * Reset connection singleton cache (for testing only)
@@ -531,6 +585,32 @@ var AmqpClient = class {
531
585
  }
532
586
  };
533
587
  //#endregion
588
+ //#region src/parsing.ts
589
+ /**
590
+ * Parse a `Buffer` as JSON, mapping any `JSON.parse` exception to the
591
+ * caller-supplied error type.
592
+ *
593
+ * Use this in consume / reply paths where a parse failure is a typed value,
594
+ * not a thrown exception — the caller decides how to translate the raw error
595
+ * into a domain-level error (e.g. {@link TechnicalError}).
596
+ *
597
+ * @typeParam E - The error type produced by `errorFn`.
598
+ * @param buffer - The raw message body to parse.
599
+ * @param errorFn - Callback invoked with the underlying `JSON.parse` error.
600
+ * @returns A `Result` containing the parsed `unknown` value or the mapped error.
601
+ *
602
+ * @example
603
+ * ```typescript
604
+ * const parsed = safeJsonParse(
605
+ * msg.content,
606
+ * (error) => new TechnicalError("Failed to parse JSON", error),
607
+ * );
608
+ * ```
609
+ */
610
+ function safeJsonParse(buffer, errorFn) {
611
+ return fromThrowable(() => JSON.parse(buffer.toString()), errorFn)();
612
+ }
613
+ //#endregion
534
614
  //#region src/telemetry.ts
535
615
  /**
536
616
  * SpanKind values from OpenTelemetry.
@@ -795,6 +875,6 @@ function _resetTelemetryCacheForTesting() {
795
875
  cachedLateRpcReplyCounter = void 0;
796
876
  }
797
877
  //#endregion
798
- export { AmqpClient, DEFAULT_CONNECT_TIMEOUT_MS, MessageValidationError, MessagingSemanticConventions, TechnicalError, _getConnectionCountForTesting, _resetConnectionsForTesting, _resetTelemetryCacheForTesting, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, recordLateRpcReply, recordPublishMetric, setupAmqpTopology, startConsumeSpan, startPublishSpan };
878
+ export { AmqpClient, DEFAULT_CONNECT_TIMEOUT_MS, MessageValidationError, MessagingSemanticConventions, TechnicalError, _getConnectionCountForTesting, _resetConnectionsForTesting, _resetTelemetryCacheForTesting, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, recordLateRpcReply, recordPublishMetric, safeJsonParse, setupAmqpTopology, startConsumeSpan, startPublishSpan };
799
879
 
800
880
  //# sourceMappingURL=index.mjs.map