comq 0.9.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 +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/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 +2 -1
- package/readme.md +140 -46
- 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 +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 +165 -47
- 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 +36 -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
|
+
#feedback = 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,34 +74,47 @@ 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 emitter = this.#emitters.get(queue)
|
|
82
|
-
const reply = this.#createReply()
|
|
83
|
-
const correlationId = randomBytes(8).toString('hex')
|
|
84
|
-
const properties = { contentType, correlationId }
|
|
89
|
+
const { buffer, properties, emitter, reply } = this.#createRequest(queue, payload, encoding)
|
|
85
90
|
|
|
86
|
-
properties.
|
|
87
|
-
|
|
88
|
-
emitter.once(correlationId, reply.resolve)
|
|
91
|
+
emitter.once(properties.correlationId, reply.resolve)
|
|
89
92
|
|
|
90
93
|
await this.#requests.send(queue, buffer, properties)
|
|
91
94
|
|
|
92
95
|
return reply
|
|
93
96
|
}))
|
|
94
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
|
+
|
|
95
118
|
consume = lazy(this, this.#createEventChannel,
|
|
96
119
|
async (exchange, group, callback) => {
|
|
97
120
|
if (callback === undefined) { // two arguments passed
|
|
@@ -110,7 +133,7 @@ class IO {
|
|
|
110
133
|
/**
|
|
111
134
|
* @param {string} exchange
|
|
112
135
|
* @param {any} payload
|
|
113
|
-
* @param {comq.
|
|
136
|
+
* @param {comq.Encoding | comq.amqp.options.Publish} [encoding]
|
|
114
137
|
* @returns {Promise<void>}
|
|
115
138
|
*/
|
|
116
139
|
async (exchange, payload, encoding) => {
|
|
@@ -128,7 +151,7 @@ class IO {
|
|
|
128
151
|
if (typeof encoding === 'object') { // properties passed
|
|
129
152
|
Object.assign(properties, encoding)
|
|
130
153
|
|
|
131
|
-
encoding = /** @type {comq.
|
|
154
|
+
encoding = /** @type {comq.Encoding} */ properties.contentType
|
|
132
155
|
}
|
|
133
156
|
|
|
134
157
|
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
@@ -138,16 +161,18 @@ class IO {
|
|
|
138
161
|
await this.#events.publish(exchange, buffer, properties)
|
|
139
162
|
})
|
|
140
163
|
|
|
141
|
-
|
|
164
|
+
seal = memo(async () => {
|
|
142
165
|
await this.#requests?.seal()
|
|
143
166
|
await this.#events?.seal()
|
|
144
|
-
|
|
167
|
+
await this.#destroyStreams(this.#replyStreams)
|
|
168
|
+
})
|
|
145
169
|
|
|
146
|
-
|
|
170
|
+
close = memo(async () => {
|
|
147
171
|
await this.seal()
|
|
172
|
+
await this.#destroyStreams(this.#replyPipes)
|
|
148
173
|
await track(this)
|
|
149
174
|
await this.#connection.close()
|
|
150
|
-
}
|
|
175
|
+
})
|
|
151
176
|
|
|
152
177
|
diagnose (event, listener) {
|
|
153
178
|
this.#diagnostics.on(event, listener)
|
|
@@ -198,8 +223,8 @@ class IO {
|
|
|
198
223
|
}
|
|
199
224
|
|
|
200
225
|
/**
|
|
201
|
-
* @param {comq.
|
|
202
|
-
* @returns {comq.channels.
|
|
226
|
+
* @param {comq.Producer} producer
|
|
227
|
+
* @returns {comq.channels.Consumer}
|
|
203
228
|
*/
|
|
204
229
|
#getRequestConsumer = (producer) =>
|
|
205
230
|
track(this,
|
|
@@ -211,43 +236,43 @@ class IO {
|
|
|
211
236
|
const payload = decode(request)
|
|
212
237
|
const reply = await producer(payload)
|
|
213
238
|
|
|
214
|
-
if (
|
|
215
|
-
})
|
|
239
|
+
if (request.properties.replyTo === undefined) return
|
|
216
240
|
|
|
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')
|
|
241
|
+
const iterator = typeof reply === 'object' &&
|
|
242
|
+
(Symbol.asyncIterator in reply ||
|
|
243
|
+
(Symbol.iterator in reply && !Array.isArray(reply) && !Buffer.isBuffer(reply)))
|
|
224
244
|
|
|
225
|
-
|
|
245
|
+
if (iterator) {
|
|
246
|
+
const readable = reply instanceof stream.Readable
|
|
247
|
+
? reply
|
|
248
|
+
: stream.Readable.from(reply)
|
|
226
249
|
|
|
227
|
-
|
|
228
|
-
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()
|
|
229
251
|
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#feedback,
|
|
253
|
+
(message, properties) => this.#reply(request, message, properties))
|
|
232
254
|
|
|
233
|
-
|
|
234
|
-
|
|
255
|
+
this.#addReplyPipe(pipe)
|
|
256
|
+
} else {
|
|
257
|
+
await this.#reply(request, reply)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
235
260
|
|
|
236
261
|
/**
|
|
237
262
|
* @param {string} queue
|
|
238
263
|
* @param {comq.ReplyEmitter} emitter
|
|
239
|
-
* @returns {comq.channels.
|
|
264
|
+
* @returns {comq.channels.Consumer}
|
|
240
265
|
*/
|
|
241
266
|
#getReplyConsumer = (queue, emitter) =>
|
|
242
267
|
(message) => {
|
|
243
268
|
const payload = decode(message)
|
|
244
269
|
|
|
245
|
-
emitter.emit(message.properties.correlationId, payload)
|
|
270
|
+
emitter.emit(message.properties.correlationId, payload, message.properties)
|
|
246
271
|
}
|
|
247
272
|
|
|
248
273
|
/**
|
|
249
|
-
* @param {comq.
|
|
250
|
-
* @returns {comq.channels.
|
|
274
|
+
* @param {comq.Consumer} callback
|
|
275
|
+
* @returns {comq.channels.Consumer}
|
|
251
276
|
*/
|
|
252
277
|
#getEventConsumer = (callback) =>
|
|
253
278
|
track(this, async (message) => {
|
|
@@ -256,6 +281,24 @@ class IO {
|
|
|
256
281
|
await callback(payload, message.properties)
|
|
257
282
|
})
|
|
258
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
|
+
|
|
259
302
|
/**
|
|
260
303
|
* @return {toa.generic.Promex}
|
|
261
304
|
*/
|
|
@@ -271,12 +314,87 @@ class IO {
|
|
|
271
314
|
return reply
|
|
272
315
|
}
|
|
273
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
|
+
|
|
274
392
|
#recover (exception) {
|
|
275
393
|
if (exception !== RETRANSMISSION) return false
|
|
276
394
|
}
|
|
277
395
|
|
|
278
396
|
#retransmit = () => {
|
|
279
|
-
for (const emitter of this.#emitters.values()) emitter.
|
|
397
|
+
for (const emitter of this.#emitters.values()) emitter.removeAllListeners()
|
|
280
398
|
|
|
281
399
|
// trigger failsafe attribute
|
|
282
400
|
for (const reply of this.#pendingReplies) reply.reject(RETRANSMISSION)
|
|
@@ -284,8 +402,8 @@ class IO {
|
|
|
284
402
|
|
|
285
403
|
/**
|
|
286
404
|
* @param {any} payload
|
|
287
|
-
* @param {comq.
|
|
288
|
-
* @returns [Buffer, comq.
|
|
405
|
+
* @param {comq.Encoding} [contentType]
|
|
406
|
+
* @returns {[Buffer, comq.Encoding]}
|
|
289
407
|
*/
|
|
290
408
|
#encode (payload, contentType) {
|
|
291
409
|
const raw = Buffer.isBuffer(payload)
|
|
@@ -298,10 +416,10 @@ class IO {
|
|
|
298
416
|
}
|
|
299
417
|
}
|
|
300
418
|
|
|
301
|
-
/** @type {comq.
|
|
419
|
+
/** @type {comq.Encoding} */
|
|
302
420
|
const OCTETS = 'application/octet-stream'
|
|
303
421
|
|
|
304
|
-
/** @type {comq.
|
|
422
|
+
/** @type {comq.Encoding} */
|
|
305
423
|
const DEFAULT = 'application/msgpack'
|
|
306
424
|
|
|
307
425
|
const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
|
package/source/shards/channel.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { EventEmitter } = require('node:events')
|
|
4
3
|
const { promex, sample } = require('@toa.io/generic')
|
|
5
4
|
const events = require('../events')
|
|
5
|
+
const emitter = require('../emitter')
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @implements {comq.Channel}
|
|
@@ -30,7 +30,7 @@ class Channel {
|
|
|
30
30
|
|
|
31
31
|
#recovery = promex()
|
|
32
32
|
|
|
33
|
-
#diagnostics =
|
|
33
|
+
#diagnostics = emitter.create()
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* @param {comq.Connection[]} connections
|
|
@@ -48,7 +48,7 @@ class Channel {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
async consume (queue, consumer) {
|
|
51
|
-
await this.#any((channel) => channel.consume(queue, consumer))
|
|
51
|
+
return await this.#any((channel) => channel.consume(queue, consumer))
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async subscribe (queue, group, consumer) {
|
|
@@ -64,17 +64,22 @@ class Channel {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async fire (queue, buffer, options) {
|
|
67
|
-
|
|
67
|
+
// noinspection JSValidateTypes
|
|
68
|
+
return await this.#one((channel) => channel.fire(queue, buffer, options))
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
async seal () {
|
|
71
72
|
await this.#all((channel) => channel.seal())
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
diagnose (event, listener) {
|
|
75
76
|
this.#diagnostics.on(event, listener)
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
forget (event, listener) {
|
|
80
|
+
this.#diagnostics.off(event, listener)
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
/**
|
|
79
84
|
* @param {comq.Connection} connection
|
|
80
85
|
* @param {number} index
|
|
@@ -167,12 +172,11 @@ class Channel {
|
|
|
167
172
|
|
|
168
173
|
/**
|
|
169
174
|
* @param {(channel: comq.Channel) => void} fn
|
|
170
|
-
* @return {Promise<void>}
|
|
171
175
|
*/
|
|
172
176
|
async #any (fn) {
|
|
173
177
|
const promises = this.#apply(fn)
|
|
174
178
|
|
|
175
|
-
await Promise.any(promises)
|
|
179
|
+
return await Promise.any(promises)
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
/**
|
|
@@ -201,7 +205,6 @@ class Channel {
|
|
|
201
205
|
|
|
202
206
|
/**
|
|
203
207
|
* @param {(channel: comq.Channel) => void} fn
|
|
204
|
-
* @return {Promise<void>}
|
|
205
208
|
*/
|
|
206
209
|
async #one (fn) {
|
|
207
210
|
if (this.#pool.length === 0) await this.#recovery
|
|
@@ -209,11 +212,11 @@ class Channel {
|
|
|
209
212
|
const channel = sample(this.#pool)
|
|
210
213
|
|
|
211
214
|
try {
|
|
212
|
-
await fn(channel)
|
|
215
|
+
return await fn(channel)
|
|
213
216
|
} catch {
|
|
214
217
|
this.#remove(channel)
|
|
215
218
|
|
|
216
|
-
await this.#one(fn)
|
|
219
|
+
return await this.#one(fn)
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
222
|
}
|