@droidsolutions-oss/amqp-ts 3.0.3 → 4.0.0-develop.1

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.
Files changed (48) hide show
  1. package/README.md +186 -45
  2. package/lib/AmqpProperties.js +1 -2
  3. package/lib/Binding.d.ts +36 -6
  4. package/lib/Binding.js +244 -75
  5. package/lib/Binding.js.map +1 -1
  6. package/lib/Connection/Connection.d.ts +87 -46
  7. package/lib/Connection/Connection.js +360 -181
  8. package/lib/Connection/Connection.js.map +1 -1
  9. package/lib/Connection/ConnectionMetrics.js +1 -2
  10. package/lib/Connection/RecoveryOptions.d.ts +6 -0
  11. package/lib/Connection/RecoveryOptions.js +2 -0
  12. package/lib/Connection/RecoveryOptions.js.map +1 -0
  13. package/lib/Connection/Topology.d.ts +1 -1
  14. package/lib/Connection/Topology.js +1 -2
  15. package/lib/Deferred.d.ts +18 -0
  16. package/lib/Deferred.js +33 -0
  17. package/lib/Deferred.js.map +1 -0
  18. package/lib/Exchange/DeclarationOptions.js +1 -2
  19. package/lib/Exchange/Exchange.d.ts +38 -17
  20. package/lib/Exchange/Exchange.js +204 -159
  21. package/lib/Exchange/Exchange.js.map +1 -1
  22. package/lib/Exchange/ExchangeType.js +1 -2
  23. package/lib/Exchange/InitializeResult.js +1 -2
  24. package/lib/LoggerFactory.js +1 -5
  25. package/lib/LoggerFactory.js.map +1 -1
  26. package/lib/Message.d.ts +11 -10
  27. package/lib/Message.js +66 -37
  28. package/lib/Message.js.map +1 -1
  29. package/lib/Queue/DeclarationOptions.js +1 -2
  30. package/lib/Queue/DeleteResult.js +1 -2
  31. package/lib/Queue/InitializeResult.js +1 -2
  32. package/lib/Queue/Queue.d.ts +87 -26
  33. package/lib/Queue/Queue.js +440 -207
  34. package/lib/Queue/Queue.js.map +1 -1
  35. package/lib/Queue/StartConsumerResult.js +1 -2
  36. package/lib/amqp-ts.d.ts +0 -1
  37. package/lib/amqp-ts.js +2 -7
  38. package/lib/amqp-ts.js.map +1 -1
  39. package/lib/errorMessage.d.ts +1 -0
  40. package/lib/errorMessage.js +18 -0
  41. package/lib/errorMessage.js.map +1 -0
  42. package/lib/index.d.ts +19 -14
  43. package/lib/index.js +7 -13
  44. package/lib/index.js.map +1 -1
  45. package/package.json +38 -60
  46. package/lib/Connection/ReconnectStrategy.d.ts +0 -4
  47. package/lib/Connection/ReconnectStrategy.js +0 -3
  48. package/lib/Connection/ReconnectStrategy.js.map +0 -1
