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
package/source/channel.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
- const { EventEmitter } = require('node:events')
4
- const { lazy, recall, promex, failsafe, timeout } = require('@toa.io/generic')
3
+ const { promex, timeout } = require('@toa.io/generic')
4
+ const { failsafe, lazy, recall } = require('./attributes')
5
+ const emitter = require('./emitter')
5
6
 
6
7
  /**
7
8
  * @implements {comq.Channel}
@@ -24,8 +25,8 @@ class Channel {
24
25
  /** @type {string[]} */
25
26
  #tags = []
26
27
 
27
- /** @type {toa.generic.Promex} */
28
- #paused
28
+ /** @type {toa.generic.Promex | null} */
29
+ #paused = null
29
30
 
30
31
  /** @type {boolean} */
31
32
  #sealed = false
@@ -36,7 +37,7 @@ class Channel {
36
37
  /** @type {Set<toa.generic.Promex>} */
37
38
  #confirmations = new Set()
38
39
 
39
- #diagnostics = new EventEmitter()
40
+ #diagnostics = emitter.create()
40
41
 
41
42
  /**
42
43
  * @param {comq.amqp.Connection} connection
@@ -56,6 +57,9 @@ class Channel {
56
57
  async create () {
57
58
  if (this.#topology.confirms) this.#channel = await this.#connection.createConfirmChannel()
58
59
  else this.#channel = await this.#connection.createChannel()
60
+
61
+ this.#channel.on('drain', this.#unpause)
62
+ this.#channel.on('return', (message) => this.#diagnostics.emit('return', message))
59
63
  }
60
64
 
61
65
  consume = recall(this,
@@ -63,10 +67,10 @@ class Channel {
63
67
  lazy(this, this.#assertQueue,
64
68
  /**
65
69
  * @param {string} queue
66
- * @param {comq.channels.consumer} callback
70
+ * @param {comq.channels.Consumer} callback
67
71
  */
68
72
  async (queue, callback) => {
69
- if (!this.#sealed) await this.#consume(queue, callback)
73
+ if (!this.#sealed) return await this.#consume(queue, callback)
70
74
  })))
71
75
 
