@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.d.mts 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.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":";;;;;;;;;;;;;cAMa,cAAA,SAAuB,KAAA;EAAA,SAGP,KAAA;cADzB,OAAA,UACyB,KAAA;AAAA;;;;;;;;;AAuB7B;cAAa,sBAAA,SAA+B,KAAA;EAAA,SAExB,MAAA;EAAA,SACA,MAAA;cADA,MAAA,UACA,MAAA;AAAA;;;;;AA7BpB;;;;;KCsCY,iBAAA;EACV,IAAA,EAAM,aAAA;EACN,iBAAA,GAAoB,4BAAA;EACpB,cAAA,GAAiB,OAAA,CAAQ,iBAAA;AAAA;;ADf3B;;KCqBY,eAAA,IAAmB,GAAA,EAAK,cAAA,mBAAiC,OAAA;;;;KAKzD,cAAA,GAAiB,OAAA,CAAQ,OAAA;kDAEnC,OAAA;AAAA;;;;KAMU,eAAA,GAAkB,OAAA,CAAQ,OAAA;uCAEpC,QAAA;AAAA;;;;;;;;;;;;;;;;;AAfF;;;;;;;;;AAKA;;;cAyCa,UAAA;EAAA,iBAkBQ,QAAA;EAAA,iBAjBF,UAAA;EAAA,iBACA,cAAA;EAAA,iBACA,IAAA;EAAA,iBACA,iBAAA;EArCP;;;;;;;;;AAiCZ;;cAkBqB,QAAA,EAAU,kBAAA,EAC3B,OAAA,EAAS,iBAAA;EADkB;;;;;;;;;EA+C7B,aAAA,CAAA,GAAiB,qBAAA;EA6Bd;;;;;EApBH,cAAA,CAAA,GAAkB,MAAA,CAAO,MAAA,OAAa,cAAA;EAsD1B;;;;;;;;;EAvCZ,OAAA,CACE,QAAA,UACA,UAAA,UACA,OAAA,EAAS,MAAA,YACT,OAAA,GAAU,cAAA,GACT,MAAA,CAAO,MAAA,UAAgB,cAAA;EAkFA;;;;;;;;EApE1B,WAAA,CACE,KAAA,UACA,OAAA,EAAS,MAAA,YACT,OAAA,GAAU,cAAA,GACT,MAAA,CAAO,MAAA,UAAgB,cAAA;EA/GT;;;;;;;;EA6HjB,OAAA,CACE,KAAA,UACA,QAAA,EAAU,eAAA,EACV,OAAA,GAAU,eAAA,GACT,MAAA,CAAO,MAAA,SAAe,cAAA;EAjEzB;;;;;;EA6EA,MAAA,CAAO,WAAA,WAAsB,MAAA,CAAO,MAAA,OAAa,cAAA;EApD/C;;;;;;EAgEF,GAAA,CAAI,GAAA,EAAK,cAAA,EAAgB,OAAA;EA5Df;;;;;;;EAuEV,IAAA,CAAK,GAAA,EAAK,cAAA,EAAgB,OAAA,YAAiB,OAAA;EArDxC;;;;;;;EAgEH,QAAA,CAAS,KAAA,GAAQ,OAAA,EAAS,OAAA,YAAmB,OAAA;EA/C3C;;;;;;;;;;;EA8DF,EAAA,CAAG,KAAA,UAAe,QAAA,MAAc,IAAA;EArCP;;;;;;;;;;EAmDzB,KAAA,CAAA,GAAS,MAAA,CAAO,MAAA,OAAa,cAAA;EAd7B;;;;EAAA,OAgCa,+BAAA,CAAA,GAAmC,OAAA;AAAA;;;;;;;;;AD5TlD;;;;;;;;;;cEgBa,0BAAA;EAAA,eACI,QAAA;EAAA,QACP,WAAA;EAAA,QACA,SAAA;EAAA,QAED,WAAA,CAAA;EFKmC;;;;;EAAA,OEEnC,WAAA,CAAA,GAAe,0BAAA;EFCW;;;;;ACSnC;;;;;ECOE,aAAA,CACE,IAAA,EAAM,aAAA,IACN,iBAAA,GAAoB,4BAAA,GACnB,qBAAA;EDPc;;;;;;;;;;ECiCX,iBAAA,CACJ,IAAA,EAAM,aAAA,IACN,iBAAA,GAAoB,4BAAA,GACnB,OAAA;EDpCuC;AAM5C;;;;;;;;;EAN4C,QCgElC,mBAAA;EDrDgB;;;;;;EAAA,QCuEhB,gBAAA;EDrED;AAMT;;;;;EANS,QCiFC,QAAA;EDzER;;;AA+BF;ECkEQ,gBAAA,CAAA,GAAoB,OAAA;AAAA;;;;;;;;;;AF/J5B;KGEY,aAAA,GAAgB,MAAA;EAC1B,KAAA;AAAA;;;;;;;;AHuBF;;;;;;;;;;KGHY,MAAA;EHMuB;;;;ACSnC;EETE,KAAA,CAAM,OAAA,UAAiB,OAAA,GAAU,aAAA;;;;;;EAOjC,IAAA,CAAK,OAAA,UAAiB,OAAA,GAAU,aAAA;EFKR;;;;;EEExB,IAAA,CAAK,OAAA,UAAiB,OAAA,GAAU,aAAA;EFFf;;;;AAMnB;EEGE,KAAA,CAAM,OAAA,UAAiB,OAAA,GAAU,aAAA;AAAA;;;;;;;;AHlDnC;;;;;;;;;;;AA0BA;;;;iBIPsB,iBAAA,CACpB,OAAA,EAAS,OAAA,EACT,QAAA,EAAU,kBAAA,GACT,OAAA;;;;;;;cCJU,4BAAA;EAAA;;;;;;;;;;;;;;;;;;;KA4BD,iBAAA;ELlBQ;;;;EKuBlB,SAAA,QAAiB,MAAA;;;AJbnB;;EImBE,iBAAA,QAAyB,OAAA;EJlBnB;;;;EIwBN,iBAAA,QAAyB,OAAA;EJtBD;;;;EI4BxB,0BAAA,QAAkC,SAAA;EJ5BlC;;;;EIkCA,0BAAA,QAAkC,SAAA;AAAA;;;;cAgHvB,wBAAA,EAA0B,iBAAA;;;;;iBAYvB,gBAAA,CACd,QAAA,EAAU,iBAAA,EACV,YAAA,UACA,UAAA,sBACA,UAAA,GAAa,UAAA,GACZ,IAAA;;;;;iBA8Ba,gBAAA,CACd,QAAA,EAAU,iBAAA,EACV,SAAA,UACA,YAAA,UACA,UAAA,GAAa,UAAA,GACZ,IAAA;;;;iBA2Ba,cAAA,CAAe,IAAA,EAAM,IAAA;;;;iBAerB,YAAA,CAAa,IAAA,EAAM,IAAA,cAAkB,KAAA,EAAO,KAAA;;;;iBAiB5C,mBAAA,CACd,QAAA,EAAU,iBAAA,EACV,YAAA,UACA,UAAA,sBACA,OAAA,WACA,UAAA;AJlNF;;;AAAA,iBIwOgB,mBAAA,CACd,QAAA,EAAU,iBAAA,EACV,SAAA,UACA,YAAA,UACA,OAAA,WACA,UAAA;;;;;;iBAsBc,8BAAA,CAAA"}
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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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { Future } from "@swan-io/boxed";
2
+ import { Future, Result } from "@swan-io/boxed";
3
3
  import amqp from "amqp-connection-manager";
