@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 +168 -18
- package/dist/index.d.cts +57 -78
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +57 -78
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +165 -19
- package/dist/index.mjs.map +1 -1
- package/docs/index.md +120 -157
- package/package.json +4 -4
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
248
|
+
* @internal
|
|
249
|
+
*/
|
|
250
|
+
declare function _getConnectionCountForTesting(): number;
|
|
251
|
+
/**
|
|
252
|
+
* Close every pooled connection and clear ref-counts. Test-only helper.
|
|
220
253
|
*
|
|
221
|
-
* @
|
|
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
|
|
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,
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
})))).
|
|
197
|
-
|
|
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
|
|
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
|
-
}))).
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
}))).
|
|
230
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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)).
|
|
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
|
-
|
|
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,
|
|
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
|