comq 0.9.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.
- package/.github/dependabot.yml +2 -2
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/release.yaml +14 -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/readme.md +2 -2
- 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 +1 -1
- package/features/steps/.types/context.d.ts +9 -1
- package/features/steps/broker.js +2 -0
- package/features/steps/common.js +12 -0
- package/features/steps/context.js +9 -2
- package/features/steps/rpc.js +235 -0
- package/lerna.json +1 -2
- package/package.json +3 -2
- package/readme.md +136 -46
- package/source/.io/ReplyPipe.js +158 -0
- package/source/.io/ReplyStream.js +139 -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 +31 -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 +2 -2
- package/source/io.js +187 -50
- package/source/shards/channel.js +13 -10
- package/source/shards/connection.js +2 -2
- package/test/channel.test.js +3 -1
- package/test/encode.test.js +2 -2
- package/test/encodings.js +1 -1
- package/test/io.diagnostics.test.js +2 -2
- package/test/io.request.test.js +4 -23
- package/test/shards/channel.test.js +1 -1
- 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 +33 -40
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,10 +290,9 @@ 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')
|
|
284
297
|
this.#diagnostics.emit('pause')
|
|
285
298
|
}
|
|
@@ -288,12 +301,12 @@ class Channel {
|
|
|
288
301
|
* @param {Error} [exception]
|
|
289
302
|
*/
|
|
290
303
|
#unpause = (exception) => {
|
|
291
|
-
if (this.#paused ===
|
|
304
|
+
if (this.#paused === null) return
|
|
292
305
|
|
|
293
306
|
if (exception === undefined) this.#paused.resolve()
|
|
294
307
|
else this.#paused.reject(exception)
|
|
295
308
|
|
|
296
|
-
this.#paused =
|
|
309
|
+
this.#paused = null
|
|
297
310
|
this.#diagnostics.emit('drain')
|
|
298
311
|
this.#diagnostics.emit('resume')
|
|
299
312
|
}
|
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.
|
|
6
|
+
/** @type {comq.diagnostics.Event[]} */
|
|
7
7
|
exports.channel = ['flow', 'drain', 'recover', 'discard', 'pause', 'resume']
|
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 {
|
|
6
|
+
const { promex, timeout } = require('@toa.io/generic')
|
|
7
|
+
const { memo, failsafe, lazy, track } = require('./attributes')
|
|
7
8
|
|
|
8
9
|
const { decode } = require('./decode')
|
|
9
10
|
const { encode } = require('./encode')
|
|
@@ -30,9 +31,18 @@ class IO {
|
|
|
30
31
|
/** @type {Map<string, comq.ReplyEmitter>} */
|
|
31
32
|
#emitters = new Map()
|
|
32
33
|
|
|
34
|
+
/** @type {comq.ReplyEmitter | null} */
|
|
35
|
+
#control = null
|
|
36
|
+
|
|
33
37
|
/** @type {Set<toa.generic.Promex>} */
|
|
34
38
|
#pendingReplies = new Set()
|
|
35
39
|
|
|
40
|
+
/** @type {Set<comq.Destroyable>} */
|
|
41
|
+
#replyStreams = new Set()
|
|
42
|
+
|
|
43
|
+
/** @type {Set<comq.Destroyable>} */
|
|
44
|
+
#replyPipes = new Set()
|
|
45
|
+
|
|
36
46
|
#diagnostics = new EventEmitter()
|
|
37
47
|
|
|
38
48
|
/**
|
|
@@ -49,7 +59,7 @@ class IO {
|
|
|
49
59
|
reply = lazy(this, this.#createRequestReplyChannels,
|
|
50
60
|
/**
|
|
51
61
|
* @param {string} queue
|
|
52
|
-
* @param {comq.
|
|
62
|
+
* @param {comq.Producer} callback
|
|
53
63
|
* @returns {Promise<void>}
|
|
54
64
|
*/
|
|
55
65
|
async (queue, callback) => {
|
|
@@ -64,30 +74,22 @@ class IO {
|
|
|
64
74
|
/**
|
|
65
75
|
* @param {string} queue
|
|
66
76
|
* @param {any | Readable} payload
|
|
67
|
-
* @param {comq.
|
|
68
|
-
* @param {comq.ReplyToPropertyFormatter} [replyToFormatter]
|
|
77
|
+
* @param {comq.Encoding} [encoding]
|
|
69
78
|
* @returns {Promise<any | Readable>}
|
|
70
79
|
*/
|
|
71
|
-
async (queue, payload, encoding
|
|
80
|
+
async (queue, payload, encoding) => {
|
|
72
81
|
if (payload instanceof stream.Readable) {
|
|
73
82
|
return pipeline(
|
|
74
83
|
payload,
|
|
75
|
-
(payload) => this.request(queue, payload, encoding
|
|
84
|
+
(payload) => this.request(queue, payload, encoding),
|
|
76
85
|
this.#requests
|
|
77
86
|
)
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const reply = this.#createReply()
|
|
83
|
-
const correlationId = randomBytes(8).toString('hex')
|
|
84
|
-
const properties = { contentType, correlationId }
|
|
89
|
+
const request = this.#createRequest(queue, payload, encoding)
|
|
90
|
+
const reply = this.#createReply(request)
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
emitter.once(correlationId, reply.resolve)
|
|
89
|
-
|
|
90
|
-
await this.#requests.send(queue, buffer, properties)
|
|
92
|
+
await this.#requests.send(queue, request.buffer, request.properties)
|
|
91
93
|
|
|
92
94
|
return reply
|
|
93
95
|
}))
|
|
@@ -110,7 +112,7 @@ class IO {
|
|
|
110
112
|
/**
|
|
111
113
|
* @param {string} exchange
|
|
112
114
|
* @param {any} payload
|
|
113
|
-
* @param {comq.
|
|
115
|
+
* @param {comq.Encoding | comq.amqp.options.Publish} [encoding]
|
|
114
116
|
* @returns {Promise<void>}
|
|
115
117
|
*/
|
|
116
118
|
async (exchange, payload, encoding) => {
|
|
@@ -128,7 +130,7 @@ class IO {
|
|
|
128
130
|
if (typeof encoding === 'object') { // properties passed
|
|
129
131
|
Object.assign(properties, encoding)
|
|
130
132
|
|
|
131
|
-
encoding = /** @type {comq.
|
|
133
|
+
encoding = /** @type {comq.Encoding} */ properties.contentType
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
@@ -138,16 +140,18 @@ class IO {
|
|
|
138
140
|
await this.#events.publish(exchange, buffer, properties)
|
|
139
141
|
})
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
seal = memo(async () => {
|
|
142
144
|
await this.#requests?.seal()
|
|
143
145
|
await this.#events?.seal()
|
|
144
|
-
|
|
146
|
+
await this.#destroyStreams(this.#replyStreams)
|
|
147
|
+
})
|
|
145
148
|
|
|
146
|
-
|
|
149
|
+
close = memo(async () => {
|
|
147
150
|
await this.seal()
|
|
151
|
+
await this.#destroyStreams(this.#replyPipes)
|
|
148
152
|
await track(this)
|
|
149
153
|
await this.#connection.close()
|
|
150
|
-
}
|
|
154
|
+
})
|
|
151
155
|
|
|
152
156
|
diagnose (event, listener) {
|
|
153
157
|
this.#diagnostics.on(event, listener)
|
|
@@ -198,8 +202,8 @@ class IO {
|
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
/**
|
|
201
|
-
* @param {comq.
|
|
202
|
-
* @returns {comq.channels.
|
|
205
|
+
* @param {comq.Producer} producer
|
|
206
|
+
* @returns {comq.channels.Consumer}
|
|
203
207
|
*/
|
|
204
208
|
#getRequestConsumer = (producer) =>
|
|
205
209
|
track(this,
|
|
@@ -211,43 +215,43 @@ class IO {
|
|
|
211
215
|
const payload = decode(request)
|
|
212
216
|
const reply = await producer(payload)
|
|
213
217
|
|
|
214
|
-
if (
|
|
215
|
-
})
|
|
218
|
+
if (request.properties.replyTo === undefined) return
|
|
216
219
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
* @returns {Promise<void>}
|
|
221
|
-
*/
|
|
222
|
-
async #reply (request, reply) {
|
|
223
|
-
if (reply === undefined) throw new Error('The `producer` function must return a value')
|
|
220
|
+
const iterator = typeof reply === 'object' &&
|
|
221
|
+
(Symbol.asyncIterator in reply ||
|
|
222
|
+
(Symbol.iterator in reply && !Array.isArray(reply) && !Buffer.isBuffer(reply)))
|
|
224
223
|
|
|
225
|
-
|
|
224
|
+
if (iterator) {
|
|
225
|
+
const readable = reply instanceof stream.Readable
|
|
226
|
+
? reply
|
|
227
|
+
: stream.Readable.from(reply)
|
|
226
228
|
|
|
227
|
-
|
|
228
|
-
if (contentType === undefined) throw new Error('Reply to a Request without the `contentType` property must be of type `Buffer`')
|
|
229
|
+
this.#control ??= await this.#createControl()
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#control,
|
|
232
|
+
(message, properties) => this.#reply(request, message, properties))
|
|
232
233
|
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
this.#addReplyPipe(pipe)
|
|
235
|
+
} else {
|
|
236
|
+
await this.#reply(request, reply)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
235
239
|
|
|
236
240
|
/**
|
|
237
241
|
* @param {string} queue
|
|
238
242
|
* @param {comq.ReplyEmitter} emitter
|
|
239
|
-
* @returns {comq.channels.
|
|
243
|
+
* @returns {comq.channels.Consumer}
|
|
240
244
|
*/
|
|
241
245
|
#getReplyConsumer = (queue, emitter) =>
|
|
242
246
|
(message) => {
|
|
243
247
|
const payload = decode(message)
|
|
244
248
|
|
|
245
|
-
emitter.emit(message.properties.correlationId, payload)
|
|
249
|
+
emitter.emit(message.properties.correlationId, payload, message.properties)
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
/**
|
|
249
|
-
* @param {comq.
|
|
250
|
-
* @returns {comq.channels.
|
|
253
|
+
* @param {comq.Consumer} callback
|
|
254
|
+
* @returns {comq.channels.Consumer}
|
|
251
255
|
*/
|
|
252
256
|
#getEventConsumer = (callback) =>
|
|
253
257
|
track(this, async (message) => {
|
|
@@ -256,10 +260,39 @@ class IO {
|
|
|
256
260
|
await callback(payload, message.properties)
|
|
257
261
|
})
|
|
258
262
|
|
|
263
|
+
/**
|
|
264
|
+
* @param {string} queue
|
|
265
|
+
* @param {any} payload
|
|
266
|
+
* @param {comq.Encoding} [encoding]
|
|
267
|
+
* @return {comq.Request}
|
|
268
|
+
*/
|
|
269
|
+
#createRequest (queue, payload, encoding) {
|
|
270
|
+
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
271
|
+
const emitter = this.#emitters.get(queue)
|
|
272
|
+
const correlationId = randomBytes(8).toString('hex')
|
|
273
|
+
|
|
274
|
+
/** @type {comq.amqp.Properties} */
|
|
275
|
+
const properties = { contentType, correlationId, replyTo: emitter.queue }
|
|
276
|
+
|
|
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
|
|
290
|
+
}
|
|
291
|
+
|
|
259
292
|
/**
|
|
260
293
|
* @return {toa.generic.Promex}
|
|
261
294
|
*/
|
|
262
|
-
#
|
|
295
|
+
#createPendingReply () {
|
|
263
296
|
const reply = promex()
|
|
264
297
|
|
|
265
298
|
this.#pendingReplies.add(reply)
|
|
@@ -271,12 +304,116 @@ class IO {
|
|
|
271
304
|
return reply
|
|
272
305
|
}
|
|
273
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
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @return {Promise<comq.ReplyEmitter>}
|
|
338
|
+
*/
|
|
339
|
+
async #createControl () {
|
|
340
|
+
const queue = 'control'
|
|
341
|
+
|
|
342
|
+
await this.#consumeReplies(queue)
|
|
343
|
+
|
|
344
|
+
return this.#emitters.get(queue)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @param {comq.Destroyable} stream
|
|
349
|
+
*/
|
|
350
|
+
#addReplyStream (stream) {
|
|
351
|
+
this.#addStream(stream, this.#replyStreams)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {comq.Destroyable} pipe
|
|
356
|
+
*/
|
|
357
|
+
#addReplyPipe (pipe) {
|
|
358
|
+
this.#addStream(pipe, this.#replyPipes)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @param {comq.Destroyable} stream
|
|
363
|
+
* @param {Set<comq.Destroyable>} streams
|
|
364
|
+
*/
|
|
365
|
+
#addStream (stream, streams) {
|
|
366
|
+
streams.add(stream)
|
|
367
|
+
stream.on('close', () => streams.delete(stream))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {Set<comq.Destroyable>} streams
|
|
372
|
+
* @return {Promise<void>}
|
|
373
|
+
*/
|
|
374
|
+
async #destroyStreams (streams) {
|
|
375
|
+
if (streams.size === 0) return
|
|
376
|
+
|
|
377
|
+
for (const stream of streams) stream.destroy()
|
|
378
|
+
|
|
379
|
+
/*
|
|
380
|
+
When streams are destroyed, they attempt to send an 'end' control message.
|
|
381
|
+
Since these messages are sent without an acknowledgment,
|
|
382
|
+
it is needed to wait briefly before closing the connection.
|
|
383
|
+
Even if these messages are lost, the reply stream will be closed anyway,
|
|
384
|
+
either due to missing heartbeat or the deletion of the stream queue.
|
|
385
|
+
*/
|
|
386
|
+
await timeout(50)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {Pick<comq.amqp.Message, 'properties'>} request
|
|
391
|
+
* @param {any} reply
|
|
392
|
+
* @param {comq.amqp.options.Publish} [properties]
|
|
393
|
+
* @returns {Promise<boolean>}
|
|
394
|
+
*/
|
|
395
|
+
async #reply (request, reply, properties = {}) {
|
|
396
|
+
if (reply === undefined) throw new Error('The `producer` function must return a value')
|
|
397
|
+
|
|
398
|
+
let { replyTo, contentType } = request.properties
|
|
399
|
+
|
|
400
|
+
if (Buffer.isBuffer(reply)) contentType = OCTETS
|
|
401
|
+
if (contentType === undefined) throw new Error('Reply to a Request without the `contentType` property must be of type `Buffer`')
|
|
402
|
+
|
|
403
|
+
const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
|
|
404
|
+
|
|
405
|
+
properties.contentType = contentType
|
|
406
|
+
properties.correlationId = request.properties.correlationId
|
|
407
|
+
|
|
408
|
+
return await this.#replies.fire(replyTo, buffer, properties)
|
|
409
|
+
}
|
|
410
|
+
|
|
274
411
|
#recover (exception) {
|
|
275
412
|
if (exception !== RETRANSMISSION) return false
|
|
276
413
|
}
|
|
277
414
|
|
|
278
415
|
#retransmit = () => {
|
|
279
|
-
for (const emitter of this.#emitters.values()) emitter.
|
|
416
|
+
for (const emitter of this.#emitters.values()) emitter.removeAllListeners()
|
|
280
417
|
|
|
281
418
|
// trigger failsafe attribute
|
|
282
419
|
for (const reply of this.#pendingReplies) reply.reject(RETRANSMISSION)
|
|
@@ -284,8 +421,8 @@ class IO {
|
|
|
284
421
|
|
|
285
422
|
/**
|
|
286
423
|
* @param {any} payload
|
|
287
|
-
* @param {comq.
|
|
288
|
-
* @returns [Buffer, comq.
|
|
424
|
+
* @param {comq.Encoding} [contentType]
|
|
425
|
+
* @returns {[Buffer, comq.Encoding]}
|
|
289
426
|
*/
|
|
290
427
|
#encode (payload, contentType) {
|
|
291
428
|
const raw = Buffer.isBuffer(payload)
|
|
@@ -298,10 +435,10 @@ class IO {
|
|
|
298
435
|
}
|
|
299
436
|
}
|
|
300
437
|
|
|
301
|
-
/** @type {comq.
|
|
438
|
+
/** @type {comq.Encoding} */
|
|
302
439
|
const OCTETS = 'application/octet-stream'
|
|
303
440
|
|
|
304
|
-
/** @type {comq.
|
|
441
|
+
/** @type {comq.Encoding} */
|
|
305
442
|
const DEFAULT = 'application/msgpack'
|
|
306
443
|
|
|
307
444
|
const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
|