4
4
  import { extractQueue } from "@amqp-contract/contract";
5
5
  //#region \0rolldown/runtime.js
@@ -119,6 +119,14 @@ var ConnectionManagerSingleton = class ConnectionManagerSingleton {
119
119
  return value;
120
120
  }
121
121
  /**
122
+ * Get the number of active pooled connections.
123
+ *
124
+ * @internal
125
+ */
126
+ _getConnectionCountForTesting() {
127
+ return this.connections.size;
128
+ }
129
+ /**
122
130
  * Reset all cached connections (for testing purposes)
123
131
  * @internal
124
132
  */
@@ -129,6 +137,25 @@ var ConnectionManagerSingleton = class ConnectionManagerSingleton {
129
137
  this.refCounts.clear();
130
138
  }
131
139
  };
140
+ /**
141
+ * Number of active pooled connections. Test-only helper — exposed in lieu of
142
+ * the underlying singleton, which is intentionally not part of the public API
143
+ * (mutating it from outside the library can break in-flight clients sharing a
144
+ * connection).
145
+ *
146
+ * @internal
147
+ */
148
+ function _getConnectionCountForTesting() {
149
+ return ConnectionManagerSingleton.getInstance()._getConnectionCountForTesting();
150
+ }
151
+ /**
152
+ * Close every pooled connection and clear ref-counts. Test-only helper.
153
+ *
154
+ * @internal
155
+ */
156
+ function _resetConnectionsForTesting() {
157
+ return ConnectionManagerSingleton.getInstance()._resetForTesting();
158
+ }
132
159
  //#endregion