72
76
  subscribe = recall(this,
@@ -75,7 +79,7 @@ class Channel {
75
79
  /**
76
80
  * @param {string} exchange
77
81
  * @param {string} queue
78
- * @param {comq.channels.consumer} callback
82
+ * @param {comq.channels.Consumer} callback
79
83
  * @returns {Promise<void>}
80
84
  */
81
85
  async (exchange, queue, callback) => {
@@ -107,9 +111,13 @@ class Channel {
107
111
  async fire (queue, buffer, options) {
108
112
  try {
109
113
  await this.#publish(DEFAULT, queue, buffer, options)
114
+
115
+ return true
110
116
  } catch (exception) {
111
117
  if (this.#failfast) throw exception
112
118
  // ignore otherwise
119
+
120
+ return false
113
121
  }
114
122
  }
115
123
 
@@ -125,6 +133,10 @@ class Channel {
125
133
  this.#diagnostics.on(event, listener)
126
134
  }
127
135
 
136
+ forget (event, listener) {
137
+ this.#diagnostics.off(event, listener)
138
+ }
139
+
128
140
  async recover (connection) {
129
141
  this.#connection = connection
130
142
 
@@ -137,7 +149,7 @@ class Channel {
137
149
 
138
150
  for (const confirmation of this.#confirmations) confirmation.reject(INTERRUPTION)
139
151
 
140
- await timeout(0) // let unpause and confirmation interruptions be handled
152
+ await timeout(0) // handle interruptions
141
153
 
142
154
  this.#recovery.resolve()
143
155
  this.#recovery = promex()
@@ -197,7 +209,7 @@ class Channel {
197
209
  * @param {comq.amqp.options.Publish} options
198
210
  */
199
211
  async #publish (exchange, queue, buffer, options) {
200
- if (this.#paused !== undefined) await this.#unpaused()
212
+ if (this.#paused !== null) await this.#unpaused()
201
213
 
202
214
  options = Object.assign({ persistent: this.#topology.persistent }, options)
203
215
 
@@ -226,8 +238,8 @@ class Channel {
226
238
 
227
239
  /**
228
240
  * @param {string} queue
229
- * @param {comq.channels.consumer} consumer
230
- * @returns {Promise<void>}
241
+ * @param {comq.channels.Consumer} consumer
242
+ * @returns {Promise<string>}
231
243
  */
232
244
  async #consume (queue, consumer) {
233
245
  /** @type {comq.amqp.options.Consume} */
@@ -239,11 +251,13 @@ class Channel {
239
251
  const response = await this.#channel.consume(queue, consumer, options)
240
252
 
241
253
  this.#tags.push(response.consumerTag)
254
+
255
+ return response.consumerTag
242
256
  }
243
257
 
244
258
  /**
245
- * @param {comq.channels.consumer} consumer
246
- * @returns {comq.channels.consumer}
259
+ * @param {comq.channels.Consumer} consumer
260
+ * @returns {comq.channels.Consumer}
247
261
  */
248
262
  #getAcknowledgingConsumer = (consumer) =>
249
263
  async (message) => {
@@ -276,24 +290,25 @@ class Channel {
276
290
  }
277
291
 
278
292
  #pause () {
279
- if (this.#paused !== undefined) return
293
+ if (this.#paused !== null) return
280
294
 
281
295
  this.#paused = promex()
282
- this.#channel.once('drain', this.#unpause)
283
296
  this.#diagnostics.emit('flow')
297
+ this.#diagnostics.emit('pause')
284
298
  }
285
299
 
286
300
  /**
287
301
  * @param {Error} [exception]
288
302
  */
289
303
  #unpause = (exception) => {
290
- if (this.#paused === undefined) return
304
+ if (this.#paused === null) return
291
305
 
292
306
  if (exception === undefined) this.#paused.resolve()
293
307
  else this.#paused.reject(exception)
294
308
 
295
- this.#paused = undefined
309
+ this.#paused = null
296
310
  this.#diagnostics.emit('drain')
311
+ this.#diagnostics.emit('resume')
297
312
  }
298
313
 
299
314
  async #unpaused () {
package/source/connect.js CHANGED
@@ -5,12 +5,12 @@ const { Connection } = require('./connection')
5
5
  const { SingletonConnection } = require('./singleton')
6
6
  const shards = require('./shards')
7
7
 
8
- /** @type {comq.connect} */
8
+ /** @type {comq.Connect} */
9
9
  const connect = async (...urls) => {
10
10
  return create(urls, connectionConstructor(Connection))
11
11
  }
12
12
 
13
- /** @type {comq.connect} */
13
+ /** @type {comq.Connect} */
14
14
  const assert = async (...urls) => {
15
15
  return create(urls, connectionConstructor(SingletonConnection))
16
16
  }
@@ -1,11 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const { EventEmitter } = require('node:events')
4
3
  const amqp = require('amqplib')
5
- const { retry, promex, failsafe } = require('@toa.io/generic')
4
+ const { retry, promex } = require('@toa.io/generic')
6
5
 
6
+ const { failsafe } = require('./attributes')
7
7
  const presets = require('./topology')
8
8
  const channels = require('./channel')
9
+ const emitter = require('./emitter')
9
10
 
10
11
  /**
11
12
  * @implements {comq.Connection}
@@ -26,7 +27,7 @@ class Connection {
26
27
  /** @type {boolean} */
27
28
  #running = false
28
29
 
29
- #diagnostics = createEmitter()
30
+ #diagnostics = emitter.create()
30
31
 
31
32
  /**
32
33
  * @param {string} url
@@ -114,14 +115,6 @@ class Connection {
114
115
  }
115
116
  }
116
117
 
117
- function createEmitter () {
118
- const emitter = new EventEmitter()
119
-
120
- emitter.setMaxListeners(10000)
121
-
122
- return emitter
123
- }
124
-
125
118
  function noop () {}
126
119
 
127
120
  /** @type {toa.generic.retry.Options} */
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ const { EventEmitter } = require('node:events')
4
+
5
+ function create () {
6
+ const emitter = new EventEmitter()
7
+
8
+ emitter.setMaxListeners(0)
9
+
10
+ return emitter
11
+ }
12
+
13
+ exports.create = create
package/source/encode.js CHANGED
@@ -4,7 +4,7 @@ const encoders = require('./encoders')
4
4
 
5
5
  /**
6
6
  * @param {any} payload
7
- * @param {comq.encoding} encoding
7
+ * @param {comq.Encoding} encoding
8
8
  * @returns {Buffer}
9
9
  */
10
10
  const encode = (payload, encoding) => {
package/source/events.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- /** @type {comq.diagnostics.event[]} */
3
+ /** @type {comq.diagnostics.Event[]} */
4
4
  exports.connection = ['open', 'close']
5
5
 
6
- /** @type {comq.diagnostics.event[]} */
7
- exports.channel = ['flow', 'drain', 'recover', 'discard']
6
+ /** @type {comq.diagnostics.Event[]} */
7
+ exports.channel = ['flow', 'drain', 'recover', 'discard', 'pause', 'resume']
package/source/io.js CHANGED
@@ -1,11 +1,14 @@
1
1
  'use strict'
2
2
 
3
+ const stream = require('node:stream')
3
4
  const { EventEmitter } = require('node:events')
4
5
  const { randomBytes } = require('node:crypto')
5
- const { lazy, track, failsafe, promex } = require('@toa.io/generic')
6
+ const { promex, timeout } = require('@toa.io/generic')
7
+ const { memo, failsafe, lazy, track } = require('./attributes')
6
8
 
7
9
  const { decode } = require('./decode')
8
10
  const { encode } = require('./encode')
11
+ const { pipeline, transform } = require('./pipeline')
9
12
  const events = require('./events')
10
13
  const io = require('./.io')
11
14
 
@@ -28,9 +31,18 @@ class IO {
28
31
  /** @type {Map<string, comq.ReplyEmitter>} */
29
32
  #emitters = new Map()
30
33
 
34
+ /** @type {comq.ReplyEmitter | null} */
35
+ #feedback = null
36
+
31
37
  /** @type {Set<toa.generic.Promex>} */
32
38
  #pendingReplies = new Set()
33
39
 
40
+ /** @type {Set<comq.Destroyable>} */
41
+ #replyStreams = new Set()
42
+
43
+ /** @type {Set<comq.Destroyable>} */
44
+ #replyPipes = new Set()
45
+
34
46
  #diagnostics = new EventEmitter()
35
47
 
36
48
  /**
@@ -47,7 +59,7 @@ class IO {
47
59
  reply = lazy(this, this.#createRequestReplyChannels,
48
60
  /**
49
61
  * @param {string} queue
50
- * @param {comq.producer} callback
62
+ * @param {comq.Producer} callback
51
63
  * @returns {Promise<void>}
52
64
  */
53
65
  async (queue, callback) => {
@@ -61,30 +73,51 @@ class IO {
61
73
  failsafe(this, this.#recover,
62
74
  /**
63
75
  * @param {string} queue
64
- * @param {any} payload
65
- * @param {comq.encoding} [encoding]
66
- * @param {comq.ReplyToPropertyFormatter} [replyToFormatter]
67
- * @returns {Promise<void>}
76
+ * @param {any | Readable} payload
77
+ * @param {comq.Encoding} [encoding]
78
+ * @returns {Promise<any | Readable>}
68
79
  */
69
- async (queue, payload, encoding, replyToFormatter) => {
70
- const [buffer, contentType] = this.#encode(payload, encoding)
71
- const emitter = this.#emitters.get(queue)
72
- const reply = this.#createReply()
73
- const correlationId = randomBytes(8).toString('hex')
74
- const properties = { contentType, correlationId }
80
+ async (queue, payload, encoding) => {
81
+ if (payload instanceof stream.Readable) {
82
+ return pipeline(
83
+ payload,
84
+ (payload) => this.request(queue, payload, encoding),
85
+ this.#requests
86
+ )
87
+ }
75
88
 
76
- properties.replyTo = replyToFormatter?.(emitter.queue) ?? emitter.queue
89
+ const { buffer, properties, emitter, reply } = this.#createRequest(queue, payload, encoding)
77
90
 
78
- emitter.once(correlationId, reply.resolve)
91
+ emitter.once(properties.correlationId, reply.resolve)
79
92
 
80
93
  await this.#requests.send(queue, buffer, properties)
81
94
 
82
95
  return reply
83
96
  }))
84
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
+ const request = this.#createRequest(queue, payload, encoding)
108
+ const stream = new io.ReplyStream(request, this.#reply.bind(this))
109
+
110
+ this.#addReplyStream(stream)
111
+
112
+ await this.#requests.send(queue, request.buffer, request.properties)
113
+ await request.reply // wait for the confirmation
114
+
115
+ return stream
116
+ }))
117
+
85
118
  consume = lazy(this, this.#createEventChannel,
86
119
  async (exchange, group, callback) => {
87
- if (callback === undefined) { // 2 arguments passed
120
+ if (callback === undefined) { // two arguments passed
88
121
  callback = group
89
122
  group = undefined
90
123
  }
@@ -100,17 +133,25 @@ class IO {
100
133
  /**
101
134
  * @param {string} exchange
102
135
  * @param {any} payload
103
- * @param {comq.encoding | comq.amqp.options.Publish} [encoding]
136
+ * @param {comq.Encoding | comq.amqp.options.Publish} [encoding]
104
137
  * @returns {Promise<void>}
105
138
  */
106
139
  async (exchange, payload, encoding) => {
140
+ if (payload instanceof stream.Readable) {
141
+ return transform(
142
+ payload,
143
+ (payload) => this.emit(exchange, payload, encoding),
144
+ this.#events
145
+ )
146
+ }
147
+
107
148
  /** @type {comq.amqp.options.Publish} */
108
149
  const properties = {}
109
150
 
110
151
  if (typeof encoding === 'object') { // properties passed
111
152
  Object.assign(properties, encoding)
112
153
 
113
- encoding = /** @type {comq.encoding} */ properties.contentType
154
+ encoding = /** @type {comq.Encoding} */ properties.contentType
114
155
  }
115
156
 
116
157
  const [buffer, contentType] = this.#encode(payload, encoding)
@@ -120,16 +161,18 @@ class IO {
120
161
  await this.#events.publish(exchange, buffer, properties)
121
162
  })
122
163
 
123
- async seal () {
164
+ seal = memo(async () => {
124
165
  await this.#requests?.seal()
125
166
  await this.#events?.seal()
126
- }
167
+ await this.#destroyStreams(this.#replyStreams)
168
+ })
127
169
 
128
- async close () {
170
+ close = memo(async () => {
129
171
  await this.seal()
172
+ await this.#destroyStreams(this.#replyPipes)
130
173
  await track(this)
131
174
  await this.#connection.close()
132
- }
175
+ })
133
176
 
134
177
  diagnose (event, listener) {
135
178
  this.#diagnostics.on(event, listener)
@@ -180,8 +223,8 @@ class IO {
180
223
  }
181
224
 
182
225
  /**
183
- * @param {comq.producer} producer
184
- * @returns {comq.channels.consumer}
226
+ * @param {comq.Producer} producer
227
+ * @returns {comq.channels.Consumer}
185
228
  */
186
229
  #getRequestConsumer = (producer) =>
187
230
  track(this,
@@ -193,43 +236,43 @@ class IO {
193
236
  const payload = decode(request)
194
237
  const reply = await producer(payload)
195
238
 
196
- if ('replyTo' in request.properties) await this.#reply(request, reply)
197
- })
239
+ if (request.properties.replyTo === undefined) return
198
240
 
199
- /**
200
- * @param {comq.amqp.Message} request
201
- * @param {any} reply
202
- * @returns {Promise<void>}
203
- */
204
- async #reply (request, reply) {
205
- if (reply === undefined) throw new Error('The `producer` function must return a value')
241
+ const iterator = typeof reply === 'object' &&
242
+ (Symbol.asyncIterator in reply ||
243
+ (Symbol.iterator in reply && !Array.isArray(reply) && !Buffer.isBuffer(reply)))
206
244
 
207
- let { replyTo, correlationId, contentType } = request.properties
245
+ if (iterator) {
246
+ const readable = reply instanceof stream.Readable
247
+ ? reply
248
+ : stream.Readable.from(reply)
208
249
 
209
- if (Buffer.isBuffer(reply)) contentType = OCTETS
210
- if (contentType === undefined) throw new Error('Reply to a Request without the `contentType` property must be of type `Buffer`')
250
+ this.#feedback ??= await this.#createFeedback()
211
251
 
212
- const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
213
- const properties = { contentType, correlationId }
252
+ const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#feedback,
253
+ (message, properties) => this.#reply(request, message, properties))
214
254
 
215
- await this.#replies.fire(replyTo, buffer, properties)
216
- }
255
+ this.#addReplyPipe(pipe)
256
+ } else {
257
+ await this.#reply(request, reply)
258
+ }
259
+ })
217
260
 
218
261
  /**
219
262
  * @param {string} queue
220
263
  * @param {comq.ReplyEmitter} emitter
221
- * @returns {comq.channels.consumer}
264
+ * @returns {comq.channels.Consumer}
222
265
  */
223
266
  #getReplyConsumer = (queue, emitter) =>
224
267
  (message) => {
225
268
  const payload = decode(message)
226
269
 
227
- emitter.emit(message.properties.correlationId, payload)
270
+ emitter.emit(message.properties.correlationId, payload, message.properties)
228
271
  }
229
272
 
230
273
  /**
231
- * @param {comq.consumer} callback
232
- * @returns {comq.channels.consumer}
274
+ * @param {comq.Consumer} callback
275
+ * @returns {comq.channels.Consumer}
233
276
  */
234
277
  #getEventConsumer = (callback) =>
235
278
  track(this, async (message) => {
@@ -238,6 +281,24 @@ class IO {
238
281
  await callback(payload, message.properties)
239
282
  })
240
283
 
284
+ /**
285
+ * @param {string} queue
286
+ * @param {any} payload
287
+ * @param {comq.Encoding} [encoding]
288
+ * @return {comq.Request}
289
+ */
290
+ #createRequest (queue, payload, encoding) {
291
+ const [buffer, contentType] = this.#encode(payload, encoding)
292
+ const emitter = this.#emitters.get(queue)
293
+ const correlationId = randomBytes(8).toString('hex')
294
+ const reply = this.#createReply()
295
+
296
+ /** @type {comq.amqp.Properties} */
297
+ const properties = { contentType, correlationId, replyTo: emitter.queue }
298
+
299
+ return { buffer, emitter, reply, properties }
300
+ }
301
+
241
302
  /**
242
303
  * @return {toa.generic.Promex}
243
304
  */
@@ -253,12 +314,87 @@ class IO {
253
314
  return reply
254
315
  }
255
316
 
317
+ /**
318
+ * @return {Promise<comq.ReplyEmitter>}
319
+ */
320
+ async #createFeedback () {
321
+ const queue = 'feedback'
322
+
323
+ await this.#consumeReplies(queue)
324
+
325
+ return this.#emitters.get(queue)
326
+ }
327
+
328
+ /**
329
+ * @param {comq.Destroyable} stream
330
+ */
331
+ #addReplyStream (stream) {
332
+ this.#addStream(stream, this.#replyStreams)
333
+ }
334
+
335
+ /**
336
+ * @param {comq.Destroyable} pipe
337
+ */
338
+ #addReplyPipe (pipe) {
339
+ this.#addStream(pipe, this.#replyPipes)
340
+ }
341
+
342
+ /**
343
+ * @param {comq.Destroyable} stream
344
+ * @param {Set<comq.Destroyable>} streams
345
+ */
346
+ #addStream (stream, streams) {
347
+ streams.add(stream)
348
+ stream.on('close', () => streams.delete(stream))
349
+ }
350
+
351
+ /**
352
+ * @param {Set<comq.Destroyable>} streams
353
+ * @return {Promise<void>}
354
+ */
355
+ async #destroyStreams (streams) {
356
+ if (streams.size === 0) return
357
+
358
+ for (const stream of streams) stream.destroy()
359
+
360
+ /*
361
+ When streams are destroyed, they attempt to send an 'end' control message.
362
+ Since these messages are sent without an acknowledgment,
363
+ it is needed to wait briefly before closing the connection.
364
+ Even if these messages are lost, the reply stream will be closed anyway,
365
+ either due to missing heartbeat or the deletion of the stream queue.
366
+ */
367
+ await timeout(50)
368
+ }
369
+
370
+ /**
371
+ * @param {Pick<comq.amqp.Message, 'properties'>} request
372
+ * @param {any} reply
373
+ * @param {comq.amqp.options.Publish} [properties]
374
+ * @returns {Promise<boolean>}
375
+ */
376
+ async #reply (request, reply, properties = {}) {
377
+ if (reply === undefined) throw new Error('The `producer` function must return a value')
378
+
379
+ let { replyTo, contentType } = request.properties
380
+
381
+ if (Buffer.isBuffer(reply)) contentType = OCTETS
382
+ if (contentType === undefined) throw new Error('Reply to a Request without the `contentType` property must be of type `Buffer`')
383
+
384
+ const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
385
+
386
+ properties.contentType = contentType
387
+ properties.correlationId = request.properties.correlationId
388
+
389
+ return await this.#replies.fire(replyTo, buffer, properties)
390
+ }
391
+
256
392
  #recover (exception) {
257
393
  if (exception !== RETRANSMISSION) return false
258
394
  }
259
395
 
260
396
  #retransmit = () => {
261
- for (const emitter of this.#emitters.values()) emitter.clear()
397
+ for (const emitter of this.#emitters.values()) emitter.removeAllListeners()
262
398
 
263
399
  // trigger failsafe attribute
264
400
  for (const reply of this.#pendingReplies) reply.reject(RETRANSMISSION)
@@ -266,8 +402,8 @@ class IO {
266
402
 
267
403
  /**
268
404
  * @param {any} payload
269
- * @param {comq.encoding} [contentType]
270
- * @returns [Buffer, comq.encoding]
405
+ * @param {comq.Encoding} [contentType]
406
+ * @returns {[Buffer, comq.Encoding]}
271
407
  */
272
408
  #encode (payload, contentType) {
273
409
  const raw = Buffer.isBuffer(payload)
@@ -280,10 +416,10 @@ class IO {
280
416
  }
281
417
  }
282
418
 
283
- /** @type {comq.encoding} */
419
+ /** @type {comq.Encoding} */
284
420
  const OCTETS = 'application/octet-stream'
285
421
 
286
- /** @type {comq.encoding} */
422
+ /** @type {comq.Encoding} */
287
423
  const DEFAULT = 'application/msgpack'
288
424
 
289
425
  const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
@@ -0,0 +1,43 @@
1
+ 'use strict'
2
+
3
+ const { Transform } = require('node:stream')
4
+ const stream = require('node:stream/promises')
5
+
6
+ function pipeline (source, transform, channel) {
7
+ const destination = new Pipeline(transform)
8
+
9
+ // eslint-disable-next-line no-void
10
+ void stream.pipeline(source, destination)
11
+
12
+ channel.diagnose('pause', source.pause.bind(source))
13
+ channel.diagnose('resume', source.resume.bind(source))
14
+
15
+ return destination
16
+ }
17
+
18
+ async function transform (source, transform, channel) {
19
+ const readable = pipeline(source, transform, channel)
20
+
21
+ // eslint-disable-next-line no-void, no-unused-vars
22
+ for await (const _ of readable) void 0
23
+ }
24
+
25
+ class Pipeline extends Transform {
26
+ #transform
27
+
28
+ constructor (transform) {
29
+ super({ objectMode: true })
30
+
31
+ this.#transform = transform
32
+ }
33
+
34
+ _transform (request, _, callback) {
35
+ const promise = this.#transform(request)
36
+
37
+ this.push(promise)
38
+ callback()
39
+ }
40
+ }
41
+
42
+ exports.pipeline = pipeline
43
+ exports.transform = transform