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.
Files changed (60) hide show
  1. package/.github/dependabot.yml +2 -2
  2. package/.github/workflows/codeql.yml +1 -1
  3. package/.github/workflows/release.yaml +14 -15
  4. package/docs/reply-stream-topology-dark.jpg +0 -0
  5. package/docs/reply-stream-topology-light.jpg +0 -0
  6. package/examples/readme.md +19 -3
  7. package/examples/streams/consumer.js +36 -0
  8. package/examples/streams/producer.js +44 -0
  9. package/features/backpressure.feature +2 -2
  10. package/features/readme.md +2 -2
  11. package/features/rpc.streams.feature +81 -0
  12. package/features/rpc.streams.shards.feature +37 -0
  13. package/features/steps/.test/context.test.js +1 -1
  14. package/features/steps/.test/events.test.js +1 -1
  15. package/features/steps/.types/context.d.ts +9 -1
  16. package/features/steps/broker.js +2 -0
  17. package/features/steps/common.js +12 -0
  18. package/features/steps/context.js +9 -2
  19. package/features/steps/rpc.js +235 -0
  20. package/lerna.json +1 -2
  21. package/package.json +3 -2
  22. package/readme.md +136 -46
  23. package/source/.io/ReplyPipe.js +158 -0
  24. package/source/.io/ReplyStream.js +139 -0
  25. package/source/.io/const.js +10 -0
  26. package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
  27. package/source/.io/index.js +5 -1
  28. package/source/attributes/failsafe.js +34 -0
  29. package/source/attributes/failsafe.test.js +133 -0
  30. package/source/attributes/index.js +13 -0
  31. package/source/attributes/lazy.js +79 -0
  32. package/source/attributes/lazy.test.js +299 -0
  33. package/source/attributes/memo.js +13 -0
  34. package/source/attributes/memo.test.js +36 -0
  35. package/source/attributes/recall.js +42 -0
  36. package/source/attributes/recall.test.js +92 -0
  37. package/source/attributes/track.js +32 -0
  38. package/source/attributes/track.test.js +133 -0
  39. package/source/channel.js +31 -18
  40. package/source/connect.js +2 -2
  41. package/source/connection.js +4 -11
  42. package/source/emitter.js +13 -0
  43. package/source/encode.js +1 -1
  44. package/source/events.js +2 -2
  45. package/source/io.js +187 -50
  46. package/source/shards/channel.js +13 -10
  47. package/source/shards/connection.js +2 -2
  48. package/test/channel.test.js +3 -1
  49. package/test/encode.test.js +2 -2
  50. package/test/encodings.js +1 -1
  51. package/test/io.diagnostics.test.js +2 -2
  52. package/test/io.request.test.js +4 -23
  53. package/test/shards/channel.test.js +1 -1
  54. package/test/shards/connection.test.js +1 -1
  55. package/types/channel.d.ts +13 -11
  56. package/types/connection.d.ts +3 -3
  57. package/types/diagnostic.d.ts +4 -3
  58. package/types/encoding.d.ts +2 -2
  59. package/types/index.d.ts +4 -4
  60. package/types/io.d.ts +33 -40
@@ -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
@@ -1,5 +1,4 @@
1
1
  {
2
- "useWorkspaces": true,
3
- "version": "0.9.0",
2
+ "version": "0.10.1",
4
3
  "conventionalCommits": true
5
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.9.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
- Distributed system communications over [AMQP](https://github.com/amqp-node/amqplib) for
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 generated by the `producer` is then encoded and sent
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, [encoding: string]): 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, [group: string], consumer): void`
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
- await io.consume('numbers_added', 'logger', ({ a, b }) => {
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, [encoding: string]): void`
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 [durable](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue).
304
- - Queues for Replies are *exclusive*.
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 "negative acknowledged" and requeued. If it
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 analyse messages that caused
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. Sending Requests,
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.