133
160
  //#region src/errors.ts
134
161
  /**
@@ -188,13 +215,20 @@ var MessageValidationError = class extends Error {
188
215
  * ```
189
216
  */
190
217
  async function setupAmqpTopology(channel, contract) {
191
- const exchangeErrors = (await Promise.allSettled(Object.values(contract.exchanges ?? {}).map((exchange) => channel.assertExchange(exchange.name, exchange.type, {
218
+ const exchanges = Object.values(contract.exchanges ?? {}).filter((e) => e.name !== "");
219
+ const exchangeErrors = (await Promise.allSettled(exchanges.map((exchange) => channel.assertExchange(exchange.name, exchange.type, {
192
220
  durable: exchange.durable,
193
221
  autoDelete: exchange.autoDelete,
194
222
  internal: exchange.internal,
195
223
  arguments: exchange.arguments
196
- })))).filter((result) => result.status === "rejected");
197
- if (exchangeErrors.length > 0) throw new AggregateError(exchangeErrors.map(({ reason }) => reason), "Failed to setup exchanges");
224
+ })))).map((result, i) => ({
225
+ result,
226
+ name: exchanges[i].name
227
+ })).filter((entry) => entry.result.status === "rejected");
228
+ if (exchangeErrors.length > 0) {
229
+ const names = exchangeErrors.map((e) => e.name).join(", ");
230
+ throw new AggregateError(exchangeErrors.map(({ result }) => result.reason), `Failed to setup exchanges: ${names}`);
231
+ }
198
232
  for (const queueEntry of Object.values(contract.queues ?? {})) {
199
233
  const queue = extractQueue(queueEntry);
200
234
  if (queue.deadLetter) {
@@ -202,7 +236,8 @@ async function setupAmqpTopology(channel, contract) {
202
236
  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.`);
203
237
  }
204
238
  }
205
- const queueErrors = (await Promise.allSettled(Object.values(contract.queues ?? {}).map((queueEntry) => {
239
+ const queueEntries = Object.values(contract.queues ?? {});
240
+ const queueErrors = (await Promise.allSettled(queueEntries.map((queueEntry) => {
206
241
  const queue = extractQueue(queueEntry);
207
242
  const queueArguments = { ...queue.arguments };
208
243
  queueArguments["x-queue-type"] = queue.type;
@@ -221,13 +256,29 @@ async function setupAmqpTopology(channel, contract) {
221
256
  autoDelete: queue.autoDelete,
222
257
  arguments: queueArguments
223
258
  });
224
- }))).filter((result) => result.status === "rejected");
225
- if (queueErrors.length > 0) throw new AggregateError(queueErrors.map(({ reason }) => reason), "Failed to setup queues");
226
- const bindingErrors = (await Promise.allSettled(Object.values(contract.bindings ?? {}).map((binding) => {
259
+ }))).map((result, i) => ({
260
+ result,
261
+ name: extractQueue(queueEntries[i]).name
262
+ })).filter((entry) => entry.result.status === "rejected");
263
+ if (queueErrors.length > 0) {
264
+ const names = queueErrors.map((e) => e.name).join(", ");
265
+ throw new AggregateError(queueErrors.map(({ result }) => result.reason), `Failed to setup queues: ${names}`);
266
+ }
267
+ const bindings = Object.values(contract.bindings ?? {});
268
+ const bindingErrors = (await Promise.allSettled(bindings.map((binding) => {
227
269
  if (binding.type === "queue") return channel.bindQueue(binding.queue.name, binding.exchange.name, binding.routingKey ?? "", binding.arguments);
228
270
  return channel.bindExchange(binding.destination.name, binding.source.name, binding.routingKey ?? "", binding.arguments);
229
- }))).filter((result) => result.status === "rejected");
230
- if (bindingErrors.length > 0) throw new AggregateError(bindingErrors.map(({ reason }) => reason), "Failed to setup bindings");
271
+ }))).map((result, i) => {
272
+ const binding = bindings[i];
273
+ return {
274
+ result,
275
+ name: binding.type === "queue" ? `${binding.exchange.name} -> ${binding.queue.name}` : `${binding.source.name} -> ${binding.destination.name}`
276
+ };
277
+ }).filter((entry) => entry.result.status === "rejected");
278
+ if (bindingErrors.length > 0) {
279
+ const names = bindingErrors.map((e) => e.name).join(", ");
280
+ throw new AggregateError(bindingErrors.map(({ result }) => result.reason), `Failed to setup bindings: ${names}`);
281
+ }
231
282
  }
232
283
  //#endregion
233
284
  //#region src/amqp-client.ts
@@ -246,6 +297,28 @@ function callSetupFunc(setup, channel) {
246
297
  return setup(channel);
247
298
  }
248
299
  /**
300
+ * Default time `waitForConnect` will wait for the broker before erroring out.
301
+ * Defaulting to a finite value (rather than waiting forever) means a fail-fast
302
+ * developer experience: a misconfigured URL, a down broker, or wrong
303
+ * credentials surface as a Result.Error within 30 seconds. Pass `null`
304
+ * explicitly to disable the timeout — `Infinity` and other non-finite values
305
+ * are also coerced to "no timeout" because Node's `setTimeout` clamps large
306
+ * delays to ~24.8 days and silently fires near-immediately on `Infinity`.
307
+ */
308
+ const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
309
+ /**
310
+ * Normalise the user-supplied connect timeout to either a positive finite
311
+ * number of milliseconds, or `null` (no timeout). `Infinity`, `NaN`, and
312
+ * non-positive values all map to `null` rather than being passed to
313
+ * `setTimeout` — see {@link DEFAULT_CONNECT_TIMEOUT_MS}.
314
+ */
315
+ function resolveConnectTimeoutMs(input) {
316
+ if (input === null) return null;
317
+ if (input === void 0) return DEFAULT_CONNECT_TIMEOUT_MS;
318
+ if (!Number.isFinite(input) || input <= 0) return null;
319
+ return input;
320
+ }
321
+ /**
249
322
  * AMQP client that manages connections and channels with automatic topology setup.
250
323
  *
251
324
  * This class handles:
@@ -278,6 +351,8 @@ var AmqpClient = class {
278
351
  channelWrapper;
279
352
  urls;
280
353
  connectionOptions;
354
+ /** Resolved timeout in ms; `null` means "wait forever". */
355
+ connectTimeoutMs;
281
356
  /**
282
357
  * Create a new AMQP client instance.
283
358
  *
@@ -293,6 +368,7 @@ var AmqpClient = class {
293
368
  this.contract = contract;
294
369
  this.urls = options.urls;
295
370
  if (options.connectionOptions !== void 0) this.connectionOptions = options.connectionOptions;
371
+ this.connectTimeoutMs = resolveConnectTimeoutMs(options.connectTimeoutMs);
296
372
  const singleton = ConnectionManagerSingleton.getInstance();
297
373
  this.connection = singleton.getConnection(options.urls, options.connectionOptions);
298
374
  const defaultSetup = (channel) => setupAmqpTopology(channel, this.contract);
@@ -324,10 +400,36 @@ var AmqpClient = class {
324
400
  /**
325
401
  * Wait for the channel to be connected and ready.
326
402
  *
327
- * @returns A Future that resolves when the channel is connected
403
+ * If `connectTimeoutMs` was provided in the constructor options, the returned
404
+ * Future resolves to `Result.Error<TechnicalError>` once the timeout elapses.
405
+ * Without a timeout, this waits forever — amqp-connection-manager retries
406
+ * connections indefinitely and never errors on its own.
407
+ *
408
+ * NOTE: When using `AmqpClient` directly (not via `TypedAmqpClient` /
409
+ * `TypedAmqpWorker`), the constructor has already incremented the pooled
410
+ * connection's reference count. Callers must invoke `close()` on the error
411
+ * path to release the connection — `waitForConnect` does not do this
412
+ * automatically. The typed factories handle this cleanup for you.
413
+ *
414
+ * @returns A Future resolving to `Result.Ok(void)` on connect, or
415
+ * `Result.Error(TechnicalError)` on timeout / connection failure.
328
416
  */
329
417
  waitForConnect() {
330
- return Future.fromPromise(this.channelWrapper.waitForConnect()).mapError((error) => new TechnicalError("Failed to connect to AMQP broker", error));
418
+ const connectPromise = this.channelWrapper.waitForConnect();
419
+ const timeoutMs = this.connectTimeoutMs;
420
+ const racedPromise = timeoutMs === null ? connectPromise : new Promise((resolve, reject) => {
421
+ const handle = setTimeout(() => {
422
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for AMQP connection after ${timeoutMs}ms`));
423
+ }, timeoutMs);
424
+ connectPromise.then(() => {
425
+ clearTimeout(handle);
426
+ resolve();
427
+ }, (error) => {
428
+ clearTimeout(handle);
429
+ reject(error);
430
+ });
431
+ });
432
+ return Future.fromPromise(racedPromise).mapError((error) => new TechnicalError("Failed to connect to AMQP broker", error));
331
433
  }
