comq 0.4.1-dev.3 → 0.5.0-dev.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/.github/labels.yaml +15 -0
- package/.github/workflows/labels.yaml +18 -0
- package/.github/workflows/release.yaml +20 -8
- package/docker-compose.yaml +20 -2
- package/docs/notes.md +14 -8
- package/examples/const.js +1 -3
- package/examples/events/consumer.js +5 -6
- package/examples/events/producer.js +3 -3
- package/examples/package.json +3 -0
- package/examples/rpc/consumer.js +3 -3
- package/examples/rpc/producer.js +3 -3
- package/features/connection.feature +3 -0
- package/features/events.feature +1 -1
- package/features/shards.feature +61 -0
- package/features/steps/.test/backpressure.test.js +2 -2
- package/features/steps/.test/broker.test.js +32 -6
- package/features/steps/.test/connection.test.js +105 -1
- package/features/steps/.test/context.mock.js +2 -2
- package/features/steps/.test/context.test.js +13 -5
- package/features/steps/.test/events.test.js +66 -3
- package/features/steps/.test/rpc.test.js +99 -1
- package/features/steps/.types/context.d.ts +11 -1
- package/features/steps/backpressure.js +1 -1
- package/features/steps/broker.js +26 -9
- package/features/steps/connection.js +69 -2
- package/features/steps/context.js +28 -12
- package/features/steps/events.js +41 -2
- package/features/steps/rpc.js +58 -1
- package/lerna.json +1 -1
- package/package.json +5 -5
- package/readme.md +75 -25
- package/source/channel.js +56 -32
- package/source/connect.js +28 -2
- package/source/connection.js +24 -17
- package/source/decode.js +1 -1
- package/source/events.js +7 -0
- package/source/io.js +21 -14
- package/source/shards/channel.js +219 -0
- package/source/shards/connection.js +56 -0
- package/source/shards/index.js +5 -0
- package/test/channel.test.js +240 -145
- package/test/connect.test.js +39 -11
- package/test/connection.mock.js +17 -13
- package/test/connection.test.js +65 -12
- package/test/decode.test.js +5 -5
- package/test/io.close.test.js +2 -2
- package/test/io.consume.test.js +1 -1
- package/test/io.reply.test.js +7 -7
- package/test/io.request.test.js +27 -1
- package/test/presets.test.js +1 -1
- package/test/shards/channel.test.js +367 -0
- package/test/shards/connection.test.js +106 -0
- package/types/amqp.d.ts +3 -1
- package/types/channel.d.ts +8 -5
- package/types/connection.d.ts +3 -1
- package/types/diagnostic.d.ts +1 -1
- package/types/topology.d.ts +4 -0
- /package/source/{presets → topology}/event.json +0 -0
- /package/source/{presets → topology}/index.js +0 -0
- /package/source/{presets → topology}/reply.json +0 -0
- /package/source/{presets → topology}/request.json +0 -0
package/readme.md
CHANGED
|
@@ -5,16 +5,18 @@ Node.js.
|
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
|
-
1. Dynamic topology
|
|
9
|
-
2. Request-reply (RPC)
|
|
10
|
-
3. Events (pub/sub)
|
|
11
|
-
4. Content encoding
|
|
12
|
-
5. Flow control and back pressure handling
|
|
13
|
-
6. Consumer acknowledgments and publisher confirms
|
|
14
|
-
7. Poison message handling
|
|
15
|
-
8. Connection tolerance
|
|
16
|
-
9.
|
|
17
|
-
10. Graceful shutdown
|
|
8
|
+
1. [Dynamic topology](#topology)
|
|
9
|
+
2. [Request](#request)-[reply](#reply) (RPC)
|
|
10
|
+
3. Events ([pub](#emission)/[sub](#consumption))
|
|
11
|
+
4. [Content encoding](#encoding)
|
|
12
|
+
5. [Flow control](#flow-control) and back pressure handling
|
|
13
|
+
6. [Consumer acknowledgments](#messages) and [publisher confirms](#channels)
|
|
14
|
+
7. [Poison message handling](#messages)
|
|
15
|
+
8. [Connection tolerance](#connection-tolerance) and broker restart resilience
|
|
16
|
+
9. [Sharded connection](#sharded-connection)
|
|
17
|
+
10. [Graceful shutdown](#graceful-shutdown)
|
|
18
|
+
|
|
19
|
+
> CommonJS, ECMAScript and TypeScript compatible (types included).
|
|
18
20
|
|
|
19
21
|
## TL;DR
|
|
20
22
|
|
|
@@ -37,7 +39,7 @@ to [`amqplib.connect`](https://amqp-node.github.io/amqplib/channel_api.html#conn
|
|
|
37
39
|
### Example
|
|
38
40
|
|
|
39
41
|
```javascript
|
|
40
|
-
|
|
42
|
+
import { connect } from 'comq'
|
|
41
43
|
|
|
42
44
|
const url = 'amqp://developer:secret@localhost'
|
|
43
45
|
const io = await connect(url)
|
|
@@ -66,7 +68,7 @@ value as in the Request.
|
|
|
66
68
|
|
|
67
69
|
## Reply
|
|
68
70
|
|
|
69
|
-
`async IO.reply(queue: string, producer)`
|
|
71
|
+
`async IO.reply(queue: string, producer): void`
|
|
70
72
|
|
|
71
73
|
`producer` function's signature is `async? (message: any): any`
|
|
72
74
|
|
|
@@ -94,7 +96,7 @@ await io.reply('add_numbers', ({ a, b }) => (a + b))
|
|
|
94
96
|
|
|
95
97
|
## Request
|
|
96
98
|
|
|
97
|
-
`async IO.request(queue: string, payload: any, [encoding: string])`
|
|
99
|
+
`async IO.request(queue: string, payload: any, [encoding: string]): any`
|
|
98
100
|
|
|
99
101
|
Send encoded Request message with `replyTo` and `correlationId` properties set and
|
|
100
102
|
return decoded Reply content.
|
|
@@ -109,7 +111,7 @@ const sum = await io.request('add_numbers', { a: 1, b: 2 })
|
|
|
109
111
|
|
|
110
112
|
## Consumption
|
|
111
113
|
|
|
112
|
-
`async IO.consume(exchange: string, [group: string], consumer)`
|
|
114
|
+
`async IO.consume(exchange: string, [group: string], consumer): void`
|
|
113
115
|
|
|
114
116
|
`consumer` function's signature is `async? (payload: any): void`
|
|
115
117
|
|
|
@@ -135,7 +137,7 @@ await io.consume('numbers_added', 'logger', ({ a, b }) => {
|
|
|
135
137
|
|
|
136
138
|
## Emission
|
|
137
139
|
|
|
138
|
-
`async IO.emit(exchange: string, payload: any, [encoding: string])`
|
|
140
|
+
`async IO.emit(exchange: string, payload: any, [encoding: string]): void`
|
|
139
141
|
|
|
140
142
|
Publish encoded Event to the `exchange`.
|
|
141
143
|
|
|
@@ -175,18 +177,61 @@ The following encoding formats are supported:
|
|
|
175
177
|
|
|
176
178
|
When initially connecting to the broker or if the established connection is lost, connection
|
|
177
179
|
attempts will be repeated indefinitely with intervals increasing up to 30 seconds. Once reconnected,
|
|
178
|
-
the topology will be recovered, and unanswered Requests and unconfirmed Events will be
|
|
180
|
+
the topology will be recovered, and unanswered Requests and unconfirmed Events will be
|
|
181
|
+
retransmitted.
|
|
179
182
|
|
|
180
183
|
If the broker rejects the connection (for example, due to access being denied), an exception will be
|
|
181
184
|
thrown.
|
|
182
185
|
|
|
186
|
+
## Sharded Connection
|
|
187
|
+
|
|
188
|
+
*Send to one, receive from all.*
|
|
189
|
+
|
|
190
|
+
A sharded connection is a mechanism that uses multiple connections simultaneously to achieve load
|
|
191
|
+
balancing and mitigate failover scenarios, utilizing a set of broker instances that are **not**
|
|
192
|
+
combined into a cluster.
|
|
193
|
+
|
|
194
|
+
`async connect(...shards: string[]): IO`
|
|
195
|
+
|
|
196
|
+
Returns an instance of `IO` once a successful connection to one of the shards is established.
|
|
197
|
+
|
|
198
|
+
Outgoing messages are sent to a single connection chosen at random from the shard pool. Shards that
|
|
199
|
+
lose their underlying connection or experience channel [back pressure](#flow-control) on
|
|
200
|
+
corresponding channel are removed from the pool until the issue is resolved. Pending messages
|
|
201
|
+
meeting these conditions are immediately routed among the remaining shards in the pool. If no shards
|
|
202
|
+
are available, messages will wait until any shard's connection is re-established.
|
|
203
|
+
|
|
204
|
+
Incoming messages are consumed from all shards.
|
|
205
|
+
|
|
206
|
+
### Example
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
|
|
210
|
+
const shard0 = 'amqp://developer:secret@localhost:5673'
|
|
211
|
+
const shard1 = 'amqp://developer:secret@localhost:5674'
|
|
212
|
+
|
|
213
|
+
const io = await connect(shard0, shard1)
|
|
214
|
+
|
|
215
|
+
// ...
|
|
216
|
+
|
|
217
|
+
await io.close()
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Flow Control
|
|
222
|
+
|
|
223
|
+
When [back pressure](https://www.rabbitmq.com/flow-control.html) is applied to a channel or the
|
|
224
|
+
underlying broker connection is lost, any current and future outgoing messages will be paused.
|
|
225
|
+
Corresponding returned promises will remain in a `pending` state until the pressure is removed or
|
|
226
|
+
the connection is restored.
|
|
227
|
+
|
|
183
228
|
## Topology
|
|
184
229
|
|
|
185
230
|
Topology is designed to deliver maximum performance while ensuring that the **at least once**
|
|
186
231
|
guarantee provided by RabbitMQ is maintained.
|
|
187
232
|
|
|
188
|
-
> Topology design assumes that unanswered Requests and unconfirmed Events will be
|
|
189
|
-
> reconnection.
|
|
233
|
+
> Topology design assumes that unanswered Requests and unconfirmed Events will be retransmitted
|
|
234
|
+
> upon reconnection.
|
|
190
235
|
|
|
191
236
|
### Dynamic
|
|
192
237
|
|
|
@@ -244,23 +289,23 @@ See:
|
|
|
244
289
|
### Cheatsheet
|
|
245
290
|
|
|
246
291
|
| Message | Prefetch | Confirms | Queue | Acknowledgment | Persistent |
|
|
247
|
-
|
|
248
|
-
| Request | limited | no | durable | manual
|
|
249
|
-
| Reply | unlimited | no | exclusive | automatic
|
|
250
|
-
| Event | limited | yes | durable | manual
|
|
292
|
+
|---------|-----------|----------|-----------|----------------|------------|
|
|
293
|
+
| Request | limited | no | durable | manual | no |
|
|
294
|
+
| Reply | unlimited | no | exclusive | automatic | no |
|
|
295
|
+
| Event | limited | yes | durable | manual | yes |
|
|
251
296
|
|
|
252
297
|
## Graceful Shutdown
|
|
253
298
|
|
|
254
299
|
### Sealing
|
|
255
300
|
|
|
256
|
-
`async IO.seal()`
|
|
301
|
+
`async IO.seal(): void`
|
|
257
302
|
|
|
258
303
|
[Stop receiving](https://amqp-node.github.io/amqplib/channel_api.html#channel_cancel) new Events and
|
|
259
304
|
Requests.
|
|
260
305
|
|
|
261
306
|
### Disconnection
|
|
262
307
|
|
|
263
|
-
`async IO.close()`
|
|
308
|
+
`async IO.close(): void`
|
|
264
309
|
|
|
265
310
|
1. Call `IO.seal()`.
|
|
266
311
|
2. Wait for any outstanding messages to be processed[^2] and acknowledged. Sending Requests,
|
|
@@ -295,19 +340,24 @@ calling `IO.close()`.
|
|
|
295
340
|
|
|
296
341
|
Subscribe to one of the diagnostic events:
|
|
297
342
|
|
|
298
|
-
- `open`: connection is
|
|
343
|
+
- `open`: connection is opened[^3].
|
|
299
344
|
- `close`: connection is closed.
|
|
300
345
|
Optional [`error`](https://amqp-node.github.io/amqplib/channel_api.html#model_events) is passed
|
|
301
346
|
as an argument.
|
|
302
347
|
- `flow`: back pressure is applied to a channel. [Channel type](./types/topology.d.ts) is passed as
|
|
303
348
|
an argument.
|
|
304
349
|
- `drain`: back pressure is removed from a channel. Channel type is passed.
|
|
350
|
+
- `remove`: channel is removed from the [pool](#sharded-connection).
|
|
305
351
|
- `recover`: channel's topology is recovered. Channel type is passed.
|
|
306
352
|
- `discard`: message is [discarded](#messages) as it repeatedly caused
|
|
307
353
|
exceptions. Channel type,
|
|
308
354
|
raw [amqp message object](https://amqp-node.github.io/amqplib/channel_api.html#channel_consume)
|
|
309
355
|
and the exception are passed as arguments.
|
|
310
356
|
|
|
357
|
+
In the case of a [sharded connection](#sharded-connection), an additional argument specifying the
|
|
358
|
+
shard number will be passed to listeners. The shard number corresponds to the number of the argument
|
|
359
|
+
used in the `connect` function call.
|
|
360
|
+
|
|
311
361
|
[^3]: As [`connect`](#connect) function returns an instance of `IO` *after* the connection has been
|
|
312
362
|
established, there is no way to capture the initial `open` event.
|
|
313
363
|
|
package/source/channel.js
CHANGED
|
@@ -7,18 +7,29 @@ const { lazy, recall, promex, failsafe, immediate } = require('@toa.io/generic')
|
|
|
7
7
|
* @implements {comq.Channel}
|
|
8
8
|
*/
|
|
9
9
|
class Channel {
|
|
10
|
+
index
|
|
11
|
+
|
|
12
|
+
/** @type {comq.amqp.Connection} */
|
|
13
|
+
#connection
|
|
14
|
+
|
|
10
15
|
/** @type {comq.Topology} */
|
|
11
16
|
#topology
|
|
12
17
|
|
|
13
18
|
/** @type {comq.amqp.Channel} */
|
|
14
19
|
#channel
|
|
15
20
|
|
|
21
|
+
/** @type {boolean} */
|
|
22
|
+
#failfast
|
|
23
|
+
|
|
16
24
|
/** @type {string[]} */
|
|
17
25
|
#tags = []
|
|
18
26
|
|
|
19
27
|
/** @type {toa.generic.Promex} */
|
|
20
28
|
#paused
|
|
21
29
|
|
|
30
|
+
/** @type {boolean} */
|
|
31
|
+
#sealed = false
|
|
32
|
+
|
|
22
33
|
/** @type {toa.generic.Promex} */
|
|
23
34
|
#recovery = promex()
|
|
24
35
|
|
|
@@ -28,16 +39,23 @@ class Channel {
|
|
|
28
39
|
#diagnostics = new EventEmitter()
|
|
29
40
|
|
|
30
41
|
/**
|
|
42
|
+
* @param {comq.amqp.Connection} connection
|
|
31
43
|
* @param {comq.Topology} topology
|
|
44
|
+
* @param {number} index
|
|
32
45
|
*/
|
|
33
|
-
constructor (topology) {
|
|
46
|
+
constructor (connection, topology, index) {
|
|
47
|
+
this.index = index
|
|
48
|
+
|
|
49
|
+
this.#connection = connection
|
|
34
50
|
this.#topology = topology
|
|
35
|
-
|
|
51
|
+
this.#failfast = index !== undefined
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
if (this.#failfast) failsafe.disable(this.send, this.publish)
|
|
54
|
+
}
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
async create () {
|
|
57
|
+
if (this.#topology.confirms) this.#channel = await this.#connection.createConfirmChannel()
|
|
58
|
+
else this.#channel = await this.#connection.createChannel()
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
consume = recall(this,
|
|
@@ -48,7 +66,7 @@ class Channel {
|
|
|
48
66
|
* @param {comq.channels.consumer} callback
|
|
49
67
|
*/
|
|
50
68
|
async (queue, callback) => {
|
|
51
|
-
await this.#consume(queue, callback)
|
|
69
|
+
if (!this.#sealed) await this.#consume(queue, callback)
|
|
52
70
|
})))
|
|
53
71
|
|
|
54
72
|
subscribe = recall(this,
|
|
@@ -61,7 +79,7 @@ class Channel {
|
|
|
61
79
|
* @returns {Promise<void>}
|
|
62
80
|
*/
|
|
63
81
|
async (exchange, queue, callback) => {
|
|
64
|
-
await this.#consume(queue, callback)
|
|
82
|
+
if (!this.#sealed) await this.#consume(queue, callback)
|
|
65
83
|
})))
|
|
66
84
|
|
|
67
85
|
send = failsafe(this, this.#recover,
|
|
@@ -80,7 +98,7 @@ class Channel {
|
|
|
80
98
|
/**
|
|
81
99
|
* @param {string} exchange
|
|
82
100
|
* @param {Buffer} buffer
|
|
83
|
-
* @param {
|
|
101
|
+
* @param {comq.amqp.options.Publish} [options]
|
|
84
102
|
*/
|
|
85
103
|
async (exchange, buffer, options) => {
|
|
86
104
|
await this.#publish(exchange, DEFAULT, buffer, options)
|
|
@@ -89,15 +107,18 @@ class Channel {
|
|
|
89
107
|
async throw (queue, buffer, options) {
|
|
90
108
|
try {
|
|
91
109
|
await this.#publish(DEFAULT, queue, buffer, options)
|
|
92
|
-
} catch {
|
|
93
|
-
|
|
110
|
+
} catch (exception) {
|
|
111
|
+
if (this.#failfast) throw exception
|
|
112
|
+
// ignore otherwise
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
async seal () {
|
|
117
|
+
this.#sealed = true
|
|
118
|
+
|
|
98
119
|
const cancellations = this.#tags.map((tag) => this.#channel.cancel(tag))
|
|
99
120
|
|
|
100
|
-
await Promise.all(cancellations)
|
|
121
|
+
await Promise.all(cancellations).catch(noop) // won't recover anyway
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
diagnose (event, listener) {
|
|
@@ -105,16 +126,18 @@ class Channel {
|
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
async recover (connection) {
|
|
108
|
-
|
|
129
|
+
this.#connection = connection
|
|
130
|
+
|
|
131
|
+
await this.create()
|
|
109
132
|
|
|
110
133
|
lazy.reset(this)
|
|
111
134
|
await recall(this)
|
|
112
135
|
|
|
113
|
-
this.#unpause(
|
|
136
|
+
this.#unpause(INTERRUPTION)
|
|
114
137
|
|
|
115
|
-
for (const confirmation of this.#confirmations) confirmation.reject(
|
|
138
|
+
for (const confirmation of this.#confirmations) confirmation.reject(INTERRUPTION)
|
|
116
139
|
|
|
117
|
-
// let unpause and confirmation
|
|
140
|
+
// let unpause and confirmation interruptions be handled
|
|
118
141
|
await immediate()
|
|
119
142
|
|
|
120
143
|
this.#recovery.resolve()
|
|
@@ -142,7 +165,7 @@ class Channel {
|
|
|
142
165
|
* @returns {Promise<void>}
|
|
143
166
|
*/
|
|
144
167
|
async #assertExchange (exchange) {
|
|
145
|
-
/** @type {
|
|
168
|
+
/** @type {comq.amqp.options.Exchange} */
|
|
146
169
|
const options = { durable: this.#topology.durable }
|
|
147
170
|
|
|
148
171
|
await this.#channel.assertExchange(exchange, 'fanout', options)
|
|
@@ -208,7 +231,7 @@ class Channel {
|
|
|
208
231
|
* @returns {Promise<void>}
|
|
209
232
|
*/
|
|
210
233
|
async #consume (queue, consumer) {
|
|
211
|
-
/** @type {
|
|
234
|
+
/** @type {comq.amqp.options.Consume} */
|
|
212
235
|
const options = {}
|
|
213
236
|
|
|
214
237
|
if (this.#topology.acknowledgments) consumer = this.#getAcknowledgingConsumer(consumer)
|
|
@@ -230,7 +253,7 @@ class Channel {
|
|
|
230
253
|
|
|
231
254
|
this.#channel.ack(message)
|
|
232
255
|
} catch (exception) {
|
|
233
|
-
if (exception.message === 'Channel closed') return // message will be requeued by the broker
|
|
256
|
+
if (exception.message === 'Channel closed') return // the message will be requeued by the broker
|
|
234
257
|
|
|
235
258
|
if (message.fields.redelivered) this.#discard(message, exception)
|
|
236
259
|
else this.#requeue(message)
|
|
@@ -238,14 +261,14 @@ class Channel {
|
|
|
238
261
|
}
|
|
239
262
|
|
|
240
263
|
/**
|
|
241
|
-
* @param {
|
|
264
|
+
* @param {comq.amqp.Message} message
|
|
242
265
|
*/
|
|
243
266
|
#requeue (message) {
|
|
244
267
|
this.#channel.nack(message, false, true)
|
|
245
268
|
}
|
|
246
269
|
|
|
247
270
|
/**
|
|
248
|
-
* @param {
|
|
271
|
+
* @param {comq.amqp.Message} message
|
|
249
272
|
* @param {Error} [exception]
|
|
250
273
|
*/
|
|
251
274
|
#discard (message, exception) {
|
|
@@ -259,6 +282,8 @@ class Channel {
|
|
|
259
282
|
this.#paused = promex()
|
|
260
283
|
this.#channel.once('drain', this.#unpause)
|
|
261
284
|
this.#diagnostics.emit('flow')
|
|
285
|
+
|
|
286
|
+
if (this.#failfast) throw INTERRUPTION
|
|
262
287
|
}
|
|
263
288
|
|
|
264
289
|
/**
|
|
@@ -276,20 +301,20 @@ class Channel {
|
|
|
276
301
|
|
|
277
302
|
async #recover (exception) {
|
|
278
303
|
if (permanent(exception)) return false
|
|
279
|
-
|
|
280
|
-
await this.#recovery
|
|
304
|
+
else await this.#recovery
|
|
281
305
|
}
|
|
282
306
|
}
|
|
283
307
|
|
|
284
308
|
/**
|
|
285
309
|
* @param {comq.amqp.Connection} connection
|
|
286
310
|
* @param {comq.Topology} topology
|
|
311
|
+
* @param {number} [index]
|
|
287
312
|
* @return {Promise<comq.Channel>}
|
|
288
313
|
*/
|
|
289
|
-
|
|
290
|
-
const channel = new Channel(topology)
|
|
314
|
+
async function create (connection, topology, index) {
|
|
315
|
+
const channel = new Channel(connection, topology, index)
|
|
291
316
|
|
|
292
|
-
await channel.create(
|
|
317
|
+
await channel.create()
|
|
293
318
|
|
|
294
319
|
return channel
|
|
295
320
|
}
|
|
@@ -297,24 +322,23 @@ const create = async (connection, topology) => {
|
|
|
297
322
|
/**
|
|
298
323
|
* @return {boolean}
|
|
299
324
|
*/
|
|
300
|
-
|
|
325
|
+
function permanent (exception) {
|
|
301
326
|
const closed = exception.message === 'Channel closed'
|
|
302
327
|
const ended = exception.message === 'Channel ended, no reply will be forthcoming'
|
|
303
|
-
const internal = exception ===
|
|
304
|
-
const unpause = exception.pause === 1
|
|
328
|
+
const internal = exception === INTERRUPTION
|
|
305
329
|
|
|
306
|
-
return !closed && !ended && !internal
|
|
330
|
+
return !closed && !ended && !internal
|
|
307
331
|
}
|
|
308
332
|
|
|
309
333
|
const DEFAULT = ''
|
|
310
334
|
|
|
311
|
-
/** @type {
|
|
335
|
+
/** @type {comq.amqp.options.Queue} */
|
|
312
336
|
const DURABLE = { durable: true }
|
|
313
337
|
|
|
314
|
-
/** @type {
|
|
338
|
+
/** @type {comq.amqp.options.Queue} */
|
|
315
339
|
const EXCLUSIVE = { exclusive: true }
|
|
316
340
|
|
|
317
|
-
const
|
|
341
|
+
const INTERRUPTION = /** @type {Error} */ Symbol('internal interruption')
|
|
318
342
|
|
|
319
343
|
function noop () {}
|
|
320
344
|
|
package/source/connect.js
CHANGED
|
@@ -2,14 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
const { IO } = require('./io')
|
|
4
4
|
const { Connection } = require('./connection')
|
|
5
|
+
const shards = require('./shards')
|
|
5
6
|
|
|
6
7
|
/** @type {comq.connect} */
|
|
7
|
-
const connect = async (
|
|
8
|
-
const connection =
|
|
8
|
+
const connect = async (...urls) => {
|
|
9
|
+
const connection = create(urls)
|
|
9
10
|
|
|
10
11
|
await connection.open()
|
|
11
12
|
|
|
12
13
|
return new IO(connection)
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @param {string[]} urls
|
|
18
|
+
* @return {comq.Connection}
|
|
19
|
+
*/
|
|
20
|
+
const create = (urls) => {
|
|
21
|
+
if (urls.length === 1) return single(urls[0])
|
|
22
|
+
else return sharded(urls)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} url
|
|
27
|
+
* @return {comq.Connection}
|
|
28
|
+
*/
|
|
29
|
+
const single = (url) => new Connection(url)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string[]} urls
|
|
33
|
+
* @return {comq.Connection}
|
|
34
|
+
*/
|
|
35
|
+
const sharded = (urls) => {
|
|
36
|
+
const connections = urls.map(single)
|
|
37
|
+
|
|
38
|
+
return new shards.Connection(connections)
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
exports.connect = connect
|
package/source/connection.js
CHANGED
|
@@ -4,7 +4,7 @@ const { EventEmitter } = require('node:events')
|
|
|
4
4
|
const amqp = require('amqplib')
|
|
5
5
|
const { retry, promex, failsafe } = require('@toa.io/generic')
|
|
6
6
|
|
|
7
|
-
const presets = require('./
|
|
7
|
+
const presets = require('./topology')
|
|
8
8
|
const channels = require('./channel')
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -14,7 +14,7 @@ class Connection {
|
|
|
14
14
|
/** @type {string} */
|
|
15
15
|
#url
|
|
16
16
|
|
|
17
|
-
/** @type {
|
|
17
|
+
/** @type {comq.amqp.Connection} */
|
|
18
18
|
#connection
|
|
19
19
|
|
|
20
20
|
/** @type {comq.Channel[]} */
|
|
@@ -33,21 +33,26 @@ class Connection {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async open () {
|
|
36
|
-
await retry(
|
|
36
|
+
await retry(this.#open, RETRY)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async close () {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// connection already closed
|
|
44
|
-
}
|
|
40
|
+
if (this.#connection === undefined) await this.#recovery
|
|
41
|
+
|
|
42
|
+
await this.#connection.close()
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
createChannel = failsafe(this, this.#recover,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
/**
|
|
47
|
+
* @param {comq.topology.type} type
|
|
48
|
+
* @param {number} [index]
|
|
49
|
+
* @return {Promise<comq.Channel>}
|
|
50
|
+
*/
|
|
51
|
+
async (type, index) => {
|
|
52
|
+
if (this.#connection === undefined) await this.#recovery
|
|
53
|
+
|
|
54
|
+
const topology = presets[type]
|
|
55
|
+
const channel = await channels.create(this.#connection, topology, index)
|
|
51
56
|
|
|
52
57
|
this.#channels.push(channel)
|
|
53
58
|
|
|
@@ -59,9 +64,9 @@ class Connection {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
/**
|
|
62
|
-
* @
|
|
67
|
+
* @type {toa.generic.retry.Task}
|
|
63
68
|
*/
|
|
64
|
-
|
|
69
|
+
#open = async (retry) => {
|
|
65
70
|
try {
|
|
66
71
|
this.#connection = await amqp.connect(this.#url)
|
|
67
72
|
} catch (exception) {
|
|
@@ -69,11 +74,11 @@ class Connection {
|
|
|
69
74
|
else throw exception
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
this.#connection.on('close', this.#close)
|
|
73
|
-
|
|
74
77
|
// prevents process crash, 'close' will be emitted next
|
|
75
78
|
// https://amqp-node.github.io/amqplib/channel_api.html#model_events
|
|
76
|
-
this.#connection.on('error',
|
|
79
|
+
this.#connection.on('error', noop)
|
|
80
|
+
|
|
81
|
+
this.#connection.on('close', this.#close)
|
|
77
82
|
this.#diagnostics.emit('open')
|
|
78
83
|
|
|
79
84
|
for (const channel of this.#channels) await channel.recover(this.#connection)
|
|
@@ -87,8 +92,8 @@ class Connection {
|
|
|
87
92
|
*/
|
|
88
93
|
#close = async (error) => {
|
|
89
94
|
this.#diagnostics.emit('close', error)
|
|
90
|
-
|
|
91
95
|
this.#connection.removeAllListeners()
|
|
96
|
+
this.#connection = undefined
|
|
92
97
|
|
|
93
98
|
if (error !== undefined) await this.open()
|
|
94
99
|
}
|
|
@@ -114,4 +119,6 @@ const transient = (exception) => {
|
|
|
114
119
|
return refused || handshake
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
function noop () {}
|
|
123
|
+
|
|
117
124
|
exports.Connection = Connection
|
package/source/decode.js
CHANGED
package/source/events.js
ADDED
package/source/io.js
CHANGED
|
@@ -6,6 +6,7 @@ const { lazy, track, failsafe, promex } = require('@toa.io/generic')
|
|
|
6
6
|
|
|
7
7
|
const { decode } = require('./decode')
|
|
8
8
|
const { encode } = require('./encode')
|
|
9
|
+
const events = require('./events')
|
|
9
10
|
const io = require('./.io')
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -38,7 +39,7 @@ class IO {
|
|
|
38
39
|
constructor (connection) {
|
|
39
40
|
this.#connection = connection
|
|
40
41
|
|
|
41
|
-
for (const event of
|
|
42
|
+
for (const event of events.connection) {
|
|
42
43
|
this.#connection.diagnose(event, (...args) => this.#diagnostics.emit(event, ...args))
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -55,13 +56,14 @@ class IO {
|
|
|
55
56
|
await this.#requests.consume(queue, consumer)
|
|
56
57
|
})
|
|
57
58
|
|
|
59
|
+
// failsafe is aimed to retransmit unanswered messages
|
|
58
60
|
request = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
|
|
59
61
|
failsafe(this, this.#recover,
|
|
60
62
|
/**
|
|
61
63
|
* @param {string} queue
|
|
62
64
|
* @param {any} payload
|
|
63
65
|
* @param {comq.encoding} [encoding]
|
|
64
|
-
* @param {comq.ReplyToPropertyFormatter} replyToFormatter
|
|
66
|
+
* @param {comq.ReplyToPropertyFormatter} [replyToFormatter]
|
|
65
67
|
* @returns {Promise<void>}
|
|
66
68
|
*/
|
|
67
69
|
async (queue, payload, encoding, replyToFormatter) => {
|
|
@@ -123,7 +125,7 @@ class IO {
|
|
|
123
125
|
this.#requests = await this.#createChannel('request')
|
|
124
126
|
this.#replies = await this.#createChannel('reply')
|
|
125
127
|
|
|
126
|
-
this.#
|
|
128
|
+
this.#setupRetransmission()
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
async #createEventChannel () {
|
|
@@ -148,13 +150,19 @@ class IO {
|
|
|
148
150
|
async #createChannel (type) {
|
|
149
151
|
const channel = await this.#connection.createChannel(type)
|
|
150
152
|
|
|
151
|
-
for (const event of
|
|
153
|
+
for (const event of events.channel) {
|
|
152
154
|
channel.diagnose(event, (...args) => this.#diagnostics.emit(event, type, ...args))
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
return channel
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
#setupRetransmission () {
|
|
161
|
+
const event = (this.#requests.sharded === true) ? 'remove' : 'recover'
|
|
162
|
+
|
|
163
|
+
this.#requests.diagnose(event, this.#retransmit)
|
|
164
|
+
}
|
|
165
|
+
|
|
158
166
|
/**
|
|
159
167
|
* @param {comq.producer} producer
|
|
160
168
|
* @returns {comq.channels.consumer}
|
|
@@ -185,6 +193,9 @@ class IO {
|
|
|
185
193
|
* @returns {comq.channels.consumer}
|
|
186
194
|
*/
|
|
187
195
|
#getReplyConsumer = (queue, emitter) =>
|
|
196
|
+
/**
|
|
197
|
+
* @param {comq.amqp.Message} message
|
|
198
|
+
*/
|
|
188
199
|
(message) => {
|
|
189
200
|
const payload = decode(message)
|
|
190
201
|
|
|
@@ -216,12 +227,14 @@ class IO {
|
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
#recover (exception) {
|
|
219
|
-
if (exception !==
|
|
230
|
+
if (exception !== RETRANSMISSION) return false
|
|
220
231
|
}
|
|
221
232
|
|
|
222
|
-
#
|
|
233
|
+
#retransmit = () => {
|
|
223
234
|
for (const emitter of Object.values(this.#emitters)) emitter.clear()
|
|
224
|
-
|
|
235
|
+
|
|
236
|
+
// trigger failsafe attribute
|
|
237
|
+
for (const reply of this.#pendingReplies) reply.reject(RETRANSMISSION)
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
/**
|
|
@@ -246,13 +259,7 @@ const OCTETS = 'application/octet-stream'
|
|
|
246
259
|
/** @type {comq.encoding} */
|
|
247
260
|
const DEFAULT = 'application/msgpack'
|
|
248
261
|
|
|
249
|
-
/** @type {
|
|
250
|
-
const CONNECTION_EVENTS = ['open', 'close']
|
|
251
|
-
|
|
252
|
-
/** @type {comq.diagnostics.event[]} */
|
|
253
|
-
const CHANNEL_EVENTS = ['flow', 'drain', 'recover', 'discard']
|
|
254
|
-
|
|
255
|
-
const REJECTION = /** @type {Error} */ Symbol('resend')
|
|
262
|
+
const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
|
|
256
263
|
|
|
257
264
|
function noop () {}
|
|
258
265
|
|