@amqp-contract/core 0.21.0 → 0.23.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.cjs CHANGED
@@ -25,6 +25,7 @@ let _swan_io_boxed = require("@swan-io/boxed");
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");
28
+ let node_module = require("node:module");
28
29
  //#region src/connection-manager.ts
29
30
  /**
30
31
  * Connection manager singleton for sharing AMQP connections across clients.
@@ -139,6 +140,14 @@ var ConnectionManagerSingleton = class ConnectionManagerSingleton {
139
140
  return value;
140
141
  }
141
142
  /**
143
+ * Get the number of active pooled connections.
144
+ *
145
+ * @internal
146
+ */
147
+ _getConnectionCountForTesting() {
148
+ return this.connections.size;
149
+ }
150
+ /**
142
151
  * Reset all cached connections (for testing purposes)
143
152
  * @internal
144
153
  */
@@ -149,6 +158,25 @@ var ConnectionManagerSingleton = class ConnectionManagerSingleton {
149
158
  this.refCounts.clear();
150
159
  }
151
160
  };
161
+ /**
162
+ * Number of active pooled connections. Test-only helper — exposed in lieu of
163
+ * the underlying singleton, which is intentionally not part of the public API
164
+ * (mutating it from outside the library can break in-flight clients sharing a
165
+ * connection).
166
+ *
167
+ * @internal
168
+ */
169
+ function _getConnectionCountForTesting() {
170
+ return ConnectionManagerSingleton.getInstance()._getConnectionCountForTesting();
171
+ }
172
+ /**
173
+ * Close every pooled connection and clear ref-counts. Test-only helper.
174
+ *
175
+ * @internal
176
+ */
177
+ function _resetConnectionsForTesting() {
178
+ return ConnectionManagerSingleton.getInstance()._resetForTesting();
179
+ }
152
180
  //#endregion
153
181
  //#region src/errors.ts
154
182
  /**
@@ -208,13 +236,20 @@ var MessageValidationError = class extends Error {
208
236
  * ```
209
237
  */