332
434
  /**
333
435
  * Publish a message to an exchange.
@@ -426,7 +528,10 @@ var AmqpClient = class {
426
528
  * @returns A Future that resolves when the channel and connection are closed
427
529
  */
428
530
  close() {
429
- return Future.fromPromise(this.channelWrapper.close()).mapError((error) => new TechnicalError("Failed to close channel", error)).flatMapOk(() => Future.fromPromise(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions)).mapError((error) => new TechnicalError("Failed to release connection", error))).mapOk(() => void 0);
531
+ return Future.fromPromise(this.channelWrapper.close()).mapError((error) => new TechnicalError("Failed to close channel", error)).flatMap((channelResult) => Future.fromPromise(ConnectionManagerSingleton.getInstance().releaseConnection(this.urls, this.connectionOptions)).mapError((error) => new TechnicalError("Failed to release connection", error)).map((releaseResult) => {
532
+ if (channelResult.isError() && releaseResult.isError()) return Result.Error(new TechnicalError("Failed to close channel and release connection", new AggregateError([channelResult.error, releaseResult.error], "Failed to close channel and release connection")));
533
+ return channelResult.isError() ? channelResult : releaseResult;
534
+ }));
430
535
  }
431
536
  /**
432
537
  * Reset connection singleton cache (for testing only)
@@ -444,7 +549,9 @@ var AmqpClient = class {
444
549
  * @see https://opentelemetry.io/docs/specs/otel/trace/api/#spankind
445
550
  */
