comq 0.10.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.
@@ -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
@@ -50,6 +50,8 @@ async function healthy () {
50
50
 
51
51
  process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-0')
52
52
  } while (process.output !== 'healthy')
53
+
54
+ await timeout(500)
53
55
  }
54
56
 
55
57
  const BROKERS_AMOUNT = 2
@@ -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.1",
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.1",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
@@ -47,7 +47,7 @@
47
47
  "scripts": {
48
48
  "prepare": "echo run: husky install",
49
49
  "test": "standard && jest",
50
- "features": "cucumber-js",
50
+ "features": "cucumber-js --publish-quiet",
51
51
  "lint": "standard --fix --verbose | snazzy"
52
52
  },
53
53
  "standard": {
package/readme.md CHANGED
@@ -202,12 +202,10 @@ await io.reply('get_numbers', function * ({ amount }) {
202
202
  })
203
203
  ```
204
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`
205
+ The Reply stream may be consumed by using the `IO.request` function:
208
206
 
209
207
  ```javascript
210
- const stream = await io.fetch('get_numbers', { amount: 10 })
208
+ const stream = await io.request('get_numbers', { amount: 10 })
211
209
 
212
210
  for await (const number of stream)
213
211
  console.log(number)
@@ -220,8 +218,6 @@ for await (const number of stream)
220
218
  <img alt="Reply topology" width="500" height="411" src="./docs/reply-stream-topology-light.jpg">
221
219
  </picture>
222
220
 
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
221
  When the producer function of `IO.reply` returns an Iterator for the first time across all request queues,
226
222
  a control queue is asserted on the [Reply channel](#channels)
227
223
  using the [reply topology](#exchanges-and-queues).
@@ -235,7 +231,7 @@ the request will be retransmitted upon reconnection.
235
231
 
236
232
  A heartbeat message is sent to the `replyTo` queue whenever a Reply stream idles for 5 seconds.
237
233
  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.
234
+ by `IO.request` is destroyed.
239
235
  These intervals are not configurable.
240
236
 
241
237
  An "end stream" message is sent to the `replyTo` queue when the Reply stream is finished.
@@ -247,7 +243,7 @@ See also [Reply stream shutdown](#reply-stream-shutdown).
247
243
  :warning:
248
244
 
249
245
  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`,
246
+ or if the Consumer crashes or destroys the stream returned by `IO.request`,
251
247
  some of the values yielded by the Reply stream may be lost.
252
248
 
253
249
  To avoid inconsistency, it is strongly recommended to use the Reply stream only with _safe_ Producers, which do not
@@ -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('@toa.io/generic')
5
+ const { IDLE_INTERVAL, control } = require('./const')
6
6
 
7
7
  class ReplyStream extends Readable {
8
- #confirmation
8
+ confirmation = promex()
9
9
 
10
10
  /** @type {ReturnType<setTimeout> | null} */
11
11
  #timeout = null
@@ -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
 
@@ -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/io.js CHANGED
@@ -32,7 +32,7 @@ class IO {
32
32
  #emitters = new Map()
33
33
 
34
34
  /** @type {comq.ReplyEmitter | null} */
35
- #feedback = null
35
+ #control = null
36
36
 
37
37
  /** @type {Set<toa.generic.Promex>} */
38
38
  #pendingReplies = new Set()
@@ -86,33 +86,12 @@ class IO {
86
86
  )
87
87
  }
88
88
 
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
89
  const request = this.#createRequest(queue, payload, encoding)
108
- const stream = new io.ReplyStream(request, this.#reply.bind(this))
109
-
110
- this.#addReplyStream(stream)
90
+ const reply = this.#createReply(request)
111
91
 
112
92
  await this.#requests.send(queue, request.buffer, request.properties)
113
- await request.reply // wait for the confirmation
114
93
 
115
- return stream
94
+ return reply
116
95
  }))
117
96
 