210
238
  async function setupAmqpTopology(channel, contract) {
211
- const exchangeErrors = (await Promise.allSettled(Object.values(contract.exchanges ?? {}).map((exchange) => channel.assertExchange(exchange.name, exchange.type, {
239
+ const exchanges = Object.values(contract.exchanges ?? {}).filter((e) => e.name !== "");
240
+ const exchangeErrors = (await Promise.allSettled(exchanges.map((exchange) => channel.assertExchange(exchange.name, exchange.type, {
212
241
  durable: exchange.durable,
213
242
  autoDelete: exchange.autoDelete,
214
243
  internal: exchange.internal,
215
244
  arguments: exchange.arguments
216
- })))).filter((result) => result.status === "rejected");
217
- if (exchangeErrors.length > 0) throw new AggregateError(exchangeErrors.map(({ reason }) => reason), "Failed to setup exchanges");
245
+ })))).map((result, i) => ({
246
+ result,
247
+ name: exchanges[i].name
248
+ })).filter((entry) => entry.result.status === "rejected");
249
+ if (exchangeErrors.length > 0) {
250
+ const names = exchangeErrors.map((e) => e.name).join(", ");
251
+ throw new AggregateError(exchangeErrors.map(({ result }) => result.reason), `Failed to setup exchanges: ${names}`);
252
+ }
218
253
  for (const queueEntry of Object.values(contract.queues ?? {})) {
219
254
  const queue = (0, _amqp_contract_contract.extractQueue)(queueEntry);
220
255
  if (queue.deadLetter) {
@@ -222,7 +257,8 @@ async function setupAmqpTopology(channel, contract) {
222
257
  if (!Object.values(contract.exchanges ?? {}).some((exchange) => exchange.name === dlxName)) throw new TechnicalError(`Queue "${queue.name}" references dead letter exchange "${dlxName}" which is not declared in the contract. Add the exchange to contract.exchanges to ensure it is created before the queue.`);
223
258
  }
224
259
  }
225
- const queueErrors = (await Promise.allSettled(Object.values(contract.queues ?? {}).map((queueEntry) => {
260
+ const queueEntries = Object.values(contract.queues ?? {});
261
+ const queueErrors = (await Promise.allSettled(queueEntries.map((queueEntry) => {
226
262
  const queue = (0, _amqp_contract_contract.extractQueue)(queueEntry);
227
263
  const queueArguments = { ...queue.arguments };
228
264
  queueArguments["x-queue-type"] = queue.type;
@@ -241,13 +277,29 @@ async function setupAmqpTopology(channel, contract) {
241
277
  autoDelete: queue.autoDelete,
242
278
  arguments: queueArguments
243
279
  });
244
- }))).filter((result) => result.status === "rejected");
245
- if (queueErrors.length > 0) throw new AggregateError(queueErrors.map(({ reason }) => reason), "Failed to setup queues");
246
- const bindingErrors = (await Promise.allSettled(Object.values(contract.bindings ?? {}).map((binding) => {
280
+ }))).map((result, i) => ({
281
+ result,
282
+ name: (0, _amqp_contract_contract.extractQueue)(queueEntries[i]).name
283
+ })).filter((entry) => entry.result.status === "rejected");
284
+ if (queueErrors.length > 0) {
285
+ const names = queueErrors.map((e) => e.name).join(", ");
286
+ throw new AggregateError(queueErrors.map(({ result }) => result.reason), `Failed to setup queues: ${names}`);
287
+ }
288
+ const bindings = Object.values(contract.bindings ?? {});
289
+ const bindingErrors = (await Promise.allSettled(bindings.map((binding) => {
247
290
  if (binding.type === "queue") return channel.bindQueue(binding.queue.name, binding.exchange.name, binding.routingKey ?? "", binding.arguments);
248
291
  return channel.bindExchange(binding.destination.name, binding.source.name, binding.routingKey ?? "", binding.arguments);
249
- }))).filter((result) => result.status === "rejected");
250
- if (bindingErrors.length > 0) throw new AggregateError(bindingErrors.map(({ reason }) => reason), "Failed to setup bindings");
292
+ }))).map((result, i) => {
293
+ const binding = bindings[i];
294
+ return {
295
+ result,
296
+ name: binding.type === "queue" ? `${binding.exchange.name} -> ${binding.queue.name}` : `${binding.source.name} -> ${binding.destination.name}`
297
+ };
298
+ }).filter((entry) => entry.result.status === "rejected");
299
+ if (bindingErrors.length > 0) {
300
+ const names = bindingErrors.map((e) => e.name).join(", ");
301
+ throw new AggregateError(bindingErrors.map(({ result }) => result.reason), `Failed to setup bindings: ${names}`);
302
+ }
251
303
  }
252
304
  //#endregion
253
305
  //#region src/amqp-client.ts
@@ -266,6 +318,28 @@ function callSetupFunc(setup, channel) {
266
318
  return setup(channel);
267
319
  }
