comq 0.8.0 → 0.10.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/dependabot.yml +2 -2
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release.yaml +16 -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/events.feature +5 -0
- package/features/readme.md +2 -2
- package/features/recovery.feature +0 -3
- package/features/rpc.feature +15 -10
- 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 +2 -2
- package/features/steps/.test/rpc.test.js +1 -1
- package/features/steps/.types/context.d.ts +9 -1
- package/features/steps/common.js +12 -0
- package/features/steps/context.js +9 -2
- package/features/steps/events.js +29 -0
- package/features/steps/rpc.js +273 -0
- package/lerna.json +1 -2
- package/package.json +2 -2
- package/readme.md +159 -31
- package/source/.io/ReplyPipe.js +158 -0
- package/source/.io/ReplyStream.js +142 -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 +33 -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 +3 -3
- package/source/io.js +184 -48
- package/source/pipeline.js +43 -0
- package/source/shards/channel.js +28 -12
- package/source/shards/connection.js +2 -2
- package/test/channel.test.js +9 -1
- package/test/encode.test.js +2 -2
- package/test/encodings.js +1 -1
- package/test/io.diagnostics.test.js +3 -3
- package/test/io.emit.test.js +18 -3
- package/test/io.request.test.js +45 -23
- package/test/pipeline.test.js +81 -0
- package/test/shards/channel.test.js +39 -3
- 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 +40 -32
package/features/steps/rpc.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const stream = require('node:stream')
|
|
3
4
|
const assert = require('node:assert')
|
|
4
5
|
const { randomBytes } = require('node:crypto')
|
|
5
6
|
const { parse } = require('@toa.io/yaml')
|
|
6
7
|
const { match, timeout, quantity } = require('@toa.io/generic')
|
|
7
8
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
9
|
+
const { Readable } = require('node:stream')
|
|
8
10
|
|
|
9
11
|
Given('function replying {token} queue:',
|
|
10
12
|
/**
|
|
@@ -19,6 +21,70 @@ Given('function replying {token} queue:',
|
|
|
19
21
|
await this.io.reply(queue, producer)
|
|
20
22
|
})
|
|
21
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
|
+
|
|
22
88
|
Given('a producer replying {token} queue',
|
|
23
89
|
/**
|
|
24
90
|
* @param {string} queue
|
|
@@ -70,6 +136,37 @@ When('the consumer sends a request to the {token} queue',
|
|
|
70
136
|
await send.call(this, queue, payload)
|
|
71
137
|
})
|
|
72
138
|
|
|
139
|
+
When('the consumer fetches 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 fetches 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} fetches 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
|
+
|
|
73
170
|
Then('the consumer receives the reply:',
|
|
74
171
|
/**
|
|
75
172
|
* @param {string} yaml
|
|
@@ -106,6 +203,162 @@ Then('the consumer does not receive the reply',
|
|
|
106
203
|
assert.equal(reply, undefined, 'The reply was received')
|
|
107
204
|
})
|
|
108
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
|
+
|
|
324
|
+
When('the consumer sends {quantity} requests to the {token} queue as a stream',
|
|
325
|
+
/**
|
|
326
|
+
* @param {string} amountQ
|
|
327
|
+
* @param {string} queue
|
|
328
|
+
* @this {comq.features.Context}
|
|
329
|
+
*/
|
|
330
|
+
async function (amountQ, queue) {
|
|
331
|
+
const amount = quantity(amountQ)
|
|
332
|
+
|
|
333
|
+
function * generate () {
|
|
334
|
+
for (let i = 0; i < amount; i++) {
|
|
335
|
+
yield randomBytes(8)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const input = stream.Readable.from(generate())
|
|
340
|
+
|
|
341
|
+
await send.call(this, queue, input)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
Then('the consumer receives {quantity} replies as a stream',
|
|
345
|
+
/**
|
|
346
|
+
* @this {comq.features.Context}
|
|
347
|
+
*/
|
|
348
|
+
async function (amountQ) {
|
|
349
|
+
const expected = quantity(amountQ)
|
|
350
|
+
const output = await this.reply
|
|
351
|
+
|
|
352
|
+
let actual = 0
|
|
353
|
+
|
|
354
|
+
// eslint-disable-next-line no-unused-vars
|
|
355
|
+
for await (const _ of output) {
|
|
356
|
+
actual++
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
assert.equal(actual, expected, 'The amount of replies mismatches')
|
|
360
|
+
})
|
|
361
|
+
|
|
109
362
|
Given('I\'m sending {quantity}B requests to the {token} queue at {quantity}Hz',
|
|
110
363
|
/**
|
|
111
364
|
* @param {string} bytesQ
|
|
@@ -145,3 +398,23 @@ async function send (queue, payload) {
|
|
|
145
398
|
|
|
146
399
|
this.reply = this.io.request(queue, payload)
|
|
147
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.fetch(queue, payload)
|
|
412
|
+
} else {
|
|
413
|
+
this.streams[number] = await this.io.fetch(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.0",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"msgpackr": "^1.9.5"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
|
-
"prepare": "husky install",
|
|
48
|
+
"prepare": "echo run: husky install",
|
|
49
49
|
"test": "standard && jest",
|
|
50
50
|
"features": "cucumber-js",
|
|
51
51
|
"lint": "standard --fix --verbose | snazzy"
|
package/readme.md
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
- [Dynamic topology](#topology)
|
|
9
|
+
- [Request](#request)-[reply](#reply) (RPC)
|
|
10
|
+
- Events ([pub](#emission)/[sub](#consumption))
|
|
11
|
+
- [Pipelines](#pipelines)
|
|
12
|
+
- [Reply streams](#reply-streams)
|
|
13
|
+
- [Content encoding](#encoding)
|
|
14
|
+
- [Flow control](#flow-control) and back pressure handling
|
|
15
|
+
- [Consumer acknowledgments](#messages) and [publisher confirms](#channels)
|
|
16
|
+
- [Poison message handling](#messages)
|
|
17
|
+
- [Connection tolerance](#connection-tolerance) and broker restart resilience
|
|
18
|
+
- [Sharded connection](#sharded-connection) :rocket:
|
|
19
|
+
- [Singleton connection](#singleton-connection)
|
|
20
|
+
- [Graceful shutdown](#graceful-shutdown)
|
|
21
|
+
|
|
22
|
+
> CommonJS, ECMAScript, and TypeScript compatible (types included).
|
|
20
23
|
|
|
21
24
|
## TL;DR
|
|
22
25
|
|
|
23
|
-
[Code examples](examples)
|
|
26
|
+
- [Code examples](examples)
|
|
27
|
+
- [Scenarios](features)
|
|
24
28
|
|
|
25
29
|
## Installation
|
|
26
30
|
|
|
@@ -73,7 +77,7 @@ value as in the Request.
|
|
|
73
77
|
`producer` function's signature is `async? (message: any): any`
|
|
74
78
|
|
|
75
79
|
Assert a `queue` and start consuming Requests. Received messages are decoded and the resulting
|
|
76
|
-
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
|
|
77
81
|
back to the queue specified in the `replyTo` property of the Request, along with a `correlationId`
|
|
78
82
|
that has the same value as in the Request.
|
|
79
83
|
|
|
@@ -96,7 +100,7 @@ await io.reply('add_numbers', ({ a, b }) => (a + b))
|
|
|
96
100
|
|
|
97
101
|
## Request
|
|
98
102
|
|
|
99
|
-
`async IO.request(queue: string, payload: any,
|
|
103
|
+
`async IO.request(queue: string, payload: any, encoding?: string): any`
|
|
100
104
|
|
|
101
105
|
Send encoded Request message with `replyTo` and `correlationId` properties set and
|
|
102
106
|
return decoded Reply content.
|
|
@@ -111,7 +115,7 @@ const sum = await io.request('add_numbers', { a: 1, b: 2 })
|
|
|
111
115
|
|
|
112
116
|
## Consumption
|
|
113
117
|
|
|
114
|
-
`async IO.consume(exchange: string,
|
|
118
|
+
`async IO.consume(exchange: string, group?: string, consumer): void`
|
|
115
119
|
|
|
116
120
|
`consumer` function's signature is `async? (payload: any): void`
|
|
117
121
|
|
|
@@ -130,14 +134,20 @@ auto-generated name.
|
|
|
130
134
|
### Example
|
|
131
135
|
|
|
132
136
|
```javascript
|
|
133
|
-
|
|
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)
|
|
134
145
|
console.log(`${a} was added to ${b}`)
|
|
135
|
-
})
|
|
136
146
|
```
|
|
137
147
|
|
|
138
148
|
## Emission
|
|
139
149
|
|
|
140
|
-
`async IO.emit(exchange: string, payload: any,
|
|
150
|
+
`async IO.emit(exchange: string, payload: any, encoding?: string): void`
|
|
141
151
|
|
|
142
152
|
Publish encoded Event to the `exchange`.
|
|
143
153
|
|
|
@@ -151,6 +161,111 @@ asserted.
|
|
|
151
161
|
await io.emit('numbers_added', { a: 1, b: 2 })
|
|
152
162
|
```
|
|
153
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.fetch` function:
|
|
206
|
+
|
|
207
|
+
`async IO.fetch(queue: string, payload: any, encoding?: string): Readable`
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
const stream = await io.fetch('get_numbers', { amount: 10 })
|
|
211
|
+
|
|
212
|
+
for await (const number of stream)
|
|
213
|
+
console.log(number)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Stream topology
|
|
217
|
+
|
|
218
|
+
<picture>
|
|
219
|
+
<source media="(prefers-color-scheme: dark)" srcset="./docs/reply-stream-topology-dark.jpg">
|
|
220
|
+
<img alt="Reply topology" width="500" height="411" src="./docs/reply-stream-topology-light.jpg">
|
|
221
|
+
</picture>
|
|
222
|
+
|
|
223
|
+
On the first call of the `IO.fetch` for each request queue, an exclusive queue for replies is asserted (a stream queue).
|
|
224
|
+
|
|
225
|
+
When the producer function of `IO.reply` returns an Iterator for the first time across all request queues,
|
|
226
|
+
a control queue is asserted on the [Reply channel](#channels)
|
|
227
|
+
using the [reply topology](#exchanges-and-queues).
|
|
228
|
+
When the Consumer destroys the Reply stream, a stream cancellation message is sent to the Producer's control queue.
|
|
229
|
+
|
|
230
|
+
### Stream control
|
|
231
|
+
|
|
232
|
+
Upon receiving a request, the Producer sends a confirmation message to the `replyTo` queue.
|
|
233
|
+
If the underlying connection is lost before the Consumer receives the confirmation message,
|
|
234
|
+
the request will be retransmitted upon reconnection.
|
|
235
|
+
|
|
236
|
+
A heartbeat message is sent to the `replyTo` queue whenever a Reply stream idles for 5 seconds.
|
|
237
|
+
If the Consumer of the reply stream doesn't receive a reply or a heartbeat message for 12 seconds, the stream returned
|
|
238
|
+
by `IO.fetch` is destroyed.
|
|
239
|
+
These intervals are not configurable.
|
|
240
|
+
|
|
241
|
+
An "end stream" message is sent to the `replyTo` queue when the Reply stream is finished.
|
|
242
|
+
|
|
243
|
+
See also [Reply stream shutdown](#reply-stream-shutdown).
|
|
244
|
+
|
|
245
|
+
### Loss of tail
|
|
246
|
+
|
|
247
|
+
:warning:
|
|
248
|
+
|
|
249
|
+
While consuming the Reply stream if the broker connection is lost,
|
|
250
|
+
or if the Consumer crashes or destroys the stream returned by `IO.fetch`,
|
|
251
|
+
some of the values yielded by the Reply stream may be lost.
|
|
252
|
+
|
|
253
|
+
To avoid inconsistency, it is strongly recommended to use the Reply stream only with _safe_ Producers, which do not
|
|
254
|
+
change the application state.
|
|
255
|
+
|
|
256
|
+
### Stream guarantees
|
|
257
|
+
|
|
258
|
+
:warning:
|
|
259
|
+
|
|
260
|
+
The reply topology guarantees
|
|
261
|
+
that the order of yielded values is [preserved](https://www.rabbitmq.com/queues.html#message-ordering).
|
|
262
|
+
At the same time, there is no guarantee that the stream will be transmitted to the end.
|
|
263
|
+
|
|
264
|
+
> When using the [Sharded connection](#sharded-connection), the order of yielded values is maintained through buffering.
|
|
265
|
+
> However, there is a scenario in which some of the yielded values may be lost if a broker crashes.
|
|
266
|
+
> In this case, the Reply stream will be destroyed once the buffer's maximum size is exceeded.
|
|
267
|
+
> Also, buffered control messages can result in [stream idling](#stream-control).
|
|
268
|
+
|
|
154
269
|
## Encoding
|
|
155
270
|
|
|
156
271
|
By default, outgoing message contents are encoded with [msgpack](https://msgpack.org) and
|
|
@@ -270,8 +385,10 @@ requests and are expecting replies.
|
|
|
270
385
|
### Exchanges and queues
|
|
271
386
|
|
|
272
387
|
- Exchanges and queues for Events, and queues for Requests
|
|
273
|
-
are
|
|
274
|
-
- Queues for Replies are
|
|
388
|
+
are _durable_.
|
|
389
|
+
- Queues for Replies are _exclusive_ and _auto deleted_.
|
|
390
|
+
|
|
391
|
+
See [queue assertion options](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue).
|
|
275
392
|
|
|
276
393
|
### Messages
|
|
277
394
|
|
|
@@ -282,16 +399,16 @@ requests and are expecting replies.
|
|
|
282
399
|
manual [acknowledgment mode](https://www.rabbitmq.com/confirms.html#acknowledgment-modes),
|
|
283
400
|
and Replies are consumed using automatic mode.
|
|
284
401
|
|
|
285
|
-
If incoming message causes an exception, then it is "
|
|
402
|
+
If an incoming message causes an exception, then it is "negatively acknowledged" and requeued. If it
|
|
286
403
|
causes an exception again, it will be discarded.
|
|
287
404
|
|
|
288
|
-
> It is highly recommended to set up a dead letter exchange policy to
|
|
405
|
+
> It is highly recommended to set up a dead letter exchange policy to analyze messages that caused
|
|
289
406
|
> exceptions. Note that in some cases, if the problematic message is a Request, a Consumer will
|
|
290
407
|
> never receive a Reply, and this can result in a prefetch deadlock of a Consumer.
|
|
291
408
|
|
|
292
409
|
See:
|
|
293
410
|
|
|
294
|
-
- [Consumer Acknowledgments and Publisher Confirms](https://www.rabbitmq.com/confirms.html)
|
|
411
|
+
- [Consumer Acknowledgments and Publisher Confirms](https://www.rabbitmq.com/confirms.html)
|
|
295
412
|
- [Negative Acknowledgment and Requeuing of Deliveries](https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue)
|
|
296
413
|
- [Dead Letter Exchanges](https://www.rabbitmq.com/dlx.html)
|
|
297
414
|
|
|
@@ -311,14 +428,14 @@ See:
|
|
|
311
428
|
|
|
312
429
|
[Stop receiving](https://amqp-node.github.io/amqplib/channel_api.html#channel_cancel) new Events and
|
|
313
430
|
Requests.
|
|
431
|
+
Sending Requests, receiving Replies, and emitting Events will still be available.
|
|
314
432
|
|
|
315
433
|
### Disconnection
|
|
316
434
|
|
|
317
435
|
`async IO.close(): void`
|
|
318
436
|
|
|
319
437
|
1. Call `IO.seal()`.
|
|
320
|
-
2. Wait for any outstanding messages to be processed[^2] and acknowledged.
|
|
321
|
-
receiving Replies, and emitting Events will still be available.
|
|
438
|
+
2. Wait for any outstanding messages to be processed[^2] and acknowledged.
|
|
322
439
|
3. Close the connection.
|
|
323
440
|
|
|
324
441
|
[^2]: Therefore, if the underlying connection is lost, `.close()` will only be completed once the
|
|
@@ -341,6 +458,13 @@ In these or other similar scenarios, it is recommended to call `IO.seal()` to st
|
|
|
341
458
|
messages, ensure that any code execution that may send outgoing messages is completed before
|
|
342
459
|
calling `IO.close()`.
|
|
343
460
|
|
|
461
|
+
### Reply stream shutdown
|
|
462
|
+
|
|
463
|
+
All current [Reply streams](#reply-streams) of the corresponding Producer or Consumer instance are destroyed when:
|
|
464
|
+
|
|
465
|
+
- the `IO.seal` function is called on the Consumer
|
|
466
|
+
- the `IO.close` function is called on the Producer
|
|
467
|
+
|
|
344
468
|
## Diagnostics
|
|
345
469
|
|
|
346
470
|
`IO` emits events for testing, diagnostics, or logging purposes.
|
|
@@ -362,12 +486,16 @@ Subscribe to one of the diagnostic events:
|
|
|
362
486
|
exceptions. Channel type,
|
|
363
487
|
raw [amqp message object](https://amqp-node.github.io/amqplib/channel_api.html#channel_consume)
|
|
364
488
|
and the exception are passed as arguments.
|
|
489
|
+
- `pause`: channel is paused. Channel type is passed.
|
|
490
|
+
- `resume`: channel is resumed. Channel type is passed.
|
|
365
491
|
|
|
366
492
|
In the case of a [sharded connection](#sharded-connection), an additional argument specifying the
|
|
367
|
-
shard number will be passed to listeners.
|
|
368
|
-
|
|
493
|
+
shard number will be passed to listeners.
|
|
494
|
+
This is applicable except for the `pause` and `resume` events,
|
|
495
|
+
which are emitted when the associated channels are paused or resumed across all shards.
|
|
496
|
+
The shard number corresponds to the position of the argument used in the `connect` function call.
|
|
369
497
|
|
|
370
|
-
[^3]: As [`connect`](#connect) function returns an instance of `IO` *after* the connection has been
|
|
498
|
+
[^3]: As the [`connect`](#connect) function returns an instance of `IO` *after* the connection has been
|
|
371
499
|
established, there is no way to capture the initial `open` event.
|
|
372
500
|
|
|
373
501
|
### Example
|