118
97
  consume = lazy(this, this.#createEventChannel,
@@ -247,9 +226,9 @@ class IO {
247
226
  ? reply
248
227
  : stream.Readable.from(reply)
249
228
 
250
- this.#feedback ??= await this.#createFeedback()
229
+ this.#control ??= await this.#createControl()
251
230
 
252
- const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#feedback,
231
+ const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#control,
253
232
  (message, properties) => this.#reply(request, message, properties))
254
233
 
255
234
  this.#addReplyPipe(pipe)
@@ -291,18 +270,29 @@ class IO {
291
270
  const [buffer, contentType] = this.#encode(payload, encoding)
292
271
  const emitter = this.#emitters.get(queue)
293
272
  const correlationId = randomBytes(8).toString('hex')
294
- const reply = this.#createReply()
295
273
 
296
274
  /** @type {comq.amqp.Properties} */
297
275
  const properties = { contentType, correlationId, replyTo: emitter.queue }
298
276
 
299
- return { buffer, emitter, reply, properties }
277
+ return { buffer, emitter, properties }
278
+ }
279
+
280
+ /**
281
+ * @param {comq.Request} request
282
+ * @return {Promise<any>}
283
+ */
284
+ #createReply (request) {
285
+ const reply = this.#createPendingReply()
286
+
287
+ request.emitter.once(request.properties.correlationId, this.#getReplyResolver(request, reply))
288
+
289
+ return reply
300
290
  }
301
291
 
302
292
  /**
303
293
  * @return {toa.generic.Promex}
304
294
  */
305
- #createReply () {
295
+ #createPendingReply () {
306
296
  const reply = promex()
307
297
 
308
298
  this.#pendingReplies.add(reply)
@@ -314,11 +304,40 @@ class IO {
314
304
  return reply
315
305
  }
316
306
 
307
+ /**
308
+ * @param {comq.Request} request
309
+ * @param reply
310
+ */
311
+ #getReplyResolver (request, reply) {
312
+ return async (payload, properties) => {
313
+ const isStream = properties.headers?.index !== undefined
314
+
315
+ if (isStream) {
316
+ const stream = this.#createReplyStream(request, payload, properties)
317
+
318
+ await stream.confirmation
319
+
320
+ reply.resolve(stream)
321
+ } else {
322
+ reply.resolve(payload)
323
+ }
324
+ }
325
+ }
326
+
327
+ #createReplyStream (request, payload, properties) {
328
+ const stream = new io.ReplyStream(request, this.#reply.bind(this))
329
+
330
+ stream.arrange(payload, properties)
331
+ this.#addReplyStream(/** @type {comq.Destroyable} */ stream)
332
+
333
+ return stream
334
+ }
335
+
317
336
  /**
318
337
  * @return {Promise<comq.ReplyEmitter>}
319
338
  */
320
- async #createFeedback () {
321
- const queue = 'feedback'
339
+ async #createControl () {
340
+ const queue = 'control'
322
341
 
323
342
  await this.#consumeReplies(queue)
324
343
 
package/types/io.d.ts CHANGED
@@ -22,18 +22,15 @@ declare namespace comq {
22
22
  buffer: Buffer
23
23
  emitter: ReplyEmitter
24
24
  properties: _amqp.Properties
25
- reply: any
26
25
  }
27
26
 
28
27
  interface IO extends _diagnostics.Diagnosable {
29
28
  reply (queue: string, produce: Producer): Promise<void>
30
29
 
31
- request<Reply = any, Request = any> (queue: string, payload: Request, encoding?: _encoding.Encoding): Promise<Reply>
30
+ request<Reply = any, Request = any> (queue: string, payload: Request, encoding?: _encoding.Encoding): Promise<Reply> | Promise<Readable>
32
31
 
33
32
  request (queue: string, stream: Readable, encoding?: _encoding.Encoding): Promise<Readable>
34
33
 
35
- fetch (queue: string, payload: any, encoding?: _encoding.Encoding): Promise<Readable>
36
-
37
34
  consume<T = any> (exchange: string, group: string, consumer: Consumer<T>): Promise<void>
38
35
 
39
36
  consume<T = any> (exchange: string, consumer: Consumer<T>): Promise<void>