268
320
  /**
321
+ * Default time `waitForConnect` will wait for the broker before erroring out.
322
+ * Defaulting to a finite value (rather than waiting forever) means a fail-fast
323
+ * developer experience: a misconfigured URL, a down broker, or wrong
324
+ * credentials surface as a Result.Error within 30 seconds. Pass `null`
325
+ * explicitly to disable the timeout — `Infinity` and other non-finite values
326
+ * are also coerced to "no timeout" because Node's `setTimeout` clamps large
327
+ * delays to ~24.8 days and silently fires near-immediately on `Infinity`.
328
+ */
329
+ const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
330
+ /**
331
+ * Normalise the user-supplied connect timeout to either a positive finite
332
+ * number of milliseconds, or `null` (no timeout). `Infinity`, `NaN`, and
333
+ * non-positive values all map to `null` rather than being passed to
334
+ * `setTimeout` — see {@link DEFAULT_CONNECT_TIMEOUT_MS}.
335
+ */
336
+ function resolveConnectTimeoutMs(input) {
337
+ if (input === null) return null;
338
+ if (input === void 0) return DEFAULT_CONNECT_TIMEOUT_MS;
339
+ if (!Number.isFinite(input) || input <= 0) return null;
340
+ return input;
341
+ }
342
+ /**
269
343
  * AMQP client that manages connections and channels with automatic topology setup.
270
344
  *
271
345
  * This class handles:
@@ -298,6 +372,8 @@ var AmqpClient = class {
298
372
  channelWrapper;
299
373
  urls;
300
374
  connectionOptions;
375
+ /** Resolved timeout in ms; `null` means "wait forever". */
376
+ connectTimeoutMs;
301
377
  /**
302
378
  * Create a new AMQP client instance.
303
379
  *
@@ -313,6 +389,7 @@ var AmqpClient = class {
313
389
  this.contract = contract;
314
390
  this.urls = options.urls;
315
391
  if (options.connectionOptions !== void 0) this.connectionOptions = options.connectionOptions;
392
+ this.connectTimeoutMs = resolveConnectTimeoutMs(options.connectTimeoutMs);
316
393
  const singleton = ConnectionManagerSingleton.getInstance();
317
394
  this.connection = singleton.getConnection(options.urls, options.connectionOptions);
318
395
  const defaultSetup = (channel) => setupAmqpTopology(channel, this.contract);
@@ -344,10 +421,36 @@ var AmqpClient = class {
344
421
  /**
345
422
  * Wait for the channel to be connected and ready.
346
423
  *
347
- * @returns A Future that resolves when the channel is connected
424
+ * If `connectTimeoutMs` was provided in the constructor options, the returned
425
+ * Future resolves to `Result.Error<TechnicalError>` once the timeout elapses.
426
+ * Without a timeout, this waits forever — amqp-connection-manager retries
427
+ * connections indefinitely and never errors on its own.
428
+ *
429
+ * NOTE: When using `AmqpClient` directly (not via `TypedAmqpClient` /
430
+ * `TypedAmqpWorker`), the constructor has already incremented the pooled
431
+ * connection's reference count. Callers must invoke `close()` on the error
432
+ * path to release the connection — `waitForConnect` does not do this
433
+ * automatically. The typed factories handle this cleanup for you.
434
+ *
435
+ * @returns A Future resolving to `Result.Ok(void)` on connect, or
436
+ * `Result.Error(TechnicalError)` on timeout / connection failure.
348
437
  */
