comq 0.9.0 → 0.10.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/.github/dependabot.yml +2 -2
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release.yaml +14 -15
- package/docs/reply-stream-topology-dark.jpg +0 -0
- package/docs/reply-stream-topology-light.jpg +0 -0
- package/examples/readme.md +19 -3
- package/examples/streams/consumer.js +36 -0
- package/examples/streams/producer.js +44 -0
- package/features/backpressure.feature +2 -2
- package/features/readme.md +2 -2
- package/features/rpc.streams.feature +81 -0
- package/features/rpc.streams.shards.feature +37 -0
- package/features/steps/.test/context.test.js +1 -1
- package/features/steps/.test/events.test.js +1 -1
- package/features/steps/.types/context.d.ts +9 -1
- package/features/steps/broker.js +2 -0
- package/features/steps/common.js +12 -0
- package/features/steps/context.js +9 -2
- package/features/steps/rpc.js +235 -0
- package/lerna.json +1 -2
- package/package.json +3 -2
- package/readme.md +136 -46
- package/source/.io/ReplyPipe.js +158 -0
- package/source/.io/ReplyStream.js +139 -0
- package/source/.io/const.js +10 -0
- package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
- package/source/.io/index.js +5 -1
- package/source/attributes/failsafe.js +34 -0
- package/source/attributes/failsafe.test.js +133 -0
- package/source/attributes/index.js +13 -0
- package/source/attributes/lazy.js +79 -0
- package/source/attributes/lazy.test.js +299 -0
- package/source/attributes/memo.js +13 -0
- package/source/attributes/memo.test.js +36 -0
- package/source/attributes/recall.js +42 -0
- package/source/attributes/recall.test.js +92 -0
- package/source/attributes/track.js +32 -0
- package/source/attributes/track.test.js +133 -0
- package/source/channel.js +31 -18
- package/source/connect.js +2 -2
- package/source/connection.js +4 -11
- package/source/emitter.js +13 -0
- package/source/encode.js +1 -1
- package/source/events.js +2 -2
- package/source/io.js +187 -50
- package/source/shards/channel.js +13 -10
- package/source/shards/connection.js +2 -2
- package/test/channel.test.js +3 -1
- package/test/encode.test.js +2 -2
- package/test/encodings.js +1 -1
- package/test/io.diagnostics.test.js +2 -2
- package/test/io.request.test.js +4 -23
- package/test/shards/channel.test.js +1 -1
- package/test/shards/connection.test.js +1 -1
- package/types/channel.d.ts +13 -11
- package/types/connection.d.ts +3 -3
- package/types/diagnostic.d.ts +4 -3
- package/types/encoding.d.ts +2 -2
- package/types/index.d.ts +4 -4
- package/types/io.d.ts +33 -40
package/features/steps/rpc.js
CHANGED
|
@@ -6,6 +6,7 @@ const { randomBytes } = require('node:crypto')
|
|
|
6
6
|
const { parse } = require('@toa.io/yaml')
|
|
7
7
|
const { match, timeout, quantity } = require('@toa.io/generic')
|
|
8
8
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
9
|
+
const { Readable } = require('node:stream')
|
|
9
10
|
|
|
10
11
|
Given('function replying {token} queue:',
|
|
11
12
|
/**
|
|
@@ -20,6 +21,70 @@ Given('function replying {token} queue:',
|
|
|
20
21
|
await this.io.reply(queue, producer)
|
|
21
22
|
})
|
|
22
23
|
|
|
24
|
+
Given('a generator replying {token} queue:',
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} queue
|
|
27
|
+
* @param {string} javascript
|
|
28
|
+
* @this {comq.features.Context}
|
|
29
|
+
*/
|
|
30
|
+
async function (queue, javascript) {
|
|
31
|
+
// eslint-disable-next-line no-new-func
|
|
32
|
+
const generator = new Function('return ' + javascript)()
|
|
33
|
+
|
|
34
|
+
function producer (input) {
|
|
35
|
+
return stream.Readable.from(generator(input))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await this.io.reply(queue, producer)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
Given('a number generator with {number}ms increasing delay replying {token} queue',
|
|
42
|
+
/**
|
|
43
|
+
* @param {number} delay
|
|
44
|
+
* @param {string} queue
|
|
45
|
+
* @this {comq.features.Context}
|
|
46
|
+
*/
|
|
47
|
+
async function (delay, queue) {
|
|
48
|
+
const that = this
|
|
49
|
+
|
|
50
|
+
class Stream extends Readable {
|
|
51
|
+
#MAX = 10
|
|
52
|
+
#count = 0
|
|
53
|
+
|
|
54
|
+
constructor () {
|
|
55
|
+
super({ objectMode: true })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async _read (size) {
|
|
59
|
+
await timeout(delay * (1 + this.#count / 5))
|
|
60
|
+
|
|
61
|
+
if (this.#count === this.#MAX) { this.push(null) } else this.push(this.#count++)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_destroy (error, callback) {
|
|
65
|
+
that.generatorDestroyed = true
|
|
66
|
+
this.push(null)
|
|
67
|
+
super._destroy(error, callback)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function producer () {
|
|
72
|
+
return new Stream()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await this.io.reply(queue, producer)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
Given('heartbeat interval is set to {number}ms',
|
|
79
|
+
function (value) {
|
|
80
|
+
global.COMQ_TESTING_HEARTBEAT_INTERVAL = value
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
Given('idle timeout is set to {number}ms',
|
|
84
|
+
function (value) {
|
|
85
|
+
global.COMQ_TESTING_IDLE_INTERVAL = value
|
|
86
|
+
})
|
|
87
|
+
|
|
23
88
|
Given('a producer replying {token} queue',
|
|
24
89
|
/**
|
|
25
90
|
* @param {string} queue
|
|
@@ -71,6 +136,37 @@ When('the consumer sends a request to the {token} queue',
|
|
|
71
136
|
await send.call(this, queue, payload)
|
|
72
137
|
})
|
|
73
138
|
|
|
139
|
+
When('the consumer requests a stream with the following request to the {token} queue:',
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} queue
|
|
142
|
+
* @param {string} yaml
|
|
143
|
+
* @this {comq.features.Context}
|
|
144
|
+
*/
|
|
145
|
+
async function (queue, yaml) {
|
|
146
|
+
const payload = parse(yaml)
|
|
147
|
+
|
|
148
|
+
await fetch.call(this, queue, payload)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
When('the consumer requests a stream with request to the {token} queue',
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} queue
|
|
154
|
+
* @this {comq.features.Context}
|
|
155
|
+
*/
|
|
156
|
+
async function (queue) {
|
|
157
|
+
await fetch.call(this, queue, null)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
When('the consumer{number} requests a stream with request to the {token} queue',
|
|
161
|
+
/**
|
|
162
|
+
* @param {number} number
|
|
163
|
+
* @param {string} queue
|
|
164
|
+
* @this {comq.features.Context}
|
|
165
|
+
*/
|
|
166
|
+
async function (number, queue) {
|
|
167
|
+
await fetch.call(this, queue, null, number)
|
|
168
|
+
})
|
|
169
|
+
|
|
74
170
|
Then('the consumer receives the reply:',
|
|
75
171
|
/**
|
|
76
172
|
* @param {string} yaml
|
|
@@ -107,8 +203,127 @@ Then('the consumer does not receive the reply',
|
|
|
107
203
|
assert.equal(reply, undefined, 'The reply was received')
|
|
108
204
|
})
|
|
109
205
|
|
|
206
|
+
Then('the consumer receives the stream:',
|
|
207
|
+
/**
|
|
208
|
+
* @param {string} yaml
|
|
209
|
+
* @this {comq.features.Context}
|
|
210
|
+
*/
|
|
211
|
+
async function (yaml) {
|
|
212
|
+
const values = parse(yaml)
|
|
213
|
+
const replies = []
|
|
214
|
+
|
|
215
|
+
for await (const reply of this.stream) replies.push(reply)
|
|
216
|
+
|
|
217
|
+
assert.equal(values.length, replies.length, `Stream length mismatch: expected ${values.length}, received ${replies.length}`)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
Then('the consumer receives the stream',
|
|
221
|
+
/**
|
|
222
|
+
* @this {comq.features.Context}
|
|
223
|
+
*/
|
|
224
|
+
async function () {
|
|
225
|
+
this.stream.on('data', (data) => this.streamValues.push(data))
|
|
226
|
+
this.stream.on('end', () => (this.streamEnded = true))
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
Then('the consumer{number} receives the stream',
|
|
230
|
+
/**
|
|
231
|
+
* @param {number} number
|
|
232
|
+
* @this {comq.features.Context}
|
|
233
|
+
*/
|
|
234
|
+
async function (number) {
|
|
235
|
+
this.streams[number].on('data', (data) => this.streamsValues[number].push(data))
|
|
236
|
+
this.streams[number].on('end', () => (this.streamsEnded[number] = true))
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
Then('the consumer has received the stream:',
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} yaml
|
|
242
|
+
* @this {comq.features.Context}
|
|
243
|
+
*/
|
|
244
|
+
async function (yaml) {
|
|
245
|
+
const values = parse(yaml)
|
|
246
|
+
|
|
247
|
+
assert.equal(this.streamEnded, true, 'The stream was not closed')
|
|
248
|
+
|
|
249
|
+
assert.equal(values.length, this.streamValues.length,
|
|
250
|
+
`Stream values count mismatch: expected ${values.length}, received ${this.streamValues.length}`)
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < values.length; i++) {
|
|
253
|
+
assert.equal(values[i], this.streamValues[i], `Stream value mismatch at index ${i}`)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
Then('the consumer{number} has received the stream:',
|
|
258
|
+
/**
|
|
259
|
+
* @param {number} number
|
|
260
|
+
* @param {string} yaml
|
|
261
|
+
* @this {comq.features.Context}
|
|
262
|
+
*/
|
|
263
|
+
async function (number, yaml) {
|
|
264
|
+
const expected = parse(yaml)
|
|
265
|
+
const values = this.streamsValues[number]
|
|
266
|
+
|
|
267
|
+
assert.equal(this.streamsEnded[number], true, 'The stream was not closed')
|
|
268
|
+
|
|
269
|
+
assert.equal(expected.length, values.length,
|
|
270
|
+
`Stream values count mismatch: expected ${expected.length}, received ${values.length}`)
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < expected.length; i++) {
|
|
273
|
+
assert.equal(expected[i], values[i], `Stream value mismatch at index ${i}`)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
Then('the consumer has received the stream containing:',
|
|
278
|
+
/**
|
|
279
|
+
* @param {string} yaml
|
|
280
|
+
* @this {comq.features.Context}
|
|
281
|
+
*/
|
|
282
|
+
async function (yaml) {
|
|
283
|
+
const values = parse(yaml)
|
|
284
|
+
|
|
285
|
+
assert.equal(this.streamEnded, true, 'The stream was not closed')
|
|
286
|
+
|
|
287
|
+
assert.equal(values.length <= this.streamValues.length, true,
|
|
288
|
+
`Received less values: expected ${values.length}, received ${this.streamValues.length}`)
|
|
289
|
+
|
|
290
|
+
for (const value of values) {
|
|
291
|
+
assert.notEqual(this.streamValues.indexOf(value), -1,
|
|
292
|
+
`Received values does not contain ${value}`)
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
Then('the consumer interrupts the stream after {number} replies',
|
|
297
|
+
/**
|
|
298
|
+
* @param {number} replies
|
|
299
|
+
* @this {comq.features.Context}
|
|
300
|
+
*/
|
|
301
|
+
async function (replies) {
|
|
302
|
+
// eslint-disable-next-line no-unused-vars
|
|
303
|
+
for await (const _ of this.stream) {
|
|
304
|
+
if (--replies === 0) break // this destroys the stream
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
assert.equal(replies, 0, 'Stream was prematurely interrupted')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
Then('the consumer interrupts the stream',
|
|
311
|
+
/**
|
|
312
|
+
* @this {comq.features.Context}
|
|
313
|
+
*/
|
|
314
|
+
async function () {
|
|
315
|
+
this.stream.destroy()
|
|
316
|
+
await timeout(0)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
Then('the generator is destroyed',
|
|
320
|
+
async function () {
|
|
321
|
+
assert.equal(this.generatorDestroyed, true, 'The generator was not destroyed')
|
|
322
|
+
})
|
|
323
|
+
|
|
110
324
|
When('the consumer sends {quantity} requests to the {token} queue as a stream',
|
|
111
325
|
/**
|
|
326
|
+
* @param {string} amountQ
|
|
112
327
|
* @param {string} queue
|
|
113
328
|
* @this {comq.features.Context}
|
|
114
329
|
*/
|
|
@@ -183,3 +398,23 @@ async function send (queue, payload) {
|
|
|
183
398
|
|
|
184
399
|
this.reply = this.io.request(queue, payload)
|
|
185
400
|
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {string} queue
|
|
404
|
+
* @param {any} payload
|
|
405
|
+
* @param {number} number
|
|
406
|
+
* @this {comq.features.Context}
|
|
407
|
+
* @return {Promise<void>}
|
|
408
|
+
*/
|
|
409
|
+
async function fetch (queue, payload, number) {
|
|
410
|
+
if (number === undefined) {
|
|
411
|
+
this.stream = await this.io.request(queue, payload)
|
|
412
|
+
} else {
|
|
413
|
+
this.streams[number] = await this.io.request(queue, payload)
|
|
414
|
+
this.streamsValues[number] = []
|
|
415
|
+
this.streamsEnded[number] = false
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
global.COMQ_TESTING_IDLE_INTERVAL = 150
|
|
420
|
+
global.COMQ_TESTING_HEARTBEAT_INTERVAL = 100
|
package/lerna.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
"msgpackr": "^1.9.5"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
|
+
"prepare": "echo run: husky install",
|
|
48
49
|
"test": "standard && jest",
|
|
49
|
-
"features": "cucumber-js",
|
|
50
|
+
"features": "cucumber-js --publish-quiet",
|
|
50
51
|
"lint": "standard --fix --verbose | snazzy"
|
|
51
52
|
},
|
|
52
53
|
"standard": {
|
package/readme.md
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# ComQ
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Node.js.
|
|
3
|
+
Production-grade communication via [AMQP](https://github.com/amqp-node/amqplib)
|
|
4
|
+
for distributed, eventually consistent systems running on Node.js.
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
8
|
- [Dynamic topology](#topology)
|
|
9
9
|
- [Request](#request)-[reply](#reply) (RPC)
|
|
10
10
|
- Events ([pub](#emission)/[sub](#consumption))
|
|
11
|
+
- [Pipelines](#pipelines)
|
|
12
|
+
- [Reply streams](#reply-streams)
|
|
11
13
|
- [Content encoding](#encoding)
|
|
12
14
|
- [Flow control](#flow-control) and back pressure handling
|
|
13
|
-
- [Pipelines](#pipelines)
|
|
14
15
|
- [Consumer acknowledgments](#messages) and [publisher confirms](#channels)
|
|
15
16
|
- [Poison message handling](#messages)
|
|
16
17
|
- [Connection tolerance](#connection-tolerance) and broker restart resilience
|
|
17
|
-
- [Sharded connection](#sharded-connection)
|
|
18
|
+
- [Sharded connection](#sharded-connection) :rocket:
|
|
18
19
|
- [Singleton connection](#singleton-connection)
|
|
19
20
|
- [Graceful shutdown](#graceful-shutdown)
|
|
20
21
|
|
|
@@ -22,7 +23,8 @@ Node.js.
|
|
|
22
23
|
|
|
23
24
|
## TL;DR
|
|
24
25
|
|
|
25
|
-
[Code examples](examples)
|
|
26
|
+
- [Code examples](examples)
|
|
27
|
+
- [Scenarios](features)
|
|
26
28
|
|
|
27
29
|
## Installation
|
|
28
30
|
|
|
@@ -75,7 +77,7 @@ value as in the Request.
|
|
|
75
77
|
`producer` function's signature is `async? (message: any): any`
|
|
76
78
|
|
|
77
79
|
Assert a `queue` and start consuming Requests. Received messages are decoded and the resulting
|
|
78
|
-
content is passed to the `producer`. The result
|
|
80
|
+
content is passed to the `producer`. The result returned by the `producer` is then encoded and sent
|
|
79
81
|
back to the queue specified in the `replyTo` property of the Request, along with a `correlationId`
|
|
80
82
|
that has the same value as in the Request.
|
|
81
83
|
|
|
@@ -98,7 +100,7 @@ await io.reply('add_numbers', ({ a, b }) => (a + b))
|
|
|
98
100
|
|
|
99
101
|
## Request
|
|
100
102
|
|
|
101
|
-
`async IO.request(queue: string, payload: any,
|
|
103
|
+
`async IO.request(queue: string, payload: any, encoding?: string): any`
|
|
102
104
|
|
|
103
105
|
Send encoded Request message with `replyTo` and `correlationId` properties set and
|
|
104
106
|
return decoded Reply content.
|
|
@@ -113,7 +115,7 @@ const sum = await io.request('add_numbers', { a: 1, b: 2 })
|
|
|
113
115
|
|
|
114
116
|
## Consumption
|
|
115
117
|
|
|
116
|
-
`async IO.consume(exchange: string,
|
|
118
|
+
`async IO.consume(exchange: string, group?: string, consumer): void`
|
|
117
119
|
|
|
118
120
|
`consumer` function's signature is `async? (payload: any): void`
|
|
119
121
|
|
|
@@ -132,14 +134,20 @@ auto-generated name.
|
|
|
132
134
|
### Example
|
|
133
135
|
|
|
134
136
|
```javascript
|
|
135
|
-
|
|
137
|
+
// with a consumer function
|
|
138
|
+
await io.consume('numbers_added', 'logger',
|
|
139
|
+
({ a, b }) => console.log(`${a} was added to ${b}`))
|
|
140
|
+
|
|
141
|
+
// as a stream
|
|
142
|
+
const events = await io.consume('numbers_added', 'logger')
|
|
143
|
+
|
|
144
|
+
for await (const { a, b } of events)
|
|
136
145
|
console.log(`${a} was added to ${b}`)
|
|
137
|
-
})
|
|
138
146
|
```
|
|
139
147
|
|
|
140
148
|
## Emission
|
|
141
149
|
|
|
142
|
-
`async IO.emit(exchange: string, payload: any,
|
|
150
|
+
`async IO.emit(exchange: string, payload: any, encoding?: string): void`
|
|
143
151
|
|
|
144
152
|
Publish encoded Event to the `exchange`.
|
|
145
153
|
|
|
@@ -153,6 +161,107 @@ asserted.
|
|
|
153
161
|
await io.emit('numbers_added', { a: 1, b: 2 })
|
|
154
162
|
```
|
|
155
163
|
|
|
164
|
+
## Pipelines
|
|
165
|
+
|
|
166
|
+
Payloads for Requests and Events can be passed as a readable stream
|
|
167
|
+
in [object mode](https://nodejs.org/api/stream.html#object-mode), enabling the handling of large amounts of data with
|
|
168
|
+
the benefits of RabbitMQ back pressure and flow control.
|
|
169
|
+
|
|
170
|
+
`async IO.request(queue: string, stream: Readable, encoding?: string): Readable`
|
|
171
|
+
|
|
172
|
+
Returns a readable stream of replies.
|
|
173
|
+
|
|
174
|
+
`async IO.emit(exchange: string, stream: Readable, encoding?: string): void`
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
function * generate () {
|
|
178
|
+
yield { a: 1, b: 2 };
|
|
179
|
+
yield { a: 3, b: 4 };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const events = Readable.from(generate())
|
|
183
|
+
|
|
184
|
+
await io.emit('numbers_added', events)
|
|
185
|
+
|
|
186
|
+
const requests = Readable.from(generate())
|
|
187
|
+
|
|
188
|
+
for await (const reply of io.request('add_numbers', requests))
|
|
189
|
+
console.log(reply)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Reply streams
|
|
193
|
+
|
|
194
|
+
The `producer` function of [`IO.reply`](#reply) may return a non-array
|
|
195
|
+
(Async)[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols).
|
|
196
|
+
In this case, the yielded values will be sent to the `replyTo` queue until the iterator is finished,
|
|
197
|
+
or a [cancellation message](#stream-control) is received, or the `replyTo` queue is deleted.
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
await io.reply('get_numbers', function * ({ amount }) {
|
|
201
|
+
for (let i = 0; i < amount; i++) yield i
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The Reply stream may be consumed by using the `IO.request` function:
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
const stream = await io.request('get_numbers', { amount: 10 })
|
|
209
|
+
|
|
210
|
+
for await (const number of stream)
|
|
211
|
+
console.log(number)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Stream topology
|
|
215
|
+
|
|
216
|
+
<picture>
|
|
217
|
+
<source media="(prefers-color-scheme: dark)" srcset="./docs/reply-stream-topology-dark.jpg">
|
|
218
|
+
<img alt="Reply topology" width="500" height="411" src="./docs/reply-stream-topology-light.jpg">
|
|
219
|
+
</picture>
|
|
220
|
+
|
|
221
|
+
When the producer function of `IO.reply` returns an Iterator for the first time across all request queues,
|
|
222
|
+
a control queue is asserted on the [Reply channel](#channels)
|
|
223
|
+
using the [reply topology](#exchanges-and-queues).
|
|
224
|
+
When the Consumer destroys the Reply stream, a stream cancellation message is sent to the Producer's control queue.
|
|
225
|
+
|
|
226
|
+
### Stream control
|
|
227
|
+
|
|
228
|
+
Upon receiving a request, the Producer sends a confirmation message to the `replyTo` queue.
|
|
229
|
+
If the underlying connection is lost before the Consumer receives the confirmation message,
|
|
230
|
+
the request will be retransmitted upon reconnection.
|
|
231
|
+
|
|
232
|
+
A heartbeat message is sent to the `replyTo` queue whenever a Reply stream idles for 5 seconds.
|
|
233
|
+
If the Consumer of the reply stream doesn't receive a reply or a heartbeat message for 12 seconds, the stream returned
|
|
234
|
+
by `IO.request` is destroyed.
|
|
235
|
+
These intervals are not configurable.
|
|
236
|
+
|
|
237
|
+
An "end stream" message is sent to the `replyTo` queue when the Reply stream is finished.
|
|
238
|
+
|
|
239
|
+
See also [Reply stream shutdown](#reply-stream-shutdown).
|
|
240
|
+
|
|
241
|
+
### Loss of tail
|
|
242
|
+
|
|
243
|
+
:warning:
|
|
244
|
+
|
|
245
|
+
While consuming the Reply stream if the broker connection is lost,
|
|
246
|
+
or if the Consumer crashes or destroys the stream returned by `IO.request`,
|
|
247
|
+
some of the values yielded by the Reply stream may be lost.
|
|
248
|
+
|
|
249
|
+
To avoid inconsistency, it is strongly recommended to use the Reply stream only with _safe_ Producers, which do not
|
|
250
|
+
change the application state.
|
|
251
|
+
|
|
252
|
+
### Stream guarantees
|
|
253
|
+
|
|
254
|
+
:warning:
|
|
255
|
+
|
|
256
|
+
The reply topology guarantees
|
|
257
|
+
that the order of yielded values is [preserved](https://www.rabbitmq.com/queues.html#message-ordering).
|
|
258
|
+
At the same time, there is no guarantee that the stream will be transmitted to the end.
|
|
259
|
+
|
|
260
|
+
> When using the [Sharded connection](#sharded-connection), the order of yielded values is maintained through buffering.
|
|
261
|
+
> However, there is a scenario in which some of the yielded values may be lost if a broker crashes.
|
|
262
|
+
> In this case, the Reply stream will be destroyed once the buffer's maximum size is exceeded.
|
|
263
|
+
> Also, buffered control messages can result in [stream idling](#stream-control).
|
|
264
|
+
|
|
156
265
|
## Encoding
|
|
157
266
|
|
|
158
267
|
By default, outgoing message contents are encoded with [msgpack](https://msgpack.org) and
|
|
@@ -182,34 +291,6 @@ underlying broker connection is lost, any current and future outgoing messages w
|
|
|
182
291
|
Corresponding returned promises will remain in a `pending` state until the pressure is removed or
|
|
183
292
|
the connection is restored.
|
|
184
293
|
|
|
185
|
-
### Pipelines
|
|
186
|
-
|
|
187
|
-
Payloads for Requests and Events can be passed as a readable stream
|
|
188
|
-
in [object mode](https://nodejs.org/api/stream.html#object-mode), enabling the handling of large amounts of data with
|
|
189
|
-
the benefits of RabbitMQ back pressure and flow control.
|
|
190
|
-
|
|
191
|
-
`async IO.request(queue: string, payload: Readable, [encoding: string]): Readable`
|
|
192
|
-
|
|
193
|
-
Returns a readable stream of replies.
|
|
194
|
-
|
|
195
|
-
`async IO.emit(exchange: string, payload: Readable, [encoding: string]): void`
|
|
196
|
-
|
|
197
|
-
```javascript
|
|
198
|
-
function * generate () {
|
|
199
|
-
yield { a: 1, b: 2 };
|
|
200
|
-
yield { a: 3, b: 4 };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const events = Readable.from(generate())
|
|
204
|
-
|
|
205
|
-
await io.emit('numbers_added', events)
|
|
206
|
-
|
|
207
|
-
const requests = Readable.from(generate())
|
|
208
|
-
|
|
209
|
-
for await (const reply of io.request('add_numbers', requests))
|
|
210
|
-
console.log(reply)
|
|
211
|
-
```
|
|
212
|
-
|
|
213
294
|
## Connection tolerance
|
|
214
295
|
|
|
215
296
|
When the established connection is lost, it will be automatically restored.
|
|
@@ -300,8 +381,10 @@ requests and are expecting replies.
|
|
|
300
381
|
### Exchanges and queues
|
|
301
382
|
|
|
302
383
|
- Exchanges and queues for Events, and queues for Requests
|
|
303
|
-
are
|
|
304
|
-
- Queues for Replies are
|
|
384
|
+
are _durable_.
|
|
385
|
+
- Queues for Replies are _exclusive_ and _auto deleted_.
|
|
386
|
+
|
|
387
|
+
See [queue assertion options](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue).
|
|
305
388
|
|
|
306
389
|
### Messages
|
|
307
390
|
|
|
@@ -312,16 +395,16 @@ requests and are expecting replies.
|
|
|
312
395
|
manual [acknowledgment mode](https://www.rabbitmq.com/confirms.html#acknowledgment-modes),
|
|
313
396
|
and Replies are consumed using automatic mode.
|
|
314
397
|
|
|
315
|
-
If incoming message causes an exception, then it is "
|
|
398
|
+
If an incoming message causes an exception, then it is "negatively acknowledged" and requeued. If it
|
|
316
399
|
causes an exception again, it will be discarded.
|
|
317
400
|
|
|
318
|
-
> It is highly recommended to set up a dead letter exchange policy to
|
|
401
|
+
> It is highly recommended to set up a dead letter exchange policy to analyze messages that caused
|
|
319
402
|
> exceptions. Note that in some cases, if the problematic message is a Request, a Consumer will
|
|
320
403
|
> never receive a Reply, and this can result in a prefetch deadlock of a Consumer.
|
|
321
404
|
|
|
322
405
|
See:
|
|
323
406
|
|
|
324
|
-
- [Consumer Acknowledgments and Publisher Confirms](https://www.rabbitmq.com/confirms.html)
|
|
407
|
+
- [Consumer Acknowledgments and Publisher Confirms](https://www.rabbitmq.com/confirms.html)
|
|
325
408
|
- [Negative Acknowledgment and Requeuing of Deliveries](https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue)
|
|
326
409
|
- [Dead Letter Exchanges](https://www.rabbitmq.com/dlx.html)
|
|
327
410
|
|
|
@@ -341,14 +424,14 @@ See:
|
|
|
341
424
|
|
|
342
425
|
[Stop receiving](https://amqp-node.github.io/amqplib/channel_api.html#channel_cancel) new Events and
|
|
343
426
|
Requests.
|
|
427
|
+
Sending Requests, receiving Replies, and emitting Events will still be available.
|
|
344
428
|
|
|
345
429
|
### Disconnection
|
|
346
430
|
|
|
347
431
|
`async IO.close(): void`
|
|
348
432
|
|
|
349
433
|
1. Call `IO.seal()`.
|
|
350
|
-
2. Wait for any outstanding messages to be processed[^2] and acknowledged.
|
|
351
|
-
receiving Replies, and emitting Events will still be available.
|
|
434
|
+
2. Wait for any outstanding messages to be processed[^2] and acknowledged.
|
|
352
435
|
3. Close the connection.
|
|
353
436
|
|
|
354
437
|
[^2]: Therefore, if the underlying connection is lost, `.close()` will only be completed once the
|
|
@@ -371,6 +454,13 @@ In these or other similar scenarios, it is recommended to call `IO.seal()` to st
|
|
|
371
454
|
messages, ensure that any code execution that may send outgoing messages is completed before
|
|
372
455
|
calling `IO.close()`.
|
|
373
456
|
|
|
457
|
+
### Reply stream shutdown
|
|
458
|
+
|
|
459
|
+
All current [Reply streams](#reply-streams) of the corresponding Producer or Consumer instance are destroyed when:
|
|
460
|
+
|
|
461
|
+
- the `IO.seal` function is called on the Consumer
|
|
462
|
+
- the `IO.close` function is called on the Producer
|
|
463
|
+
|
|
374
464
|
## Diagnostics
|
|
375
465
|
|
|
376
466
|
`IO` emits events for testing, diagnostics, or logging purposes.
|