comq 0.10.0 → 0.10.2

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.
@@ -21,7 +21,7 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - name: Setup Node
24
- uses: actions/setup-node@v3
24
+ uses: actions/setup-node@v4
25
25
  with:
26
26
  node-version: 16
27
27
  registry-url: 'https://registry.npmjs.org'
@@ -5,14 +5,14 @@ Feature: Reply streams
5
5
  And heartbeat interval is set to 100ms
6
6
  And idle timeout is set to 150ms
7
7
 
8
- Scenario: Fetching a stream
8
+ Scenario: Requesting a stream
9
9
  Given function replying `get_numbers` queue:
10
10
  """
11
11
  function * ({ limit }) {
12
12
  for (let i = 0; i < limit; i++) yield i
13
13
  }
14
14
  """
15
- When the consumer fetches a stream with the following request to the `get_numbers` queue:
15
+ When the consumer requests a stream with the following request to the `get_numbers` queue:
16
16
  """yaml
17
17
  limit: 5
18
18
  """
@@ -21,11 +21,11 @@ Feature: Reply streams
21
21
  [0, 1, 2, 3, 4]
22
22
  """
23
23
 
24
- Scenario: Fetching streams concurrently
24
+ Scenario: Requesting streams concurrently
25
25
  Given a number generator with 10ms increasing delay replying `get_numbers` queue
26
- When the consumer0 fetches a stream with request to the `get_numbers` queue
26
+ When the consumer0 requests a stream with request to the `get_numbers` queue
27
27
  And the consumer0 receives the stream
28
- And the consumer1 fetches a stream with request to the `get_numbers` queue
28
+ And the consumer1 requests a stream with request to the `get_numbers` queue
29
29
  And the consumer1 receives the stream
30
30
  And after 300ms
31
31
  Then the consumer0 has received the stream:
@@ -39,7 +39,7 @@ Feature: Reply streams
39
39
 
40
40
  Scenario: Interrupting a stream
41
41
  Given a number generator with 20ms increasing delay replying `get_numbers` queue
42
- When the consumer fetches a stream with request to the `get_numbers` queue
42
+ When the consumer requests a stream with request to the `get_numbers` queue
43
43
  And the consumer interrupts the stream after 3 replies
44
44
  # feedback message must be delivered
45
45
  And after 10ms
@@ -48,7 +48,7 @@ Feature: Reply streams
48
48
  Scenario: Reply stream idle timeout
49
49
  Given heartbeat interval is set to 300ms
50
50
  And a number generator with 100ms increasing delay replying `get_numbers` queue
51
- When the consumer fetches a stream with request to the `get_numbers` queue
51
+ When the consumer requests a stream with request to the `get_numbers` queue
52
52
  Then the consumer receives the stream:
53
53
  """yaml
54
54
  [0, 1, 3]
@@ -59,14 +59,14 @@ Feature: Reply streams
59
59
  Scenario: Reply stream heartbeat
60
60
  # delay will exceed the idle timeout on the 3rd reply
61
61
  Given a number generator with 90ms increasing delay replying `get_numbers` queue
62
- When the consumer fetches a stream with request to the `get_numbers` queue
62
+ When the consumer requests a stream with request to the `get_numbers` queue
63
63
  Then the consumer interrupts the stream after 5 replies
64
64
  And after 200ms
65
65
  Then the generator is destroyed
66
66
 
67
67
  Scenario: Broker crashes while streaming reply
68
68
  Given a number generator with 90ms increasing delay replying `get_numbers` queue
69
- When the consumer fetches a stream with request to the `get_numbers` queue
69
+ When the consumer requests a stream with request to the `get_numbers` queue
70
70
  Then the consumer receives the stream
71
71
  And after 300ms
72
72
  Then the broker has crashed
@@ -5,14 +5,14 @@ Feature: Reply streams over shards
5
5
  And heartbeat interval is set to 100ms
6
6
  And idle timeout is set to 150ms
7
7
 
8
- Scenario: Fetching a stream using sharded connection
8
+ Scenario: Requesting a stream using sharded connection
9
9
  Given a generator replying `get_numbers` queue:
10
10
  """
11
11
  function * ({ amount }) {
12
12
  for (let i = 0; i < amount; i++) yield i
13
13
  }
14
14
  """
15
- When the consumer fetches a stream with the following request to the `get_numbers` queue:
15
+ When the consumer requests a stream with the following request to the `get_numbers` queue:
16
16
  """yaml
17
17
  amount: 5
18
18
  """