349
438
  waitForConnect() {
350
- return _swan_io_boxed.Future.fromPromise(this.channelWrapper.waitForConnect()).mapError((error) => new TechnicalError("Failed to connect to AMQP broker", error));
439
+ const connectPromise = this.channelWrapper.waitForConnect();
440
+ const timeoutMs = this.connectTimeoutMs;
441
+ const racedPromise = timeoutMs === null ? connectPromise : new Promise((resolve, reject) => {
442
+ const handle = setTimeout(() => {
443
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for AMQP connection after ${timeoutMs}ms`));
444
+ }, timeoutMs);
445
+ connectPromise.then(() => {
446
+ clearTimeout(handle);
447
+ resolve();
448
+ }, (error) => {
449
+ clearTimeout(handle);
450
+ reject(error);
451
+ });
452
+ });
453
+ return _swan_io_boxed.Future.fromPromise(racedPromise).mapError((error) => new TechnicalError("Failed to connect to AMQP broker", error));
351
454
  }
352
455
  /**
353
456
  * Publish a message to an exchange.
@@ -446,7 +549,10 @@ var AmqpClient = class {
446
549
  * @returns A Future that resolves when the channel and connection are closed
447
550
  */
448
551
  close() {
449
- return _swan_io_boxed.Future.fromPromise(this.channelWrapper.close()).mapError((error) => new TechnicalError("Failed to close channel", error)).flatMapOk(() => _swan_io_boxed.Future.fromPromise(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions)).mapError((error) => new TechnicalError("Failed to release connection", error))).mapOk(() => void 0);
552
+ return _swan_io_boxed.Future.fromPromise(this.channelWrapper.close()).mapError((error) => new TechnicalError("Failed to close channel", error)).flatMap((channelResult) => _swan_io_boxed.Future.fromPromise(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions)).mapError((error) => new TechnicalError("Failed to release connection", error)).map((releaseResult) => {
553
+ if (channelResult.isError() && releaseResult.isError()) return _swan_io_boxed.Result.Error(new TechnicalError("Failed to close channel and release connection", new AggregateError([channelResult.error, releaseResult.error], "Failed to close channel and release connection")));
554
+ return channelResult.isError() ? channelResult : releaseResult;
555
+ }));
450
556
  }
451
557
  /**
452
558
  * Reset connection singleton cache (for testing only)
@@ -464,7 +570,9 @@ var AmqpClient = class {
464
570
  * @see https://opentelemetry.io/docs/specs/otel/trace/api/#spankind
465
571
  */
466
572
  const SpanKind = {
573
+ /** Producer span represents a message producer */
467
574
  PRODUCER: 3,
575
+ /** Consumer span represents a message consumer */
468
576
  CONSUMER: 4
469
577
  };
470
578
  /**
@@ -491,13 +599,27 @@ const MessagingSemanticConventions = {
491
599
  * Instrumentation scope name for amqp-contract.
492
600
  */
493
601
  const INSTRUMENTATION_SCOPE_NAME = "@amqp-contract";
494
- const INSTRUMENTATION_SCOPE_VERSION = "0.1.0";
602
+ /**
603
+ * Instrumentation scope version, sourced from this package's package.json so
604
+ * the OTel meter version always tracks the released library version. We use
605
+ * `createRequire` rather than a JSON import attribute so the same source builds
606
+ * to ESM, CJS, and runs under bundlers that don't yet understand
607
+ * `import … with { type: "json" }`.
608
+ */
609
+ const INSTRUMENTATION_SCOPE_VERSION = (() => {
610
+ try {
611
+ return (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json").version ?? "0.0.0";
612
+ } catch {
613
+ return "0.0.0";
614
+ }
615
+ })();
495
616
  let otelApi;
496
617
  let cachedTracer;
497
618
  let cachedPublishCounter;
498
619
  let cachedConsumeCounter;
499
620
  let cachedPublishLatencyHistogram;
500
621
  let cachedConsumeLatencyHistogram;
622
+ let cachedLateRpcReplyCounter;
501
623
  /**
502
624
  * Try to load the OpenTelemetry API module.
503
625
  * Returns null if the module is not available.
@@ -528,14 +650,16 @@ function getMeterInstruments() {
528
650
  publishCounter: cachedPublishCounter,
529
651
  consumeCounter: cachedConsumeCounter,
530
652
  publishLatencyHistogram: cachedPublishLatencyHistogram,
531
- consumeLatencyHistogram: cachedConsumeLatencyHistogram
653
+ consumeLatencyHistogram: cachedConsumeLatencyHistogram,
654
+ lateRpcReplyCounter: cachedLateRpcReplyCounter
532
655
  };
533
656
  const api = tryLoadOpenTelemetryApi();
534
657
  if (!api) return {
535
658
  publishCounter: void 0,
536
659
  consumeCounter: void 0,
537
660
  publishLatencyHistogram: void 0,
538
- consumeLatencyHistogram: void 0
661
+ consumeLatencyHistogram: void 0,
662
+ lateRpcReplyCounter: void 0
539
663
  };
540
664
  const meter = api.metrics.getMeter(INSTRUMENTATION_SCOPE_NAME, INSTRUMENTATION_SCOPE_VERSION);
541
665
  cachedPublishCounter = meter.createCounter("amqp.client.messages.published", {
@@ -554,11 +678,16 @@ function getMeterInstruments() {
554
678
  description: "Duration of message processing operations",
555
679
  unit: "ms"
556
680
  });
681
+ cachedLateRpcReplyCounter = meter.createCounter("amqp.client.rpc.late_reply", {
682
+ description: "RPC replies received after the caller stopped waiting (timeout, cancellation, or unknown correlationId)",
683
+ unit: "{message}"
684
+ });
557
685
  return {
558
686
  publishCounter: cachedPublishCounter,
559
687
  consumeCounter: cachedConsumeCounter,
560
688
  publishLatencyHistogram: cachedPublishLatencyHistogram,
561
- consumeLatencyHistogram: cachedConsumeLatencyHistogram
689
+ consumeLatencyHistogram: cachedConsumeLatencyHistogram,
690
+ lateRpcReplyCounter: cachedLateRpcReplyCounter
562
691
  };
563
692
  }
564
693
  /**
@@ -569,7 +698,8 @@ const defaultTelemetryProvider = {
569
698
  getPublishCounter: () => getMeterInstruments().publishCounter,
570
699
  getConsumeCounter: () => getMeterInstruments().consumeCounter,
571
700
  getPublishLatencyHistogram: () => getMeterInstruments().publishLatencyHistogram,
572
- getConsumeLatencyHistogram: () => getMeterInstruments().consumeLatencyHistogram
701
+ getConsumeLatencyHistogram: () => getMeterInstruments().consumeLatencyHistogram,
702
+ getLateRpcReplyCounter: () => getMeterInstruments().lateRpcReplyCounter
573
703
  };
574
704
  /**
575
705
  * Create a span for a publish operation.
@@ -667,6 +797,22 @@ function recordConsumeMetric(provider, queueName, consumerName, success, duratio
667
797
  consumeLatencyHistogram?.record(durationMs, attributes);
668
798
  }
669
799
  /**
800
+ * Record an RPC reply that arrived after the caller stopped waiting.
801
+ *
802
+ * @param reason - Why the reply was orphaned. `"unknown-correlation-id"` is
803
+ * the typical "caller already timed out" case; `"missing-correlation-id"`
804
+ * means the broker delivered a reply with no correlationId at all (a
805
+ * protocol violation by the responder).
806
+ */
807
+ function recordLateRpcReply(provider, reason) {
808
+ const counter = provider.getLateRpcReplyCounter();
809
+ const attributes = {
810
+ [MessagingSemanticConventions.MESSAGING_SYSTEM]: MessagingSemanticConventions.MESSAGING_SYSTEM_RABBITMQ,
811
+ reason
812
+ };
813
+ counter?.add(1, attributes);
814
+ }
815
+ /**
670
816
  * Reset the cached OpenTelemetry API module and instruments.
671
817
  * For testing purposes only.
672
818
  * @internal
@@ -678,18 +824,22 @@ function _resetTelemetryCacheForTesting() {
678
824
  cachedConsumeCounter = void 0;
679
825
  cachedPublishLatencyHistogram = void 0;
680
826
  cachedConsumeLatencyHistogram = void 0;
827
+ cachedLateRpcReplyCounter = void 0;
681
828
  }
682
829
  //#endregion
683
830
  exports.AmqpClient = AmqpClient;
684
- exports.ConnectionManagerSingleton = ConnectionManagerSingleton;
831
+ exports.DEFAULT_CONNECT_TIMEOUT_MS = DEFAULT_CONNECT_TIMEOUT_MS;
685
832
  exports.MessageValidationError = MessageValidationError;
686
833
  exports.MessagingSemanticConventions = MessagingSemanticConventions;
687
834
  exports.TechnicalError = TechnicalError;
835
+ exports._getConnectionCountForTesting = _getConnectionCountForTesting;
836
+ exports._resetConnectionsForTesting = _resetConnectionsForTesting;
688
837
  exports._resetTelemetryCacheForTesting = _resetTelemetryCacheForTesting;
689
838
  exports.defaultTelemetryProvider = defaultTelemetryProvider;
690
839
  exports.endSpanError = endSpanError;
691
840
  exports.endSpanSuccess = endSpanSuccess;
692
841
  exports.recordConsumeMetric = recordConsumeMetric;
842
+ exports.recordLateRpcReply = recordLateRpcReply;
693
843
  exports.recordPublishMetric = recordPublishMetric;
694
844
  exports.setupAmqpTopology = setupAmqpTopology;
695
845
  exports.startConsumeSpan = startConsumeSpan;
package/dist/index.d.cts CHANGED
@@ -31,17 +31,32 @@ declare class MessageValidationError extends Error {
31
31
  }
32
32
  //#endregion
33
33
  //#region src/amqp-client.d.ts
34
+ /**
35
+ * Default time `waitForConnect` will wait for the broker before erroring out.
36
+ * Defaulting to a finite value (rather than waiting forever) means a fail-fast
37
+ * developer experience: a misconfigured URL, a down broker, or wrong
38
+ * credentials surface as a Result.Error within 30 seconds. Pass `null`
39
+ * explicitly to disable the timeout — `Infinity` and other non-finite values
40
+ * are also coerced to "no timeout" because Node's `setTimeout` clamps large
41
+ * delays to ~24.8 days and silently fires near-immediately on `Infinity`.
42
+ */
43
+ declare const DEFAULT_CONNECT_TIMEOUT_MS = 30000;
34
44
  /**
35
45
  * Options for creating an AMQP client.
36
46
  *
37
47
  * @property urls - AMQP broker URL(s). Multiple URLs provide failover support.
38
48
  * @property connectionOptions - Optional connection configuration (heartbeat, reconnect settings, etc.).
39
49
  * @property channelOptions - Optional channel configuration options.
50
+ * @property connectTimeoutMs - Maximum time in ms to wait for the channel to
51
+ * become ready in `waitForConnect`. Defaults to {@link DEFAULT_CONNECT_TIMEOUT_MS}.
52
+ * Pass `null` to disable the timeout entirely (amqp-connection-manager will
53
+ * retry indefinitely).
40
54
  */
41
55
  type AmqpClientOptions = {
42
56
  urls: ConnectionUrl[];
43
57
  connectionOptions?: AmqpConnectionManagerOptions | undefined;
44
58
  channelOptions?: Partial<CreateChannelOpts> | undefined;
59
+ connectTimeoutMs?: number | null | undefined;
45
60
  };
46
61
  /**
47
62
  * Callback type for consuming messages.
@@ -93,6 +108,8 @@ declare class AmqpClient {
93
108
  private readonly channelWrapper;
94
109
  private readonly urls;
95
110
  private readonly connectionOptions?;
111
+ /** Resolved timeout in ms; `null` means "wait forever". */
112
+ private readonly connectTimeoutMs;
96
113
  /**
97
114
  * Create a new AMQP client instance.
98
115
  *
@@ -118,7 +135,19 @@ declare class AmqpClient {
118
135
  /**
119
136
  * Wait for the channel to be connected and ready.
120
137
  *
121
- * @returns A Future that resolves when the channel is connected
138
+ * If `connectTimeoutMs` was provided in the constructor options, the returned
139
+ * Future resolves to `Result.Error<TechnicalError>` once the timeout elapses.
140
+ * Without a timeout, this waits forever — amqp-connection-manager retries
141
+ * connections indefinitely and never errors on its own.
142
+ *
143
+ * NOTE: When using `AmqpClient` directly (not via `TypedAmqpClient` /
144
+ * `TypedAmqpWorker`), the constructor has already incremented the pooled
145
+ * connection's reference count. Callers must invoke `close()` on the error
146
+ * path to release the connection — `waitForConnect` does not do this
147
+ * automatically. The typed factories handle this cleanup for you.
148
+ *
149
+ * @returns A Future resolving to `Result.Ok(void)` on connect, or
150
+ * `Result.Error(TechnicalError)` on timeout / connection failure.
122
151
  */
123
152
  waitForConnect(): Future<Result<void, TechnicalError>>;
124
153
  /**
@@ -211,85 +240,20 @@ declare class AmqpClient {
211
240
  //#endregion
212
241
  //#region src/connection-manager.d.ts
213
242
  /**
214
- * Connection manager singleton for sharing AMQP connections across clients.
243
+ * Number of active pooled connections. Test-only helper exposed in lieu of
244
+ * the underlying singleton, which is intentionally not part of the public API
245
+ * (mutating it from outside the library can break in-flight clients sharing a
246
+ * connection).
215
247
  *
216
- * This singleton implements connection pooling to avoid creating multiple connections
217
- * to the same broker, which is a RabbitMQ best practice. Connections are identified
218
- * by their URLs and connection options, and reference counting ensures connections
219
- * are only closed when all clients have released them.
248
+ * @internal
249
+ */
250
+ declare function _getConnectionCountForTesting(): number;
251
+ /**
252
+ * Close every pooled connection and clear ref-counts. Test-only helper.
220
253
  *
221
- * @example
222
- * ```typescript
223
- * const manager = ConnectionManagerSingleton.getInstance();
224
- * const connection = manager.getConnection(['amqp://localhost']);
225
- * // ... use connection ...
226
- * await manager.releaseConnection(['amqp://localhost']);
227
- * ```
254
+ * @internal
228
255
  */
229
- declare class ConnectionManagerSingleton {
230
- private static instance;
231
- private connections;
232
- private refCounts;
233
- private constructor();
234
- /**
235
- * Get the singleton instance of the connection manager.
236
- *
237
- * @returns The singleton instance
238
- */
239
- static getInstance(): ConnectionManagerSingleton;
240
- /**
241
- * Get or create a connection for the given URLs and options.
242
- *
243
- * If a connection already exists with the same URLs and options, it is reused
244
- * and its reference count is incremented. Otherwise, a new connection is created.
245
- *
246
- * @param urls - AMQP broker URL(s)
247
- * @param connectionOptions - Optional connection configuration
248
- * @returns The AMQP connection manager instance
249
- */
250
- getConnection(urls: ConnectionUrl[], connectionOptions?: AmqpConnectionManagerOptions): AmqpConnectionManager;
251
- /**
252
- * Release a connection reference.
253
- *
254
- * Decrements the reference count for the connection. If the count reaches zero,
255
- * the connection is closed and removed from the pool.
256
- *
257
- * @param urls - AMQP broker URL(s) used to identify the connection
258
- * @param connectionOptions - Optional connection configuration used to identify the connection
259
- * @returns A promise that resolves when the connection is released (and closed if necessary)
260
- */
261
- releaseConnection(urls: ConnectionUrl[], connectionOptions?: AmqpConnectionManagerOptions): Promise<void>;
262
- /**
263
- * Create a unique key for a connection based on URLs and options.
264
- *
265
- * The key is deterministic: same URLs and options always produce the same key,
266
- * enabling connection reuse.
267
- *
268
- * @param urls - AMQP broker URL(s)
269
- * @param connectionOptions - Optional connection configuration
270
- * @returns A unique string key identifying the connection
271
- */
272
- private createConnectionKey;
273
- /**
274
- * Serialize connection options to a deterministic string.
275
- *
276
- * @param options - Connection options to serialize
277
- * @returns A JSON string with sorted keys for deterministic comparison
278
- */
279
- private serializeOptions;
280
- /**
281
- * Deep sort an object's keys for deterministic serialization.
282
- *
283
- * @param value - The value to deep sort (can be object, array, or primitive)
284
- * @returns The value with all object keys sorted alphabetically
285
- */
286
- private deepSort;
287
- /**
288
- * Reset all cached connections (for testing purposes)
289
- * @internal
290
- */
291
- _resetForTesting(): Promise<void>;
292
- }
256
+ declare function _resetConnectionsForTesting(): Promise<void>;
293
257
  //#endregion
294
258
  //#region src/logger.d.ts
295
259
  /**
@@ -421,6 +385,12 @@ type TelemetryProvider = {
421
385
  * Returns undefined if OpenTelemetry is not available.
422
386
  */
423
387
  getConsumeLatencyHistogram: () => Histogram | undefined;
388
+ /**
389
+ * Get a counter for RPC replies that arrive after the caller has gone away
390
+ * (timeout, cancellation, or unknown correlationId). Returns undefined if
391
+ * OpenTelemetry is not available.
392
+ */
393
+ getLateRpcReplyCounter: () => Counter | undefined;
424
394
  };
425
395
  /**
426
396
  * Default telemetry provider that uses OpenTelemetry API if available.
@@ -452,6 +422,15 @@ declare function recordPublishMetric(provider: TelemetryProvider, exchangeName:
452
422
  * Record a consume metric.
453
423
  */
454
424
  declare function recordConsumeMetric(provider: TelemetryProvider, queueName: string, consumerName: string, success: boolean, durationMs: number): void;
425
+ /**
426
+ * Record an RPC reply that arrived after the caller stopped waiting.
427
+ *
428
+ * @param reason - Why the reply was orphaned. `"unknown-correlation-id"` is
429
+ * the typical "caller already timed out" case; `"missing-correlation-id"`
430
+ * means the broker delivered a reply with no correlationId at all (a
431
+ * protocol violation by the responder).
432
+ */
433
+ declare function recordLateRpcReply(provider: TelemetryProvider, reason: "unknown-correlation-id" | "missing-correlation-id"): void;
455
434
  /**
456
435
  * Reset the cached OpenTelemetry API module and instruments.
457
436
  * For testing purposes only.
@@ -459,5 +438,5 @@ declare function recordConsumeMetric(provider: TelemetryProvider, queueName: str
459
438
  */
460
439
  declare function _resetTelemetryCacheForTesting(): void;
461
440
  //#endregion
462
- export { AmqpClient, type AmqpClientOptions, ConnectionManagerSingleton, type ConsumeCallback, type ConsumerOptions, type Logger, type LoggerContext, MessageValidationError, MessagingSemanticConventions, type PublishOptions, TechnicalError, type TelemetryProvider, _resetTelemetryCacheForTesting, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, recordPublishMetric, setupAmqpTopology, startConsumeSpan, startPublishSpan };
441
+ 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 };
463
442
  //# 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/setup.ts","../src/telemetry.ts"],"mappings":"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"}