package/README.md CHANGED
@@ -10,6 +10,7 @@ This is a fork of [amqp-ts](https://github.com/abreits/amqp-ts).
10
10
 
11
11
  - [Overview](#overview)
12
12
  - [Usage](#usage)
13
+ - [Migrating from 3.x](#migrating)
13
14
 
14
15
  # Overview <a name="overview"></a>
15
16
 
@@ -20,7 +21,7 @@ Amqp-ts is a library for nodejs that simplifies communication with AMQP message
20
21
  Changes to the [original amqp-ts](https://github.com/abreits/amqp-ts) package:
21
22
 
22
23
  - using up-to-date [amqplib](http://www.squaremobius.net/amqp.node/) (unfortunately the original package uses a four year old version that only support outdated NodeJS version)
23
- - dropped support for outdated NodeJS versions (Compile target in tsconfig is ES2019 which is fully supported by Node 12).
24
+ - dropped support for outdated NodeJS versions, version 4 needs Node.js 22.13 or newer and is published as an ES module.
24
25
  - dropped winston as dependency, instead a factory can be provided to the connection constructor which returns any logger.
25
26
  - the deprecated methods like `Exchange.publish`, `Exchange.startConsumer`, `Queue.publish` and `Queue.startConsumer` are removed from this library, since their replacements like `Exchange.send` and `Exchange.activateConsumer` already existed.
26
27
  - fixes for https://github.com/abreits/amqp-ts/pull/46 and https://github.com/abreits/amqp-ts/issues/20 have been implemented
@@ -30,7 +31,7 @@ Changes to the [original amqp-ts](https://github.com/abreits/amqp-ts) package:
30
31
 
31
32
  - [High level non opinioned library], no need to worry about channels etc.
32
33
  - ['Lazy' initialization](#initialization), async AMQP dependencies are resolved automatically
33
- - [Automatic reconnection](#reconnect), when the connection with the AMQP server fails, the whole connection and configuration is rebuilt automatically
34
+ - [Automatic reconnection](#reconnect), when the connection with the AMQP server fails, the whole connection and configuration is rebuilt automatically (using the connection recovery of amqplib 2)
34
35
  - Written in typescript.
35
36
 
36
37
  ## Current status
@@ -46,27 +47,35 @@ It does depend on the following npm libraries:
46
47
  No need to nest functionality, just create a connection, declare your exchanges, queues and
47
48
  bindings and send and receive messages. The library takes care of any direct dependencies.
48
49
 
49
- If you define an exchange and a queue and bind the queue to the exchange and want to make sure that the queue is connected to the exchange before you send a message to the exchange you can call the `connection.completeConfiguration()` method and act on the promise it returns.
50
+ If you define an exchange and a queue and bind the queue to the exchange and want to make sure that the queue is connected to the exchange before you send a message to the exchange you can call the `connection.completeConfiguration()` method and act on the promise it returns. It is also the way to gate the start of your application on the topology, see [Waiting for the topology at startup](#startup).
50
51
 
51
52
  # Usage <a name="usage"></a>
52
53
 
54
+ Install the package (it needs Node.js `>=22.13.0` and brings its own typings, no `@types/amqplib` required):
55
+
56
+ ```sh
57
+ npm install @droidsolutions-oss/amqp-ts
58
+ ```
59
+
60
+ The package is an ES module. All examples below use `import` and top-level `await`, so run them as ES modules (`.mjs` files, or `"type": "module"` in your `package.json`). Everything is exported from the package root; see [Migrating from 3.x](#migrating) if you use CommonJS.
61
+
53
62
  There are multiple ways to declare your exchanges, queues and bindings. Each of those has an `initialized` property which is a promise that resolves when initialization is complete. You can either await it yourself or call `completeConfiguration` on the connection like explained in [Lazy initialization](#initialization).
54
63
 
55
64
  ## Async Typescript Example
56
65
 
57
66
  ```ts
58
- import { Connection, Message } from "amqp-ts";
67
+ import { Connection, Message } from "@droidsolutions-oss/amqp-ts";
59
68
 
60
- var connection = new Connection("amqp://localhost");
69
+ const connection = new Connection("amqp://localhost");
61
70
 
62
71
  // publish
63
- var exchange = await connection.declareExchangeAsync("ExchangeName");
72
+ const exchange = await connection.declareExchangeAsync("ExchangeName");
64
73
 
65
- var msg = new Message("Test");
74
+ const msg = new Message("Test");
66
75
  exchange.send(msg);
67
76
 
68
77
  // subscribe/consume
69
- var queue = await connection.declareQueueAsync("QueueName");
78
+ const queue = await connection.declareQueueAsync("QueueName");
70
79
  await queue.bind(exchange);
71
80
  await queue.activateConsumer((message: Message) => {
72
81
  console.log("Message received: " + message.getContent());
@@ -76,11 +85,11 @@ await queue.activateConsumer((message: Message) => {
76
85
  ## Lazy Typescript Example
77
86
 
78
87
  ```ts
79
- import { Connection, Message } from "amqp-ts";
88
+ import { Connection, Message } from "@droidsolutions-oss/amqp-ts";
80
89
 
81
- var connection = new Connection("amqp://localhost");
82
- var exchange = connection.declareExchange("ExchangeName");
83
- var queue = connection.declareQueue("QueueName");
90
+ const connection = new Connection("amqp://localhost");
91
+ const exchange = connection.declareExchange("ExchangeName");
92
+ const queue = connection.declareQueue("QueueName");
84
93
  void queue.bind(exchange);
85
94
  void queue.activateConsumer((message: Message) => {
86
95
  console.log("Message received: " + message.getContent());
@@ -88,18 +97,18 @@ void queue.activateConsumer((message: Message) => {
88
97
 
89
98
  // it is possible that the following message is not received because
90
99
  // it can be sent before the queue, binding or consumer exist
91
- var msg = new Message("Test");
100
+ const msg = new Message("Test");
92
101
  exchange.send(msg);
93
102
 
94
103
  // This waits until all exchanges/queues/bindings are initialized
95
104
  await connection.completeConfiguration();
96
105
  // the following message will be received because
97
106
  // everything you defined earlier for this connection now exists
98
- var msg2 = new Message("Test2");
107
+ const msg2 = new Message("Test2");
99
108
  exchange.send(msg2);
100
109
  ```
101
110
 
102
- More examples can be found in the [tutorials directory](https://github.com/abreits/amqp-ts/tree/master/tutorials).
111
+ More examples can be found in the [tutorials directory](https://github.com/droidsolutions/amqp-ts/tree/master/tutorials).
103
112
 
104
113
  ## Messages
105
114
 
@@ -116,7 +125,7 @@ Properties that are set by this library are:
116
125
 
117
126
  The last argument of the `Connection` constructor takes a logger factory function. If you specify it, the function should return a logger that matches the `SimpleLogger` interface. This type is heavily inspired by the style of [Pino](https://github.com/pinojs/pino), but you can write a wrapper for any logger you like. It should support `%s`, `%d` and `%o` string interpolation, as they are used for internal log messages.
118
127
 
119
- If you do not specify a logger factory, no logs are emitted.
128
+ If you do not specify a logger factory, no logs are emitted. This includes errors thrown by your own event listeners, which are caught and logged at error level (see [Events](#events)), and messages that have to be dropped (see [Sending messages](#sending)). So pass a logger factory in production, otherwise these problems are invisible.
120
129
 
121
130
  Example usage with Pino:
122
131
 
@@ -143,7 +152,7 @@ loggerFactory = (context, meta) => {
143
152
  return logger.child(context, meta);
144
153
  };
145
154
 
146
- const connection = new Connection("amqp://localhost", {}, { retries: 0, interval: 1500 }, loggerFactory);
155
+ const connection = new Connection("amqp://localhost", {}, {}, loggerFactory);
147
156
  ```
148
157
 
149
158
  The above example would result in log messages like these:
@@ -218,63 +227,131 @@ const loggerFactory = (context, meta) => {
218
227
 
219
228
  To know the status of the connection: `connection.isConnected`. Returns true if the connection exists and false, otherwise.
220
229
 
221
- ## Events
230
+ `connection.getConnection` returns the current [amqplib](https://amqp-node.github.io/amqplib/) `ChannelModel`, or `undefined` while the connection is down (before the first connect, during a reconnect and after `close()`). It is replaced on every reconnect, so do not keep a reference to it. Messaging should go through the exchanges and queues of this library, which survive reconnects.
231
+
232
+ `connection.initialized` is a promise that resolves once the connection is established and the declared topology was set up. While the connection is down it is replaced by a pending promise that resolves after the next successful reconnect. It rejects when `close()` is called or recovery gives up before that. The same is true for the `initialized` promises of exchanges, queues, bindings and the consumer of a queue (`activateConsumer`): they reject on `close()` and when recovery gives up, and reading `initialized` of an entity after `close()` returns a rejected promise.
222
233
 
223
- #on('open_connection', function() {...})
234
+ `connection.initialized` only reflects the connection. An exchange, queue, binding or consumer that fails on its own (for example a `noCreate` queue that does not exist, or a consumer the broker refuses) rejects its own promise and is reported through [`error_connection`](#events), but it does not keep `connection.initialized` from resolving, so one broken entity cannot take the whole connection down.
224
235
 
225
- It's emitted when a connection is concretized and can publish/subscribe in Rabbit Bus.
236
+ ### Waiting for the topology at startup <a name="startup"></a>
226
237
 
227
- #on('close_connection', function() {...})
238
+ To start only once everything you declared works, await `connection.completeConfiguration()`. It waits until every declared exchange, queue and binding is initialized and every activated consumer is registered, and it rejects when one of them failed, at the first connect or later. It rejects as long as the failed entity is not declared again (a refused consumer: until the next reconnect registers it), so asking again does not hide a failure.
228
239
 
229
- It's emitted when a connection is closed, after calling the close method.
240
+ ```ts
241
+ import { Connection, Message } from "@droidsolutions-oss/amqp-ts";
242
+
243
+ const connection = new Connection("amqp://localhost");
244
+ const queue = connection.declareQueue("orders", { durable: true });
245
+ void queue.bind(connection.declareExchange("events", "topic"), "order.*");
246
+ void queue.activateConsumer((message: Message) => console.log("order", message.getContent()));
247
+
248
+ try {
249
+ await connection.completeConfiguration();
250
+ } catch (err) {
251
+ console.error("the topology could not be set up", err);
252
+ await connection.close();
253
+ process.exit(1);
254
+ }
255
+ ```
230
256
 
231
- #on('lost_connection', function() {...})
257
+ ## Sending messages <a name="sending"></a>
232
258
 
233
- It is emitted when the connection is lost and before attempting to re-establish the connection.
259
+ `exchange.send(message, routingKey)` and `queue.send(message)` return `void`. A message sent before the connection or the exchange/queue is ready is buffered and sent as soon as it is. The same goes for a message sent while the connection or the channel is lost or closing, for example while amqplib closes the connection after a protocol error: it is sent once the exchange/queue is initialized again. A message that can no longer be sent, because recovery gave up, `close()` was called or the exchange/queue was closed or deleted, is logged at warn level and dropped. (In 3.x, sending to a closed entity threw a `TypeError`.) If you need to know that a message left, await `connection.completeConfiguration()` or the `initialized` promise of the target before sending.
234
260
 
235
- #on('trying_connect', function() {...})
261
+ ## Events <a name="events"></a>
236
262
 
237
- It is emitted during the time that try re-establish the connection.
263
+ The `Connection` is an `EventEmitter`. It emits these events:
238
264
 
239
- #on('re_established_connection', function() {...})
265
+ | Event | Emitted when |
266
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
267
+ | `open_connection` | The first connection is established and the declared topology (exchanges, queues, bindings, consumers) was set up. An entity that failed on its own does not prevent it. |
268
+ | `re_established_connection` | A later connection, after `lost_connection`, is established and the topology is restored. |
269
+ | `lost_connection` | The connection was lost unexpectedly (not after `close()`), before reconnecting is attempted. |
270
+ | `trying_connect` | A (re)connect attempt was scheduled, once per attempt. |
271
+ | `close_connection` | The connection was closed after you called `close()`. |
272
+ | `error_connection(err)` | Recovery gave up (see `maxRetries`), or a single exchange, queue, binding or consumer failed on its own, at its first declaration or again during recovery. `err` is the cause. |
240
273
 
241
- It is emitted when the connection is re-established.
274
+ ```ts
275
+ connection.on("open_connection", () => console.log("connected"));
276
+ connection.on("lost_connection", () => console.log("connection lost, reconnecting"));
277
+ connection.on("re_established_connection", () => console.log("connected again"));
278
+ connection.on("error_connection", (err) => console.error("connection problem", err));
279
+ ```
242
280
 
243
- #on('error_connection', function(err) {...})
281
+ Notes:
244
282
 
245
- It's emitted when a error is registered during the connection.
283
+ - `error_connection` is not by itself a "the connection is dead" signal. It also fires when only one entity failed, although the connection itself is fine: for example a `noCreate` exchange or queue that does not exist, a queue whose declaration conflicts with an existing one, a binding the broker refuses, a consumer the broker refuses, or one it cancels again right after it was registered again. That includes the first declaration, also of entities nobody awaits, and the declarations repeated during recovery. After recovery gave up (finite `maxRetries`) it fires with the give-up error and no more reconnects happen.
284
+ - To find out at startup whether the topology works, await [`completeConfiguration()`](#startup) instead of listening for this event.
285
+ - Exceptions thrown by your listeners are caught and logged at error level instead of propagating, and the listeners after the failing one still run. With the default (empty) logger such errors are invisible, so pass a [logger factory](#logging).
246
286
 
247
287
  ## Automatic Reconnection <a name="reconnect"></a>
248
288
 
249
- When the library detects that the connection with the AMQP server is lost, it tries to automatically reconnect to the server.
289
+ When the library detects that the connection with the AMQP server is lost, it reconnects automatically and rebuilds the whole topology (exchanges, queues, bindings and consumers). This uses the connection recovery of amqplib 2. The third argument of the `Connection` constructor takes the `RecoveryOptions`:
290
+
291
+ | Option | Default | Meaning |
292
+ | ------------------- | ---------- | --------------------------------------------------------------------------------------------------------- |
293
+ | `initialDelay` | `100` | Delay before the first retry, in milliseconds. |
294
+ | `maxDelay` | `30000` | Upper bound of the delay, in milliseconds. |
295
+ | `factor` | `2` | The delay is multiplied by this factor after every attempt (exponential backoff). |
296
+ | `jitter` | `0.2` | Fraction (0 to 1) by which each delay randomly varies up or down, so not all clients reconnect at once. The delay never exceeds `maxDelay`. |
297
+ | `maxRetries` | `Infinity` | Number of reconnect attempts before giving up. Also bounds the first connect unless `initialMaxRetries` is set. |
298
+ | `initialMaxRetries` | `maxRetries` | Number of attempts before the first successful connect only. Afterwards `maxRetries` applies. |
299
+ | `calculateDelay` | - | `(attempt: number) => number`, your own delay in milliseconds (attempts start at 1) instead of the backoff. |
300
+
301
+ By default it retries forever. Example that fails fast when the broker is unreachable at startup, but keeps retrying once it was connected:
302
+
303
+ ```ts
304
+ import { Connection, type RecoveryOptions } from "@droidsolutions-oss/amqp-ts";
305
+
306
+ const recovery: RecoveryOptions = { initialMaxRetries: 3, initialDelay: 500, maxDelay: 10000 };
307
+ const connection = new Connection("amqp://localhost", {}, recovery);
308
+ connection.on("error_connection", (err) => {
309
+ // fired at startup after 3 failed attempts, or when a single exchange, queue, binding or consumer fails
310
+ console.error("connection problem", err);
311
+ });
312
+
313
+ await connection.initialized; // rejects if the broker is still unreachable after 3 retries
314
+ ```
315
+
316
+ `RecoveryOptions` is amqplib's type without `setup` and `waitForConnect`, which amqp-ts uses itself.
317
+
318
+ ### What happens on connection loss
319
+
320
+ - On `lost_connection` the connection and all exchanges, queues, bindings and consumers become "not initialized" and their `initialized` promises are replaced by pending ones. Messages you send meanwhile are buffered.
321
+ - After each reconnect the topology is declared again in dependency order (exchanges, queues, bindings, consumers) before `re_established_connection` (or `open_connection` for the first connect) is emitted and the pending promises resolve. A binding to an exchange of another `Connection` is the exception: it waits for that connection to have the exchange initialized, so it may be created later.
322
+ - If recovery gives up (finite `maxRetries`), `error_connection` fires with the give-up error, every pending `initialized` rejects, and buffered messages are dropped with a warn log.
323
+ - Without a reconnect: when the broker closes the channel of a single exchange or queue (for example error 406 on a double ack, or 404 when publishing to a deleted exchange), only that entity gets a new channel, including its bindings and consumer. A consumer cancelled by the broker (for example because its queue was deleted) is registered again. If the broker cancels it again within a second of that, it stays cancelled until the next reconnect and `error_connection` is emitted.
324
+ - A consumer the broker refuses (for example 403 because another connection holds an exclusive consumer on the queue) is not retried on the same connection. It is registered again on the next reconnect, or when you call `stopConsumer()` and then `activateConsumer()` again; until then its promise and `completeConfiguration()` reject.
250
325
 
251
326
  ## Automatic JSON content
252
327
 
253
328
  If your message content is not a string and not a buffer it is converted automatically to a JSON string. The `contentType` is set to `application/json` and the `contentEncoding` is set to `utf-8`. Vice versa if you call `getContent()` on the message and the `contentType` is set to `application/json` the string will be parsed and the parsed result will be returned. If you want to avoid the `any` type you can use `getJsonContent<T>()` instead. This method throws an error, if the `contentType` is not set to `application/json`.
254
329
 
255
- ## Automatic ReplyTo
330
+ ## Automatic ReplyTo and RPC <a name="rpc"></a>
256
331
 
257
- If your message consumer returns a value and the `replyTo` property is set in the message property, the value is send to the queue given in the `replyTo` field. The correlationId is set to the response message if it is set in the request. If you want to set other properties you can specify them when calling `activateConsumer` i nthe options. If you don't return any value (a.k.a return `undefined`) no automatic reply is send. You have manage this by yourself then.
332
+ If your message consumer returns a value and the `replyTo` property is set in the message property, the value is sent to the queue given in the `replyTo` field. The correlationId is set to the response message if it is set in the request. If you want to set other properties you can specify them when calling `activateConsumer` in the options. If you don't return any value (a.k.a return `undefined`) no automatic reply is sent. You have to manage this yourself then.
333
+
334
+ This is how you implement request/response (RPC) with this library: the requester sends a message with `replyTo` (and a `correlationId` to match the answer) and consumes its reply queue, the consumer simply returns the result.
258
335
 
259
336
  Examples:
260
337
 
261
338
  Provider/Publisher side:
262
339
 
263
340
  ```ts
264
- import { Connection, Message } from "amqp-ts";
341
+ import { Connection, Message } from "@droidsolutions-oss/amqp-ts";
265
342
 
266
- var connection = new Connection("amqp://localhost");
267
- var exchange = await connection.declareExchangeAsync("ExchangeName");
268
- var queue = await connection.declareQueueAsync("rpc_reply");
343
+ const connection = new Connection("amqp://localhost");
344
+ const exchange = await connection.declareExchangeAsync("ExchangeName");
345
+ const queue = await connection.declareQueueAsync("rpc_reply");
269
346
  await queue.activateConsumer((message: Message) => {
270
347
  console.log("RPC reply Message received: " + message.properties.correlationId); // should log the ID you set in the message below
271
348
 
272
- const content = message.getJsonContent<MyResultDto>();
349
+ const content = message.getJsonContent<{ my: string }>();
273
350
  console.log("Result is ", content.my); // "Result is result"
274
351
  });
275
352
 
276
353
  // sending the message
277
- var msg = new Message(
354
+ const msg = new Message(
278
355
  { some: "value" },
279
356
  {
280
357
  correlationId: "jC7oTFEeWIfmn_c8Z5Aa",
@@ -285,19 +362,21 @@ var msg = new Message(
285
362
  exchange.send(msg, "rpc_queue");
286
363
  ```
287
364
 
365
+ The consumer of the reply queue is a normal consumer and is registered again after a reconnect. Replies that arrive while it is down stay in the queue.
366
+
288
367
  Consumer/Subscriber side:
289
368
 
290
369
  ```ts
291
- import { Connection, Message } from "amqp-ts";
370
+ import { Connection, Message } from "@droidsolutions-oss/amqp-ts";
292
371
 
293
- var connection = new Connection("amqp://localhost");
294
- var exchange = await connection.declareExchangeAsync("ExchangeName");
295
- var queue = await connection.declareQueueAsync("rpc_queue");
372
+ const connection = new Connection("amqp://localhost");
373
+ const exchange = await connection.declareExchangeAsync("ExchangeName");
374
+ const queue = await connection.declareQueueAsync("rpc_queue");
296
375
  await queue.bind(exchange);
297
376
  await queue.activateConsumer((message: Message) => {
298
377
  console.log(`Message from ${message.properties.appId} received: ${message.properties.correlationId}`); // "Message from app:my-app component:rpc-client received: jC7oTFEeWIfmn_c8Z5Aa
299
378
 
300
- const content = message.getJsonContent<MyDto>();
379
+ const content = message.getJsonContent<{ some: string }>();
301
380
  console.log("Content is ", content.some); // "Content is value"
302
381
  // process message content
303
382
 
@@ -309,4 +388,66 @@ await queue.activateConsumer((message: Message) => {
309
388
 
310
389
  The connection instance offers a method that returns basic metrics of the connection. For the metrics there is a TypeScript interface `ConnectionMetrics` that contains possible metrics of the connection.
311
390
 
312
- To receive them you can call `getMetrics()` on the connection instance.
391
+ To receive them you can call `getMetrics()` on the connection instance.
392
+
393
+ ## Migrating from 3.x <a name="migrating"></a>
394
+
395
+ Version 4.0 is a major release. This section lists everything you have to change, and the behaviour changes you may notice.
396
+
397
+ ### Breaking changes
398
+
399
+ - **ESM only.** The package is an ES module. In ESM code import from the package root as before (`import { Connection } from "@droidsolutions-oss/amqp-ts"`). In CommonJS code you can still load it with `require()` on Node `>=22.13.0` (which can `require()` ES modules without a flag):
400
+
401
+ ```js
402
+ const { Connection, Message } = require("@droidsolutions-oss/amqp-ts");
403
+ ```
404
+
405
+ In TypeScript, set `"module": "node20"` or `"nodenext"` (with a matching `moduleResolution`) so that `require` of an ES module is understood. `module: "commonjs"` will not resolve the new package.
406
+ - **Node.js `>=22.13.0`.** Older versions are not supported (the `engines` field says so).
407
+ - **No deep imports.** Only the package root is exported. `import { Message } from "@droidsolutions-oss/amqp-ts/lib/Message"` (and every other `lib/...` path) fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`. All public classes and types are exported from the root: `Connection`, `Exchange`, `Queue`, `Binding`, `Message`, `ApplicationName` and the types `AmqpProperties`, `ConnectionMetrics`, `RecoveryOptions`, `Topology`, `LoggerFactory`, `SimpleLogger`, `ExchangeType`, `ExchangeDeclarationOptions`, `QueueDeclarationOptions`, `ExchangeInitializeResult`, `QueueInitializeResult`, `DeleteResult`, `StartConsumerResult`.
408
+ - **`RecoveryOptions` replace `ReconnectStrategy`.** The third `Connection` argument is now amqplib's [`RecoveryOptions`](#reconnect). The fixed interval is replaced by exponential backoff with jitter.
409
+
410
+ ```ts
411
+ // 3.x
412
+ new Connection(url, {}, { retries: 0, interval: 1500 }); // retry forever, every 1.5 s
413
+ new Connection(url, {}, { retries: 5, interval: 1500 }); // give up after 5 attempts
414
+
415
+ // 4.x
416
+ new Connection(url); // the default retries forever (maxRetries: Infinity) with backoff
417
+ new Connection(url, {}, { maxRetries: 5 }); // give up after 5 attempts
418
+ new Connection(url, {}, { initialDelay: 1500, maxDelay: 1500, factor: 1, jitter: 0 }); // fixed delay, if you really want it
419
+ ```
420
+
421
+ `{ retries: 0 }` (forever) becomes the default, `{ retries: n }` becomes `{ maxRetries: n }`, and the `interval` becomes `initialDelay`/`maxDelay`/`factor`/`jitter`. New is `initialMaxRetries`, which lets startup fail fast while later recovery retries forever. The `ReconnectStrategy` type is gone.
422
+ - **`heartbeat=0` disables heartbeats.** amqplib reads the heartbeat from the query of the connection URL (`amqp://localhost?heartbeat=0`), not from the socket options. amqplib 2 treats `heartbeat=0` as "no heartbeats" instead of "use the server's value". If your URL contains `heartbeat=0` to get the server's value, remove the parameter instead.
423
+ - **`Exchange.rpc` and `Queue.rpc` are removed.** `rpc` only resolved the first call with its reply and lost its reply consumer after a reconnect. Send a message with `replyTo` and `correlationId` and consume the reply queue instead; the consumer's return value is sent back automatically. See [Automatic ReplyTo and RPC](#rpc):
424
+
425
+ ```ts
426
+ // 3.x
427
+ const result = await queue.rpc(21);
428
+ console.log(result.getContent());
429
+
430
+ // 4.x
431
+ const replies = await connection.declareQueueAsync("my_replies", { exclusive: true });
432
+ await replies.activateConsumer((reply) => console.log(reply.getContent()), { noAck: true });
433
+ queue.send(new Message(21, { replyTo: "my_replies", correlationId: "some-unique-id" }));
434
+ ```
435
+
436
+ Match replies to requests by `correlationId` if you have more than one call in flight.
437
+ - **`getConnection` and `connection`.** `connection.getConnection` now returns the current amqplib `ChannelModel` (or `undefined` while disconnected) instead of the amqplib `Connection`, and the public `connection` field is gone. Use `getConnection` and check for `undefined`.
438
+ - **`tryToConnect` is removed.** The connection is established and re-established automatically, there is nothing to call. Use the [events](#events) and `connection.initialized` to react to its state.
439
+ - **Typings.** `@types/amqplib` is no longer needed (amqplib ships its own types), remove it from your dependencies. Underscore-prefixed members (`_channel`, `_exchanges`, `_queues`, ...) are internal and no longer in the typings.
440
+
441
+ ### Behaviour changes
442
+
443
+ - **`error_connection` is broader.** Besides "recovery gave up" (finite `maxRetries`, with the give-up error) it also fires whenever a single exchange, queue or binding cannot be declared, or a consumer cannot be registered, whether at its first declaration or again during recovery. The connection itself is fine then, so do not treat the event as "connection dead".
444
+ - **Gate startup with `completeConfiguration()`.** `connection.initialized` and `open_connection` only reflect the connection: an entity that fails on its own does not reject or delay them. `connection.completeConfiguration()` rejects while a declared exchange, queue or binding failed or an activated consumer was refused, also when you ask after the failure. See [Waiting for the topology at startup](#startup).
445
+ - **`deleteConfiguration()` resolves with `undefined`.** It deletes the bindings, then the queues, then the exchanges, one phase after the other, and rejects with the first failure. In 3.x it ran everything at once and resolved with the array of results (`Promise.all`).
446
+ - **`unbind` of an unknown binding resolves.** `queue.unbind()` and `exchange.unbind()` resolve when there is no such binding (for example when unbinding twice). In 3.x they threw a `TypeError`. Deleting a binding that is still being created again rejects its pending `initialized`.
447
+ - **Pending `initialized` promises reject.** Every pending `initialized` (connection, exchanges, queues, bindings and consumers) rejects when you call `close()` and when recovery gives up. Reading the `initialized` of an entity after `close()` returns a rejected promise. Add error handling where you await them without one.
448
+ - **Unsendable messages are dropped.** `send()` still returns `void`. Messages that can no longer be sent (recovery gave up, `close()` was called, the exchange or queue was closed or deleted) are logged at warn level and dropped. In 3.x a send to a closed entity threw a `TypeError`.
449
+ - **Listener exceptions are logged.** Exceptions thrown by your event listeners are caught and logged at error level instead of propagating. With the default (empty) logger they are invisible, so pass a `loggerFactory`.
450
+ - **Bindings wait for their source exchange.** A binding is created once its source exchange is initialized through this library, and fails if that exchange cannot be declared. In 3.x a queue binding was created as soon as the queue was ready, whenever the exchange happened to exist on the broker. Declare the source exchange on the same connection (`connection.declareExchange(...)`) before binding to it.
451
+ - **`activateConsumer` rejects if the queue declaration fails.** The returned promise rejects with the declaration error.
452
+ - **Reconnect timing.** The default is exponential backoff with jitter (100 ms initial delay, 30 s maximum, factor 2, jitter 0.2) retrying forever, instead of a fixed 1.5 s interval.
453
+ - **Broker-side channel and consumer failures are repaired.** A channel the broker closes (for example on a double ack) is re-created for that exchange or queue only, and a consumer cancelled by the broker is registered again, see [What happens on connection loss](#reconnect).
@@ -1,3 +1,2 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
3
2
  //# sourceMappingURL=AmqpProperties.js.map
package/lib/Binding.d.ts CHANGED
@@ -1,18 +1,48 @@
1
- import { Exchange } from "./Exchange/Exchange";
2
- import { Queue } from "./Queue/Queue";
1
+ import { Exchange } from "./Exchange/Exchange.js";
2
+ import { Queue } from "./Queue/Queue.js";
3
3
  export declare class Binding {
4
4
  private pattern;
5
5
  private args;
6
6
  static id(destination: Exchange | Queue, source: Exchange, pattern?: string): string;
7
7
  static removeBindingsContaining(connectionPoint: Exchange | Queue): Promise<any>;
8
+ /**
9
+ * A promise that resolves once the binding is created. It is replaced by a pending promise while the binding is
10
+ * created again after a lost connection. It rejects when the binding or one of its ends fails on its own, or when
11
+ * the connection is closed or recovery gives up while it is pending.
12
+ */
8
13
  initialized: Promise<Binding>;
9
- _source: Exchange;
10
- _destination: Exchange | Queue;
11
14
  private log;
15
+ private _deferred;
16
+ /** The generation of the last initialization, and that initialization. */
17
+ private _generation;
18
+ private _initializing;
19
+ /** Changes with every initialization, so a superseded one can tell. */
20
+ private _attempt;
21
+ /** The creation of the binding on the broker that is in flight, if any. Never rejects. */
22
+ private _creating;
23
+ /** Set once {@link delete} was called, after which nothing creates the binding again. */
24
+ private _deleting;
12
25
  constructor(destination: Exchange | Queue, source: Exchange, pattern?: string, args?: any);
13
- _initialize(): void;
26
+ /** Whether this binding is still declared, i.e. neither deleted, replaced nor removed together with a failed end. */
27
+ private get _isDeclared();
28
+ /**
29
+ * Creates the binding on a short-lived channel of the given model. The creation is tracked, so {@link delete} can
30
+ * wait for it instead of unbinding before it.
31
+ */
32
+ private _create;
33
+ /**
34
+ * Runs a bind or unbind on a short-lived channel of the given model. A failing bind or unbind closes the channel it
35
+ * runs on, so it must not be the destination's channel, which would then re-initialize, nor one shared with other
36
+ * bindings, which would then fail as well.
37
+ */
38
+ private _onOwnChannel;
39
+ private _bind;
14
40
  /**
15
- * Deletes this binding of either a queue or an exchange.
41
+ * Deletes this binding of either a queue or an exchange. It is no longer declared from then on, so nothing creates it
42
+ * again, and an {@link initialized} that is still pending rejects. The unbind runs once the destination is
43
+ * initialized, on a short-lived channel. Deleting it again returns the same promise, also after a failure. A binding
44
+ * that was already replaced by binding the same pair again is only settled: the broker binding is the newer one's.
16
45
  */
17
46
  delete(): Promise<void>;
47
+ private _delete;
18
48
  }