@@ -23,7 +23,7 @@ Feature: Reply streams over shards
23
23
 
24
24
  Scenario: Broker crashes while fetching a stream
25
25
  Given a number generator with 90ms increasing delay replying `get_numbers` queue
26
- When the consumer fetches a stream with request to the `get_numbers` queue
26
+ When the consumer requests a stream with request to the `get_numbers` queue
27
27
  And the consumer receives the stream
28
28
  And after 300ms
29
29
  And the broker has crashed
@@ -32,7 +32,7 @@ Given('one of the brokers is/has {status}',
32
32
  const actions = {
33
33
  up: async (n = 0) => {
34
34
  await execute('docker start comq-rmq-' + n)
35
- await healthy()
35
+ await healthy(n)
36
36
  },
37
37
  down: async (n = 0) => {
38
38
  await execute('docker stop comq-rmq-' + n)
@@ -42,13 +42,13 @@ const actions = {
42
42
  }
43
43
  }
44
44
 
45
- async function healthy () {
45
+ async function healthy (n) {
46
46
  let process
47
47
 
48
48
  do {
49
49
  await timeout(HEALTHCHECK_INTERVAL)
50
50
 
51
- process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-0')
51
+ process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-' + n)
52
52
  } while (process.output !== 'healthy')
53
53
  }
54
54
 
@@ -4,7 +4,7 @@ const stream = require('node:stream')
4
4
  const assert = require('node:assert')
5
5
  const { randomBytes } = require('node:crypto')
6
6
  const { timeout, quantity, match } = require('@toa.io/generic')
7
- const { parse } = require('@toa.io/yaml')
7
+ const { load: parse } = require('js-yaml')
8
8
  const { Given, When, Then } = require('@cucumber/cucumber')
9
9
 
10
10
  Given('(that ){token} is consuming events from the {token} exchange',
@@ -64,6 +64,7 @@ When('an event is emitted to the {token} exchange with properties:',
64
64
 
65
65
  When('a stream of {quantity} events is emitted to the {token} exchange',
66
66
  /**
67
+ * @param {string} amountQ
67
68
  * @param {string} exchange
68
69
  * @this {comq.features.Context}
69
70
  */
@@ -3,7 +3,7 @@
3
3
  const stream = require('node:stream')
4
4
  const assert = require('node:assert')
5
5
  const { randomBytes } = require('node:crypto')
6
- const { parse } = require('@toa.io/yaml')
6
+ const { load: parse } = require('js-yaml')
7
7
  const { match, timeout, quantity } = require('@toa.io/generic')
8
8
  const { Given, When, Then } = require('@cucumber/cucumber')
9
9
  const { Readable } = require('node:stream')
@@ -136,7 +136,7 @@ When('the consumer sends a request to the {token} queue',
136
136
  await send.call(this, queue, payload)
137
137
  })
138
138
 
139
- When('the consumer fetches a stream with the following request to the {token} queue:',
139
+ When('the consumer requests a stream with the following request to the {token} queue:',
140
140
  /**
141
141
  * @param {string} queue
142
142
  * @param {string} yaml
@@ -148,7 +148,7 @@ When('the consumer fetches a stream with the following request to the {token} qu
148
148
  await fetch.call(this, queue, payload)
149
149
  })
150
150
 
151
- When('the consumer fetches a stream with request to the {token} queue',
151
+ When('the consumer requests a stream with request to the {token} queue',
152
152
  /**
153
153
  * @param {string} queue
154
154
  * @this {comq.features.Context}
@@ -157,7 +157,7 @@ When('the consumer fetches a stream with request to the {token} queue',
157
157
  await fetch.call(this, queue, null)
158
158
  })
159
159
 
160
- When('the consumer{number} fetches a stream with request to the {token} queue',
160
+ When('the consumer{number} requests a stream with request to the {token} queue',
161
161
  /**
162
162
  * @param {number} number
163
163
  * @param {string} queue
@@ -408,9 +408,9 @@ async function send (queue, payload) {
408
408
  */
409
409
  async function fetch (queue, payload, number) {
410
410
  if (number === undefined) {
411
- this.stream = await this.io.fetch(queue, payload)
411
+ this.stream = await this.io.request(queue, payload)
412
412
  } else {
413
- this.streams[number] = await this.io.fetch(queue, payload)
413
+ this.streams[number] = await this.io.request(queue, payload)
414
414
  this.streamsValues[number] = []
415
415
  this.streamsEnded[number] = false
416
416
  }
package/lerna.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.10.0",
2
+ "version": "0.10.2",
3
3
  "conventionalCommits": true
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
@@ -27,27 +27,28 @@
27
27
  "main": "source/index.js",
28
28
  "types": "types/index.d.ts",
29
29
  "devDependencies": {
30
- "@cucumber/cucumber": "^9.1.2",
30
+ "@cucumber/cucumber": "^10.0.1",
31
31
  "@toa.io/command": "^0.4.0",
32
- "@toa.io/tomato": "^0.4.0",
33
- "@toa.io/yaml": "^0.4.0",
34
- "@types/amqplib": "^0.10.1",
35
- "@types/jest": "^29.5.2",
32
+ "@toa.io/generic": "^1.0.0-alpha.0",
33
+ "@types/amqplib": "^0.10.3",
34
+ "@types/jest": "^29.5.8",
36
35
  "husky": "^8.0.3",
37
36
  "jest": "^29.5.0",
37
+ "js-yaml": "^4.1.0",
38
38
  "randomstring": "^1.3.0",
39
39
  "snazzy": "^9.0.0",
40
40
  "standard": "^17.1.0"
41
41
  },
42
42
  "dependencies": {
43
- "@toa.io/generic": "^0.4.0",
44
43
  "amqplib": "^0.10.3",
45
- "msgpackr": "^1.9.5"
44
+ "msgpackr": "^1.9.5",
45
+ "promex": "^0.1.1",
46
+ "reretry": "^0.1.0"
46
47
  },
47
48
  "scripts": {
48
49
  "prepare": "echo run: husky install",
49
50
  "test": "standard && jest",
50
- "features": "cucumber-js",
51
+ "features": "cucumber-js --fail-fast",
51
52
  "lint": "standard --fix --verbose | snazzy"
52
53
  },
53
54
  "standard": {
package/readme.md CHANGED
@@ -137,12 +137,6 @@ auto-generated name.
137
137
  // with a consumer function
138
138
  await io.consume('numbers_added', 'logger',
139
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)
145
- console.log(`${a} was added to ${b}`)
146
140
  ```
147
141
 
148
142
  ## Emission
@@ -202,12 +196,10 @@ await io.reply('get_numbers', function * ({ amount }) {
202
196
  })
203
197
  ```
204
198
 
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`
199
+ The Reply stream may be consumed by using the `IO.request` function:
208
200
 
209
201
  ```javascript
210
- const stream = await io.fetch('get_numbers', { amount: 10 })
202
+ const stream = await io.request('get_numbers', { amount: 10 })
211
203
 
212
204
  for await (const number of stream)
213
205
  console.log(number)
@@ -220,8 +212,6 @@ for await (const number of stream)
220
212
  <img alt="Reply topology" width="500" height="411" src="./docs/reply-stream-topology-light.jpg">
221
213
  </picture>
222
214
 
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
215
  When the producer function of `IO.reply` returns an Iterator for the first time across all request queues,
226
216
  a control queue is asserted on the [Reply channel](#channels)
227
217
  using the [reply topology](#exchanges-and-queues).
@@ -235,7 +225,7 @@ the request will be retransmitted upon reconnection.
235
225
 
236
226
  A heartbeat message is sent to the `replyTo` queue whenever a Reply stream idles for 5 seconds.
237
227
  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.
228
+ by `IO.request` is destroyed.
239
229
  These intervals are not configurable.
240
230
 
241
231
  An "end stream" message is sent to the `replyTo` queue when the Reply stream is finished.
@@ -247,7 +237,7 @@ See also [Reply stream shutdown](#reply-stream-shutdown).
247
237
  :warning:
248
238
 
249
239
  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`,
240
+ or if the Consumer crashes or destroys the stream returned by `IO.request`,
251
241
  some of the values yielded by the Reply stream may be lost.
252
242
 
253
243
  To avoid inconsistency, it is strongly recommended to use the Reply stream only with _safe_ Producers, which do not
@@ -354,9 +344,6 @@ and it will be closed when the last instance of `IO` using that connection is [d
354
344
  Topology is designed to deliver maximum performance while ensuring that the **at least once**
355
345
  guarantee provided by RabbitMQ is maintained.
356
346
 
357
- > Topology design assumes that unanswered Requests and unconfirmed Events will be retransmitted
358
- > upon reconnection.
359
-
360
347
  ### Dynamic
361
348
 
362
349
  Static topology refers to the process of defining the complete topology declaration along with the
@@ -1,11 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { IDLE_INTERVAL, control } = require('./const')
4
-
5
3
  const { Readable } = require('node:stream')
4
+ const { Promex } = require('promex')
5
+ const { IDLE_INTERVAL, control } = require('./const')
6
6
 
7
7
  class ReplyStream extends Readable {
8
- #confirmation
8
+ confirmation = new Promex()
9
9
 
10
10
  /** @type {ReturnType<setTimeout> | null} */
11
11
  #timeout = null
@@ -26,8 +26,8 @@ class ReplyStream extends Readable {
26
26
 
27
27
  #buffered = 0
28
28
 
29
- /** @type {Record<number, unknown>} */
30
- #queue = {}
29
+ /** @type {Map<number, unknown>} */
30
+ #queue = new Map()
31
31
 
32
32
  /**
33
33
  * @param {comq.Request} request
@@ -36,14 +36,12 @@ class ReplyStream extends Readable {
36
36
  constructor (request, reply) {
37
37
  super({ objectMode: true })
38
38
 
39
- this.#confirmation = request.reply
40
39
  this.#emitter = request.emitter
41
40
  this.#correlationId = request.properties.correlationId
42
41
  this.#idleInterval = global['COMQ_TESTING_IDLE_INTERVAL'] || IDLE_INTERVAL
43
42
  this.#reply = reply
44
43
 
45
- this.#emitter.on(this.#correlationId, this._arrange.bind(this))
46
- this.#confirmation.catch(this._clear.bind(this))
44
+ this.#emitter.on(this.#correlationId, this.arrange.bind(this))
47
45
  }
48
46
 
49
47
  _destroy (error, callback) {
@@ -59,9 +57,8 @@ class ReplyStream extends Readable {
59
57
  /**
60
58
  * @param {unknown} payload
61
59
  * @param {comq.amqp.Properties} properties
62
- * @private
63
60
  */
64
- _arrange (payload, properties) {
61
+ arrange (payload, properties) {
65
62
  if (properties.headers.index !== this.#index) {
66
63
  this._buffer(payload, properties)
67
64
 
@@ -73,12 +70,12 @@ class ReplyStream extends Readable {
73
70
  if (this.#buffered > 0) {
74
71
  let message
75
72
 
76
- while ((message = this.#queue[this.#index])) {
77
- delete this.#queue[this.#index]
73
+ while ((message = this.#queue.get(this.#index))) {
74
+ this.#queue.delete(this.#index)
78
75
  this._add(message.payload, message.properties)
79
76
  }
80
77
 
81
- this.#buffered = Object.keys(this.#queue).length
78
+ this.#buffered = this.#queue.size
82
79
  }
83
80
  }
84
81
 
@@ -101,7 +98,7 @@ class ReplyStream extends Readable {
101
98
  if (this.#buffered > MAX_BUFFER_SIZE) this.destroy()
102
99
 
103
100
  this.#buffered++
104
- this.#queue[properties.headers.index] = { payload, properties }
101
+ this.#queue.set(properties.headers.index, { payload, properties })
105
102
  }
106
103
 
107
104
  /**
@@ -113,7 +110,7 @@ class ReplyStream extends Readable {
113
110
  switch (message) {
114
111
  case control.ok:
115
112
  this.#control = { properties }
116
- this.#confirmation.resolve()
113
+ this.confirmation.resolve()
117
114
  break
118
115
  case control.heartbeat:
119
116
  break
package/source/channel.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { promex, timeout } = require('@toa.io/generic')
3
+ const { Promex } = require('promex')
4
4
  const { failsafe, lazy, recall } = require('./attributes')
5
5
  const emitter = require('./emitter')
6
6
 
@@ -25,16 +25,16 @@ class Channel {
25
25
  /** @type {string[]} */
26
26
  #tags = []
27
27
 
28
- /** @type {toa.generic.Promex | null} */
28
+ /** @type {Promex | null} */
29
29
  #paused = null
30
30
 
31
31
  /** @type {boolean} */
32
32
  #sealed = false
33
33
 
34
- /** @type {toa.generic.Promex} */
35
- #recovery = promex()
34
+ /** @type {Promex} */
35
+ #recovery = new Promex()
36
36
 
37
- /** @type {Set<toa.generic.Promex>} */
37
+ /** @type {Set<Promex>} */
38
38
  #confirmations = new Set()
39
39
 
40
40
  #diagnostics = emitter.create()
@@ -149,10 +149,11 @@ class Channel {
149
149
 
150
150
  for (const confirmation of this.#confirmations) confirmation.reject(INTERRUPTION)
151
151
 
152
- await timeout(0) // handle interruptions
152
+ // handle interruptions
153
+ await new Promise(resolve => setTimeout(resolve, 0))
153
154
 
154
155
  this.#recovery.resolve()
155
- this.#recovery = promex()
156
+ this.#recovery = new Promex()
156
157
  this.#diagnostics.emit('recover')
157
158
  }
158
159
 
@@ -222,10 +223,10 @@ class Channel {
222
223
  }
223
224
 
224
225
  /**
225
- * @return {toa.generic.Promex}
226
+ * @return {Promex}
226
227
  */
227
228
  #confirmation () {
228
- const confirmation = promex()
229
+ const confirmation = new Promex()
229
230
 
230
231
  this.#confirmations.add(confirmation)
231
232
 
@@ -292,7 +293,7 @@ class Channel {
292
293
  #pause () {
293
294
  if (this.#paused !== null) return
294
295
 
295
- this.#paused = promex()
296
+ this.#paused = new Promex()
296
297
  this.#diagnostics.emit('flow')
297
298
  this.#diagnostics.emit('pause')
298
299
  }
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const amqp = require('amqplib')
4
- const { retry, promex } = require('@toa.io/generic')
4
+ const { Promex } = require('promex')
5
+ const { retry } = require('reretry')
5
6
 
6
7
  const { failsafe } = require('./attributes')
7
8
  const presets = require('./topology')
@@ -21,8 +22,8 @@ class Connection {
21
22
  /** @type {comq.Channel[]} */
22
23
  #channels = []
23
24
 
24
- /** @type {toa.generic.Promex} */
25
- #recovery = promex()
25
+ /** @type {Promex} */
26
+ #recovery = new Promex()
26
27
 
27
28
  /** @type {boolean} */
28
29
  #running = false
@@ -37,7 +38,7 @@ class Connection {
37
38
  }
38
39
 
39
40
  async open () {
40
- await retry(this.#open, RETRY)
41
+ await retry(this.#open)
41
42
 
42
43
  this.#running = true
43
44
  }
@@ -69,14 +70,11 @@ class Connection {
69
70
  this.#diagnostics.on(event, listener)
70
71
  }
71
72
 
72
- /**
73
- * @type {toa.generic.retry.Task}
74
- */
75
73
  #open = async (retry) => {
76
74
  try {
77
75
  this.#connection = await amqp.connect(this.#url)
78
76
  } catch (exception) {
79
- if (this.#transient(exception)) return retry()
77
+ if (this.#transient(exception)) return retry
80
78
  else throw exception
81
79
  }
82
80
 
@@ -90,7 +88,7 @@ class Connection {
90
88
  for (const channel of this.#channels) await channel.recover(this.#connection)
91
89
 
92
90
  this.#recovery.resolve()
93
- this.#recovery = promex()
91
+ this.#recovery = new Promex()
94
92
  }
95
93
 
96
94
  /**
@@ -117,9 +115,4 @@ class Connection {
117
115
 
118
116
  function noop () {}
119
117
 
120
- /** @type {toa.generic.retry.Options} */
121
- const RETRY = {
122
- retries: Infinity
123
- }
124
-
125
118
  exports.Connection = Connection
package/source/io.js CHANGED
@@ -3,7 +3,8 @@
3
3
  const stream = require('node:stream')
4
4
  const { EventEmitter } = require('node:events')
5
5
  const { randomBytes } = require('node:crypto')
6
- const { promex, timeout } = require('@toa.io/generic')
6
+ const { setTimeout } = require('node:timers/promises')
7
+ const { Promex } = require('promex')
7
8
  const { memo, failsafe, lazy, track } = require('./attributes')
8
9
 
9
10
  const { decode } = require('./decode')
@@ -32,9 +33,9 @@ class IO {
32
33
  #emitters = new Map()
33
34
 
34
35
  /** @type {comq.ReplyEmitter | null} */
35
- #feedback = null
36
+ #control = null
36
37
 
37
- /** @type {Set<toa.generic.Promex>} */
38
+ /** @type {Set<Promex>} */
38
39
  #pendingReplies = new Set()
39
40
 
40
41
  /** @type {Set<comq.Destroyable>} */
@@ -86,33 +87,12 @@ class IO {
86
87
  )
87
88
  }
88
89
 
89
- const { buffer, properties, emitter, reply } = this.#createRequest(queue, payload, encoding)
90
-
91
- emitter.once(properties.correlationId, reply.resolve)
92
-
93
- await this.#requests.send(queue, buffer, properties)
94
-
95
- return reply
96
- }))
97
-
98
- fetch = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
99
- failsafe(this, this.#recover,
100
- /**
101
- * @param {string} queue
102
- * @param {any} payload
103
- * @param {comq.Encoding} [encoding]
104
- * @returns {Promise<Readable>}
105
- */
106
- async (queue, payload, encoding) => {
107
90
  const request = this.#createRequest(queue, payload, encoding)
108
- const stream = new io.ReplyStream(request, this.#reply.bind(this))
109
-
110
- this.#addReplyStream(stream)
91
+ const reply = this.#createReply(request)
111
92
 
112
93
  await this.#requests.send(queue, request.buffer, request.properties)
113
- await request.reply // wait for the confirmation
114
94
 
115
- return stream
95
+ return reply
116
96
  }))
117
97
 
118
98
  consume = lazy(this, this.#createEventChannel,
@@ -247,9 +227,9 @@ class IO {
247
227
  ? reply
248
228
  : stream.Readable.from(reply)
249
229
 
250
- this.#feedback ??= await this.#createFeedback()
230
+ this.#control ??= await this.#createControl()
251
231
 
252
- const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#feedback,
232
+ const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#control,
253
233
  (message, properties) => this.#reply(request, message, properties))
254
234
 
255
235
  this.#addReplyPipe(pipe)
@@ -291,19 +271,30 @@ class IO {
291
271
  const [buffer, contentType] = this.#encode(payload, encoding)
292
272
  const emitter = this.#emitters.get(queue)
293
273
  const correlationId = randomBytes(8).toString('hex')
294
- const reply = this.#createReply()
295
274
 
296
275
  /** @type {comq.amqp.Properties} */
297
276
  const properties = { contentType, correlationId, replyTo: emitter.queue }
298
277
 
299
- return { buffer, emitter, reply, properties }
278
+ return { buffer, emitter, properties }
279
+ }
280
+
281
+ /**
282
+ * @param {comq.Request} request
283
+ * @return {Promex<any>}
284
+ */
285
+ #createReply (request) {
286
+ const reply = this.#createPendingReply()
287
+
288
+ request.emitter.once(request.properties.correlationId, this.#getReplyResolver(request, reply))
289
+
290
+ return reply
300
291
  }
301
292
 
302
293
  /**
303
- * @return {toa.generic.Promex}
294
+ * @return {Promex}
304
295
  */
305
- #createReply () {
306
- const reply = promex()
296
+ #createPendingReply () {
297
+ const reply = new Promex()
307
298
 
308
299
  this.#pendingReplies.add(reply)
309
300
 
@@ -314,11 +305,40 @@ class IO {
314
305
  return reply
315
306
  }
316
307
 
308
+ /**
309
+ * @param {comq.Request} request
310
+ * @param reply
311
+ */
312
+ #getReplyResolver (request, reply) {
313
+ return async (payload, properties) => {
314
+ const isStream = properties.headers?.index !== undefined
315
+
316
+ if (isStream) {
317
+ const stream = this.#createReplyStream(request, payload, properties)
318
+
319
+ await stream.confirmation
320
+
321
+ reply.resolve(stream)
322
+ } else {
323
+ reply.resolve(payload)
324
+ }
325
+ }
326
+ }
327
+
328
+ #createReplyStream (request, payload, properties) {
329
+ const stream = new io.ReplyStream(request, this.#reply.bind(this))
330
+
331
+ stream.arrange(payload, properties)
332
+ this.#addReplyStream(/** @type {comq.Destroyable} */ stream)
333
+
334
+ return stream
335
+ }
336
+
317
337
  /**
318
338
  * @return {Promise<comq.ReplyEmitter>}
319
339
  */
320
- async #createFeedback () {
321
- const queue = 'feedback'
340
+ async #createControl () {
341
+ const queue = 'control'
322
342
 
323
343
  await this.#consumeReplies(queue)
324
344
 
@@ -364,7 +384,7 @@ class IO {
364
384
  Even if these messages are lost, the reply stream will be closed anyway,
365
385
  either due to missing heartbeat or the deletion of the stream queue.
366
386
  */
367
- await timeout(50)
387
+ await setTimeout(50)
368
388
  }
369
389
 
370
390
  /**