446
551
  const SpanKind = {
552
+ /** Producer span represents a message producer */
447
553
  PRODUCER: 3,
554
+ /** Consumer span represents a message consumer */
448
555
  CONSUMER: 4
449
556
  };
450
557
  /**
@@ -471,13 +578,27 @@ const MessagingSemanticConventions = {
471
578
  * Instrumentation scope name for amqp-contract.
472
579
  */
473
580
  const INSTRUMENTATION_SCOPE_NAME = "@amqp-contract";
474
- const INSTRUMENTATION_SCOPE_VERSION = "0.1.0";
581
+ /**
582
+ * Instrumentation scope version, sourced from this package's package.json so
583
+ * the OTel meter version always tracks the released library version. We use
584
+ * `createRequire` rather than a JSON import attribute so the same source builds
585
+ * to ESM, CJS, and runs under bundlers that don't yet understand
586
+ * `import … with { type: "json" }`.
587
+ */
588
+ const INSTRUMENTATION_SCOPE_VERSION = (() => {
589
+ try {
590
+ return createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
591
+ } catch {
592
+ return "0.0.0";
593
+ }
594
+ })();
475
595
  let otelApi;
476
596
  let cachedTracer;
477
597
  let cachedPublishCounter;
478
598
  let cachedConsumeCounter;
479
599
  let cachedPublishLatencyHistogram;
480
600
  let cachedConsumeLatencyHistogram;
