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.
Files changed (67) hide show
  1. package/.github/dependabot.yml +2 -2
  2. package/.github/workflows/codeql.yml +1 -1
  3. package/.github/workflows/release.yaml +16 -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/events.feature +5 -0
  11. package/features/readme.md +2 -2
  12. package/features/recovery.feature +0 -3
  13. package/features/rpc.feature +15 -10
  14. package/features/rpc.streams.feature +81 -0
  15. package/features/rpc.streams.shards.feature +37 -0
  16. package/features/steps/.test/context.test.js +1 -1
  17. package/features/steps/.test/events.test.js +2 -2
  18. package/features/steps/.test/rpc.test.js +1 -1
  19. package/features/steps/.types/context.d.ts +9 -1
  20. package/features/steps/common.js +12 -0
  21. package/features/steps/context.js +9 -2
  22. package/features/steps/events.js +29 -0
  23. package/features/steps/rpc.js +273 -0
  24. package/lerna.json +1 -2
  25. package/package.json +2 -2
  26. package/readme.md +159 -31
  27. package/source/.io/ReplyPipe.js +158 -0
  28. package/source/.io/ReplyStream.js +142 -0
  29. package/source/.io/const.js +10 -0
  30. package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
  31. package/source/.io/index.js +5 -1
  32. package/source/attributes/failsafe.js +34 -0
  33. package/source/attributes/failsafe.test.js +133 -0
  34. package/source/attributes/index.js +13 -0
  35. package/source/attributes/lazy.js +79 -0
  36. package/source/attributes/lazy.test.js +299 -0
  37. package/source/attributes/memo.js +13 -0
  38. package/source/attributes/memo.test.js +36 -0
  39. package/source/attributes/recall.js +42 -0
  40. package/source/attributes/recall.test.js +92 -0
  41. package/source/attributes/track.js +32 -0
  42. package/source/attributes/track.test.js +133 -0
  43. package/source/channel.js +33 -18
  44. package/source/connect.js +2 -2
  45. package/source/connection.js +4 -11
  46. package/source/emitter.js +13 -0
  47. package/source/encode.js +1 -1
  48. package/source/events.js +3 -3
  49. package/source/io.js +184 -48
  50. package/source/pipeline.js +43 -0
  51. package/source/shards/channel.js +28 -12
  52. package/source/shards/connection.js +2 -2
  53. package/test/channel.test.js +9 -1
  54. package/test/encode.test.js +2 -2
  55. package/test/encodings.js +1 -1
  56. package/test/io.diagnostics.test.js +3 -3
  57. package/test/io.emit.test.js +18 -3
  58. package/test/io.request.test.js +45 -23
  59. package/test/pipeline.test.js +81 -0
  60. package/test/shards/channel.test.js +39 -3
  61. package/test/shards/connection.test.js +1 -1
  62. package/types/channel.d.ts +13 -11
  63. package/types/connection.d.ts +3 -3
  64. package/types/diagnostic.d.ts +4 -3
  65. package/types/encoding.d.ts +2 -2
  66. package/types/index.d.ts +4 -4
  67. package/types/io.d.ts +40 -32
@@ -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
@@ -1,5 +1,4 @@
1
1
  {
2
- "useWorkspaces": true,
3
- "version": "0.8.0",
2
+ "version": "0.10.0",
4
3
  "conventionalCommits": true
5
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.8.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
- 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
- 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).
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 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
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, [encoding: string]): 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, [group: string], consumer): void`
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
- 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)
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, [encoding: string]): void`
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 [durable](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue).
274
- - Queues for Replies are *exclusive*.
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 "negative acknowledged" and requeued. If it
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 analyse messages that caused
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. Sending Requests,
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. The shard number corresponds to the number of the argument
368
- used in the `connect` function call.
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