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.
- package/.github/dependabot.yml +2 -2
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release.yaml +16 -15
- package/docs/reply-stream-topology-dark.jpg +0 -0
- package/docs/reply-stream-topology-light.jpg +0 -0
- package/examples/readme.md +19 -3
- package/examples/streams/consumer.js +36 -0
- package/examples/streams/producer.js +44 -0
- package/features/backpressure.feature +2 -2
- package/features/events.feature +5 -0
- package/features/readme.md +2 -2
- package/features/recovery.feature +0 -3
- package/features/rpc.feature +15 -10
- package/features/rpc.streams.feature +81 -0
- package/features/rpc.streams.shards.feature +37 -0
- package/features/steps/.test/context.test.js +1 -1
- package/features/steps/.test/events.test.js +2 -2
- package/features/steps/.test/rpc.test.js +1 -1
- package/features/steps/.types/context.d.ts +9 -1
- package/features/steps/common.js +12 -0
- package/features/steps/context.js +9 -2
- package/features/steps/events.js +29 -0
- package/features/steps/rpc.js +273 -0
- package/lerna.json +1 -2
- package/package.json +2 -2
- package/readme.md +159 -31
- package/source/.io/ReplyPipe.js +158 -0
- package/source/.io/ReplyStream.js +142 -0
- package/source/.io/const.js +10 -0
- package/source/.io/{replies.js → createReplyEmitter.js} +3 -6
- package/source/.io/index.js +5 -1
- package/source/attributes/failsafe.js +34 -0
- package/source/attributes/failsafe.test.js +133 -0
- package/source/attributes/index.js +13 -0
- package/source/attributes/lazy.js +79 -0
- package/source/attributes/lazy.test.js +299 -0
- package/source/attributes/memo.js +13 -0
- package/source/attributes/memo.test.js +36 -0
- package/source/attributes/recall.js +42 -0
- package/source/attributes/recall.test.js +92 -0
- package/source/attributes/track.js +32 -0
- package/source/attributes/track.test.js +133 -0
- package/source/channel.js +33 -18
- package/source/connect.js +2 -2
- package/source/connection.js +4 -11
- package/source/emitter.js +13 -0
- package/source/encode.js +1 -1
- package/source/events.js +3 -3
- package/source/io.js +184 -48
- package/source/pipeline.js +43 -0
- package/source/shards/channel.js +28 -12
- package/source/shards/connection.js +2 -2
- package/test/channel.test.js +9 -1
- package/test/encode.test.js +2 -2
- package/test/encodings.js +1 -1
- package/test/io.diagnostics.test.js +3 -3
- package/test/io.emit.test.js +18 -3
- package/test/io.request.test.js +45 -23
- package/test/pipeline.test.js +81 -0
- package/test/shards/channel.test.js +39 -3
- package/test/shards/connection.test.js +1 -1
- package/types/channel.d.ts +13 -11
- package/types/connection.d.ts +3 -3
- package/types/diagnostic.d.ts +4 -3
- package/types/encoding.d.ts +2 -2
- package/types/index.d.ts +4 -4
- package/types/io.d.ts +40 -32
package/source/channel.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const { lazy, recall
|
|
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 =
|
|
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.
|
|
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.
|
|
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) //
|
|
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 !==
|
|
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.
|
|
230
|
-
* @returns {Promise<
|
|
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.
|
|
246
|
-
* @returns {comq.channels.
|
|
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 !==
|
|
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 ===
|
|
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 =
|
|
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.
|
|
8
|
+
/** @type {comq.Connect} */
|
|
9
9
|
const connect = async (...urls) => {
|
|
10
10
|
return create(urls, connectionConstructor(Connection))
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** @type {comq.
|
|
13
|
+
/** @type {comq.Connect} */
|
|
14
14
|
const assert = async (...urls) => {
|
|
15
15
|
return create(urls, connectionConstructor(SingletonConnection))
|
|
16
16
|
}
|
package/source/connection.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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} */
|
package/source/encode.js
CHANGED
package/source/events.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
/** @type {comq.diagnostics.
|
|
3
|
+
/** @type {comq.diagnostics.Event[]} */
|
|
4
4
|
exports.connection = ['open', 'close']
|
|
5
5
|
|
|
6
|
-
/** @type {comq.diagnostics.
|
|
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 {
|
|
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.
|
|
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.
|
|
66
|
-
* @
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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) { //
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
184
|
-
* @returns {comq.channels.
|
|
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 (
|
|
197
|
-
})
|
|
239
|
+
if (request.properties.replyTo === undefined) return
|
|
198
240
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
245
|
+
if (iterator) {
|
|
246
|
+
const readable = reply instanceof stream.Readable
|
|
247
|
+
? reply
|
|
248
|
+
: stream.Readable.from(reply)
|
|
208
249
|
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
252
|
+
const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#feedback,
|
|
253
|
+
(message, properties) => this.#reply(request, message, properties))
|
|
214
254
|
|
|
215
|
-
|
|
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.
|
|
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.
|
|
232
|
-
* @returns {comq.channels.
|
|
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.
|
|
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.
|
|
270
|
-
* @returns [Buffer, comq.
|
|
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.
|
|
419
|
+
/** @type {comq.Encoding} */
|
|
284
420
|
const OCTETS = 'application/octet-stream'
|
|
285
421
|
|
|
286
|
-
/** @type {comq.
|
|
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
|