601
+ let cachedLateRpcReplyCounter;
481
602
  /**
482
603
  * Try to load the OpenTelemetry API module.
483
604
  * Returns null if the module is not available.
@@ -508,14 +629,16 @@ function getMeterInstruments() {
508
629
  publishCounter: cachedPublishCounter,
509
630
  consumeCounter: cachedConsumeCounter,
510
631
  publishLatencyHistogram: cachedPublishLatencyHistogram,
511
- consumeLatencyHistogram: cachedConsumeLatencyHistogram
632
+ consumeLatencyHistogram: cachedConsumeLatencyHistogram,
633
+ lateRpcReplyCounter: cachedLateRpcReplyCounter
512
634
  };
513
635
  const api = tryLoadOpenTelemetryApi();
514
636
  if (!api) return {
515
637
  publishCounter: void 0,
516
638
  consumeCounter: void 0,
517
639
  publishLatencyHistogram: void 0,
518
- consumeLatencyHistogram: void 0
640
+ consumeLatencyHistogram: void 0,
641
+ lateRpcReplyCounter: void 0
519
642
  };
520
643
  const meter = api.metrics.getMeter(INSTRUMENTATION_SCOPE_NAME, INSTRUMENTATION_SCOPE_VERSION);
521
644
  cachedPublishCounter = meter.createCounter("amqp.client.messages.published", {
@@ -534,11 +657,16 @@ function getMeterInstruments() {
534
657
  description: "Duration of message processing operations",
535
658
  unit: "ms"
536
659
  });
660
+ cachedLateRpcReplyCounter = meter.createCounter("amqp.client.rpc.late_reply", {
661
+ description: "RPC replies received after the caller stopped waiting (timeout, cancellation, or unknown correlationId)",
662
+ unit: "{message}"
663
+ });
537
664
  return {
538
665
  publishCounter: cachedPublishCounter,
539
666
  consumeCounter: cachedConsumeCounter,
540
667
  publishLatencyHistogram: cachedPublishLatencyHistogram,
541
- consumeLatencyHistogram: cachedConsumeLatencyHistogram
668
+ consumeLatencyHistogram: cachedConsumeLatencyHistogram,
669
+ lateRpcReplyCounter: cachedLateRpcReplyCounter
542
670
  };
543
671
  }
544
672
  /**
@@ -549,7 +677,8 @@ const defaultTelemetryProvider = {
549
677
  getPublishCounter: () => getMeterInstruments().publishCounter,
550
678
  getConsumeCounter: () => getMeterInstruments().consumeCounter,
551
679
  getPublishLatencyHistogram: () => getMeterInstruments().publishLatencyHistogram,
552
- getConsumeLatencyHistogram: () => getMeterInstruments().consumeLatencyHistogram
680
+ getConsumeLatencyHistogram: () => getMeterInstruments().consumeLatencyHistogram,
681
+ getLateRpcReplyCounter: () => getMeterInstruments().lateRpcReplyCounter
553
682
  };
554
683
  /**
555
684
  * Create a span for a publish operation.
@@ -647,6 +776,22 @@ function recordConsumeMetric(provider, queueName, consumerName, success, duratio
647
776
  consumeLatencyHistogram?.record(durationMs, attributes);
648
777
  }
649
778
  /**
779
+ * Record an RPC reply that arrived after the caller stopped waiting.
780
+ *
781
+ * @param reason - Why the reply was orphaned. `"unknown-correlation-id"` is
782
+ * the typical "caller already timed out" case; `"missing-correlation-id"`
783
+ * means the broker delivered a reply with no correlationId at all (a
784
+ * protocol violation by the responder).
785
+ */
786
+ function recordLateRpcReply(provider, reason) {
787
+ const counter = provider.getLateRpcReplyCounter();
788
+ const attributes = {
789
+ [MessagingSemanticConventions.MESSAGING_SYSTEM]: MessagingSemanticConventions.MESSAGING_SYSTEM_RABBITMQ,
790
+ reason
791
+ };
792
+ counter?.add(1, attributes);
793
+ }
794
+ /**
650
795
  * Reset the cached OpenTelemetry API module and instruments.
651
796
  * For testing purposes only.
652
797
  * @internal
@@ -658,8 +803,9 @@ function _resetTelemetryCacheForTesting() {
658
803
  cachedConsumeCounter = void 0;
659
804
  cachedPublishLatencyHistogram = void 0;
660
805
  cachedConsumeLatencyHistogram = void 0;
806
+ cachedLateRpcReplyCounter = void 0;
661
807
  }
662
808
  //#endregion
663
- export { AmqpClient, ConnectionManagerSingleton, MessageValidationError, MessagingSemanticConventions, TechnicalError, _resetTelemetryCacheForTesting, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, recordPublishMetric, setupAmqpTopology, startConsumeSpan, startPublishSpan };
809
+ export { AmqpClient, DEFAULT_CONNECT_TIMEOUT_MS, MessageValidationError, MessagingSemanticConventions, TechnicalError, _getConnectionCountForTesting, _resetConnectionsForTesting, _resetTelemetryCacheForTesting, defaultTelemetryProvider, endSpanError, endSpanSuccess, recordConsumeMetric, recordLateRpcReply, recordPublishMetric, setupAmqpTopology, startConsumeSpan, startPublishSpan };
664
810
 
665
811
  //# sourceMappingURL=index.mjs.map