@droidsolutions-oss/amqp-ts 3.0.4 → 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.
- package/README.md +186 -45
- package/lib/AmqpProperties.js +1 -2
- package/lib/Binding.d.ts +36 -6
- package/lib/Binding.js +244 -75
- package/lib/Binding.js.map +1 -1
- package/lib/Connection/Connection.d.ts +87 -46
- package/lib/Connection/Connection.js +360 -181
- package/lib/Connection/Connection.js.map +1 -1
- package/lib/Connection/ConnectionMetrics.js +1 -2
- package/lib/Connection/RecoveryOptions.d.ts +6 -0
- package/lib/Connection/RecoveryOptions.js +2 -0
- package/lib/Connection/RecoveryOptions.js.map +1 -0
- package/lib/Connection/Topology.d.ts +1 -1
- package/lib/Connection/Topology.js +1 -2
- package/lib/Deferred.d.ts +18 -0
- package/lib/Deferred.js +33 -0
- package/lib/Deferred.js.map +1 -0
- package/lib/Exchange/DeclarationOptions.js +1 -2
- package/lib/Exchange/Exchange.d.ts +38 -17
- package/lib/Exchange/Exchange.js +204 -159
- package/lib/Exchange/Exchange.js.map +1 -1
- package/lib/Exchange/ExchangeType.js +1 -2
- package/lib/Exchange/InitializeResult.js +1 -2
- package/lib/LoggerFactory.js +1 -5
- package/lib/LoggerFactory.js.map +1 -1
- package/lib/Message.d.ts +11 -10
- package/lib/Message.js +66 -37
- package/lib/Message.js.map +1 -1
- package/lib/Queue/DeclarationOptions.js +1 -2
- package/lib/Queue/DeleteResult.js +1 -2
- package/lib/Queue/InitializeResult.js +1 -2
- package/lib/Queue/Queue.d.ts +87 -26
- package/lib/Queue/Queue.js +440 -207
- package/lib/Queue/Queue.js.map +1 -1
- package/lib/Queue/StartConsumerResult.js +1 -2
- package/lib/amqp-ts.d.ts +0 -1
- package/lib/amqp-ts.js +2 -7
- package/lib/amqp-ts.js.map +1 -1
- package/lib/errorMessage.d.ts +1 -0
- package/lib/errorMessage.js +18 -0
- package/lib/errorMessage.js.map +1 -0
- package/lib/index.d.ts +19 -14
- package/lib/index.js +7 -13
- package/lib/index.js.map +1 -1
- package/package.json +38 -60
- package/lib/Connection/ReconnectStrategy.d.ts +0 -4
- package/lib/Connection/ReconnectStrategy.js +0 -3
- 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
|
|
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
|
-
|
|
69
|
+
const connection = new Connection("amqp://localhost");
|
|
61
70
|
|
|
62
71
|
// publish
|
|
63
|
-
|
|
72
|
+
const exchange = await connection.declareExchangeAsync("ExchangeName");
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
const msg = new Message("Test");
|
|
66
75
|
exchange.send(msg);
|
|
67
76
|
|
|
68
77
|
// subscribe/consume
|
|
69
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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", {}, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
+
### Waiting for the topology at startup <a name="startup"></a>
|
|
226
237
|
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
+
## Sending messages <a name="sending"></a>
|
|
232
258
|
|
|
233
|
-
|
|
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
|
-
|
|
261
|
+
## Events <a name="events"></a>
|
|
236
262
|
|
|
237
|
-
|
|
263
|
+
The `Connection` is an `EventEmitter`. It emits these events:
|
|
238
264
|
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
+
Notes:
|
|
244
282
|
|
|
245
|
-
It
|
|
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
|
|
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
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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<
|
|
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).
|
package/lib/AmqpProperties.js
CHANGED
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
|
-
|
|
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
|
}
|