comq 0.10.0 → 0.10.2
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/workflows/release.yaml +1 -1
- package/features/rpc.streams.feature +9 -9
- package/features/rpc.streams.shards.feature +3 -3
- package/features/steps/broker.js +3 -3
- package/features/steps/events.js +2 -1
- package/features/steps/rpc.js +6 -6
- package/lerna.json +1 -1
- package/package.json +10 -9
- package/readme.md +4 -17
- package/source/.io/ReplyStream.js +12 -15
- package/source/channel.js +11 -10
- package/source/connection.js +7 -14
- package/source/io.js +56 -36
- package/source/shards/channel.js +6 -6
- package/types/io.d.ts +1 -4
- package/features/steps/.test/backpressure.test.js +0 -79
- package/features/steps/.test/broker.test.js +0 -101
- package/features/steps/.test/command.mock.js +0 -5
- package/features/steps/.test/comq.mock.js +0 -10
- package/features/steps/.test/connection.test.js +0 -285
- package/features/steps/.test/context.mock.js +0 -14
- package/features/steps/.test/context.test.js +0 -74
- package/features/steps/.test/events.test.js +0 -279
- package/features/steps/.test/io.mock.js +0 -14
- package/features/steps/.test/poison.test.js +0 -40
- package/features/steps/.test/rpc.test.js +0 -261
|
@@ -5,14 +5,14 @@ Feature: Reply streams
|
|
|
5
5
|
And heartbeat interval is set to 100ms
|
|
6
6
|
And idle timeout is set to 150ms
|
|
7
7
|
|
|
8
|
-
Scenario:
|
|
8
|
+
Scenario: Requesting a stream
|
|
9
9
|
Given function replying `get_numbers` queue:
|
|
10
10
|
"""
|
|
11
11
|
function * ({ limit }) {
|
|
12
12
|
for (let i = 0; i < limit; i++) yield i
|
|
13
13
|
}
|
|
14
14
|
"""
|
|
15
|
-
When the consumer
|
|
15
|
+
When the consumer requests a stream with the following request to the `get_numbers` queue:
|
|
16
16
|
"""yaml
|
|
17
17
|
limit: 5
|
|
18
18
|
"""
|
|
@@ -21,11 +21,11 @@ Feature: Reply streams
|
|
|
21
21
|
[0, 1, 2, 3, 4]
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
Scenario:
|
|
24
|
+
Scenario: Requesting streams concurrently
|
|
25
25
|
Given a number generator with 10ms increasing delay replying `get_numbers` queue
|
|
26
|
-
When the consumer0
|
|
26
|
+
When the consumer0 requests a stream with request to the `get_numbers` queue
|
|
27
27
|
And the consumer0 receives the stream
|
|
28
|
-
And the consumer1
|
|
28
|
+
And the consumer1 requests a stream with request to the `get_numbers` queue
|
|
29
29
|
And the consumer1 receives the stream
|
|
30
30
|
And after 300ms
|
|
31
31
|
Then the consumer0 has received the stream:
|
|
@@ -39,7 +39,7 @@ Feature: Reply streams
|
|
|
39
39
|
|
|
40
40
|
Scenario: Interrupting a stream
|
|
41
41
|
Given a number generator with 20ms increasing delay replying `get_numbers` queue
|
|
42
|
-
When the consumer
|
|
42
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
43
43
|
And the consumer interrupts the stream after 3 replies
|
|
44
44
|
# feedback message must be delivered
|
|
45
45
|
And after 10ms
|
|
@@ -48,7 +48,7 @@ Feature: Reply streams
|
|
|
48
48
|
Scenario: Reply stream idle timeout
|
|
49
49
|
Given heartbeat interval is set to 300ms
|
|
50
50
|
And a number generator with 100ms increasing delay replying `get_numbers` queue
|
|
51
|
-
When the consumer
|
|
51
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
52
52
|
Then the consumer receives the stream:
|
|
53
53
|
"""yaml
|
|
54
54
|
[0, 1, 3]
|
|
@@ -59,14 +59,14 @@ Feature: Reply streams
|
|
|
59
59
|
Scenario: Reply stream heartbeat
|
|
60
60
|
# delay will exceed the idle timeout on the 3rd reply
|
|
61
61
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
62
|
-
When the consumer
|
|
62
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
63
63
|
Then the consumer interrupts the stream after 5 replies
|
|
64
64
|
And after 200ms
|
|
65
65
|
Then the generator is destroyed
|
|
66
66
|
|
|
67
67
|
Scenario: Broker crashes while streaming reply
|
|
68
68
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
69
|
-
When the consumer
|
|
69
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
70
70
|
Then the consumer receives the stream
|
|
71
71
|
And after 300ms
|
|
72
72
|
Then the broker has crashed
|
|
@@ -5,14 +5,14 @@ Feature: Reply streams over shards
|
|
|
5
5
|
And heartbeat interval is set to 100ms
|
|
6
6
|
And idle timeout is set to 150ms
|
|
7
7
|
|
|
8
|
-
Scenario:
|
|
8
|
+
Scenario: Requesting a stream using sharded connection
|
|
9
9
|
Given a generator replying `get_numbers` queue:
|
|
10
10
|
"""
|
|
11
11
|
function * ({ amount }) {
|
|
12
12
|
for (let i = 0; i < amount; i++) yield i
|
|
13
13
|
}
|
|
14
14
|
"""
|
|
15
|
-
When the consumer
|
|
15
|
+
When the consumer requests a stream with the following request to the `get_numbers` queue:
|
|
16
16
|
"""yaml
|
|
17
17
|
amount: 5
|
|
18
18
|
"""
|
|
@@ -23,7 +23,7 @@ Feature: Reply streams over shards
|
|
|
23
23
|
|
|
24
24
|
Scenario: Broker crashes while fetching a stream
|
|
25
25
|
Given a number generator with 90ms increasing delay replying `get_numbers` queue
|
|
26
|
-
When the consumer
|
|
26
|
+
When the consumer requests a stream with request to the `get_numbers` queue
|
|
27
27
|
And the consumer receives the stream
|
|
28
28
|
And after 300ms
|
|
29
29
|
And the broker has crashed
|
package/features/steps/broker.js
CHANGED
|
@@ -32,7 +32,7 @@ Given('one of the brokers is/has {status}',
|
|
|
32
32
|
const actions = {
|
|
33
33
|
up: async (n = 0) => {
|
|
34
34
|
await execute('docker start comq-rmq-' + n)
|
|
35
|
-
await healthy()
|
|
35
|
+
await healthy(n)
|
|
36
36
|
},
|
|
37
37
|
down: async (n = 0) => {
|
|
38
38
|
await execute('docker stop comq-rmq-' + n)
|
|
@@ -42,13 +42,13 @@ const actions = {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async function healthy () {
|
|
45
|
+
async function healthy (n) {
|
|
46
46
|
let process
|
|
47
47
|
|
|
48
48
|
do {
|
|
49
49
|
await timeout(HEALTHCHECK_INTERVAL)
|
|
50
50
|
|
|
51
|
-
process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-
|
|
51
|
+
process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-' + n)
|
|
52
52
|
} while (process.output !== 'healthy')
|
|
53
53
|
}
|
|
54
54
|
|
package/features/steps/events.js
CHANGED
|
@@ -4,7 +4,7 @@ const stream = require('node:stream')
|
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const { randomBytes } = require('node:crypto')
|
|
6
6
|
const { timeout, quantity, match } = require('@toa.io/generic')
|
|
7
|
-
const { parse } = require('
|
|
7
|
+
const { load: parse } = require('js-yaml')
|
|
8
8
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
9
9
|
|
|
10
10
|
Given('(that ){token} is consuming events from the {token} exchange',
|
|
@@ -64,6 +64,7 @@ When('an event is emitted to the {token} exchange with properties:',
|
|
|
64
64
|
|
|
65
65
|
When('a stream of {quantity} events is emitted to the {token} exchange',
|
|
66
66
|
/**
|
|
67
|
+
* @param {string} amountQ
|
|
67
68
|
* @param {string} exchange
|
|
68
69
|
* @this {comq.features.Context}
|
|
69
70
|
*/
|
package/features/steps/rpc.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const stream = require('node:stream')
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const { randomBytes } = require('node:crypto')
|
|
6
|
-
const { parse } = require('
|
|
6
|
+
const { load: parse } = require('js-yaml')
|
|
7
7
|
const { match, timeout, quantity } = require('@toa.io/generic')
|
|
8
8
|
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
9
9
|
const { Readable } = require('node:stream')
|
|
@@ -136,7 +136,7 @@ When('the consumer sends a request to the {token} queue',
|
|
|
136
136
|
await send.call(this, queue, payload)
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
When('the consumer
|
|
139
|
+
When('the consumer requests a stream with the following request to the {token} queue:',
|
|
140
140
|
/**
|
|
141
141
|
* @param {string} queue
|
|
142
142
|
* @param {string} yaml
|
|
@@ -148,7 +148,7 @@ When('the consumer fetches a stream with the following request to the {token} qu
|
|
|
148
148
|
await fetch.call(this, queue, payload)
|
|
149
149
|
})
|
|
150
150
|
|
|
151
|
-
When('the consumer
|
|
151
|
+
When('the consumer requests a stream with request to the {token} queue',
|
|
152
152
|
/**
|
|
153
153
|
* @param {string} queue
|
|
154
154
|
* @this {comq.features.Context}
|
|
@@ -157,7 +157,7 @@ When('the consumer fetches a stream with request to the {token} queue',
|
|
|
157
157
|
await fetch.call(this, queue, null)
|
|
158
158
|
})
|
|
159
159
|
|
|
160
|
-
When('the consumer{number}
|
|
160
|
+
When('the consumer{number} requests a stream with request to the {token} queue',
|
|
161
161
|
/**
|
|
162
162
|
* @param {number} number
|
|
163
163
|
* @param {string} queue
|
|
@@ -408,9 +408,9 @@ async function send (queue, payload) {
|
|
|
408
408
|
*/
|
|
409
409
|
async function fetch (queue, payload, number) {
|
|
410
410
|
if (number === undefined) {
|
|
411
|
-
this.stream = await this.io.
|
|
411
|
+
this.stream = await this.io.request(queue, payload)
|
|
412
412
|
} else {
|
|
413
|
-
this.streams[number] = await this.io.
|
|
413
|
+
this.streams[number] = await this.io.request(queue, payload)
|
|
414
414
|
this.streamsValues[number] = []
|
|
415
415
|
this.streamsEnded[number] = false
|
|
416
416
|
}
|
package/lerna.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -27,27 +27,28 @@
|
|
|
27
27
|
"main": "source/index.js",
|
|
28
28
|
"types": "types/index.d.ts",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@cucumber/cucumber": "^
|
|
30
|
+
"@cucumber/cucumber": "^10.0.1",
|
|
31
31
|
"@toa.io/command": "^0.4.0",
|
|
32
|
-
"@toa.io/
|
|
33
|
-
"@
|
|
34
|
-
"@types/
|
|
35
|
-
"@types/jest": "^29.5.2",
|
|
32
|
+
"@toa.io/generic": "^1.0.0-alpha.0",
|
|
33
|
+
"@types/amqplib": "^0.10.3",
|
|
34
|
+
"@types/jest": "^29.5.8",
|
|
36
35
|
"husky": "^8.0.3",
|
|
37
36
|
"jest": "^29.5.0",
|
|
37
|
+
"js-yaml": "^4.1.0",
|
|
38
38
|
"randomstring": "^1.3.0",
|
|
39
39
|
"snazzy": "^9.0.0",
|
|
40
40
|
"standard": "^17.1.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@toa.io/generic": "^0.4.0",
|
|
44
43
|
"amqplib": "^0.10.3",
|
|
45
|
-
"msgpackr": "^1.9.5"
|
|
44
|
+
"msgpackr": "^1.9.5",
|
|
45
|
+
"promex": "^0.1.1",
|
|
46
|
+
"reretry": "^0.1.0"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
49
|
"prepare": "echo run: husky install",
|
|
49
50
|
"test": "standard && jest",
|
|
50
|
-
"features": "cucumber-js",
|
|
51
|
+
"features": "cucumber-js --fail-fast",
|
|
51
52
|
"lint": "standard --fix --verbose | snazzy"
|
|
52
53
|
},
|
|
53
54
|
"standard": {
|
package/readme.md
CHANGED
|
@@ -137,12 +137,6 @@ auto-generated name.
|
|
|
137
137
|
// with a consumer function
|
|
138
138
|
await io.consume('numbers_added', 'logger',
|
|
139
139
|
({ a, b }) => console.log(`${a} was added to ${b}`))
|
|
140
|
-
|
|
141
|
-
// as a stream
|
|
142
|
-
const events = await io.consume('numbers_added', 'logger')
|
|
143
|
-
|
|
144
|
-
for await (const { a, b } of events)
|
|
145
|
-
console.log(`${a} was added to ${b}`)
|
|
146
140
|
```
|
|
147
141
|
|
|
148
142
|
## Emission
|
|
@@ -202,12 +196,10 @@ await io.reply('get_numbers', function * ({ amount }) {
|
|
|
202
196
|
})
|
|
203
197
|
```
|
|
204
198
|
|
|
205
|
-
The Reply stream may be consumed by using the `IO.
|
|
206
|
-
|
|
207
|
-
`async IO.fetch(queue: string, payload: any, encoding?: string): Readable`
|
|
199
|
+
The Reply stream may be consumed by using the `IO.request` function:
|
|
208
200
|
|
|
209
201
|
```javascript
|
|
210
|
-
const stream = await io.
|
|
202
|
+
const stream = await io.request('get_numbers', { amount: 10 })
|
|
211
203
|
|
|
212
204
|
for await (const number of stream)
|
|
213
205
|
console.log(number)
|
|
@@ -220,8 +212,6 @@ for await (const number of stream)
|
|
|
220
212
|
<img alt="Reply topology" width="500" height="411" src="./docs/reply-stream-topology-light.jpg">
|
|
221
213
|
</picture>
|
|
222
214
|
|
|
223
|
-
On the first call of the `IO.fetch` for each request queue, an exclusive queue for replies is asserted (a stream queue).
|
|
224
|
-
|
|
225
215
|
When the producer function of `IO.reply` returns an Iterator for the first time across all request queues,
|
|
226
216
|
a control queue is asserted on the [Reply channel](#channels)
|
|
227
217
|
using the [reply topology](#exchanges-and-queues).
|
|
@@ -235,7 +225,7 @@ the request will be retransmitted upon reconnection.
|
|
|
235
225
|
|
|
236
226
|
A heartbeat message is sent to the `replyTo` queue whenever a Reply stream idles for 5 seconds.
|
|
237
227
|
If the Consumer of the reply stream doesn't receive a reply or a heartbeat message for 12 seconds, the stream returned
|
|
238
|
-
by `IO.
|
|
228
|
+
by `IO.request` is destroyed.
|
|
239
229
|
These intervals are not configurable.
|
|
240
230
|
|
|
241
231
|
An "end stream" message is sent to the `replyTo` queue when the Reply stream is finished.
|
|
@@ -247,7 +237,7 @@ See also [Reply stream shutdown](#reply-stream-shutdown).
|
|
|
247
237
|
:warning:
|
|
248
238
|
|
|
249
239
|
While consuming the Reply stream if the broker connection is lost,
|
|
250
|
-
or if the Consumer crashes or destroys the stream returned by `IO.
|
|
240
|
+
or if the Consumer crashes or destroys the stream returned by `IO.request`,
|
|
251
241
|
some of the values yielded by the Reply stream may be lost.
|
|
252
242
|
|
|
253
243
|
To avoid inconsistency, it is strongly recommended to use the Reply stream only with _safe_ Producers, which do not
|
|
@@ -354,9 +344,6 @@ and it will be closed when the last instance of `IO` using that connection is [d
|
|
|
354
344
|
Topology is designed to deliver maximum performance while ensuring that the **at least once**
|
|
355
345
|
guarantee provided by RabbitMQ is maintained.
|
|
356
346
|
|
|
357
|
-
> Topology design assumes that unanswered Requests and unconfirmed Events will be retransmitted
|
|
358
|
-
> upon reconnection.
|
|
359
|
-
|
|
360
347
|
### Dynamic
|
|
361
348
|
|
|
362
349
|
Static topology refers to the process of defining the complete topology declaration along with the
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { IDLE_INTERVAL, control } = require('./const')
|
|
4
|
-
|
|
5
3
|
const { Readable } = require('node:stream')
|
|
4
|
+
const { Promex } = require('promex')
|
|
5
|
+
const { IDLE_INTERVAL, control } = require('./const')
|
|
6
6
|
|
|
7
7
|
class ReplyStream extends Readable {
|
|
8
|
-
|
|
8
|
+
confirmation = new Promex()
|
|
9
9
|
|
|
10
10
|
/** @type {ReturnType<setTimeout> | null} */
|
|
11
11
|
#timeout = null
|
|
@@ -26,8 +26,8 @@ class ReplyStream extends Readable {
|
|
|
26
26
|
|
|
27
27
|
#buffered = 0
|
|
28
28
|
|
|
29
|
-
/** @type {
|
|
30
|
-
#queue =
|
|
29
|
+
/** @type {Map<number, unknown>} */
|
|
30
|
+
#queue = new Map()
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* @param {comq.Request} request
|
|
@@ -36,14 +36,12 @@ class ReplyStream extends Readable {
|
|
|
36
36
|
constructor (request, reply) {
|
|
37
37
|
super({ objectMode: true })
|
|
38
38
|
|
|
39
|
-
this.#confirmation = request.reply
|
|
40
39
|
this.#emitter = request.emitter
|
|
41
40
|
this.#correlationId = request.properties.correlationId
|
|
42
41
|
this.#idleInterval = global['COMQ_TESTING_IDLE_INTERVAL'] || IDLE_INTERVAL
|
|
43
42
|
this.#reply = reply
|
|
44
43
|
|
|
45
|
-
this.#emitter.on(this.#correlationId, this.
|
|
46
|
-
this.#confirmation.catch(this._clear.bind(this))
|
|
44
|
+
this.#emitter.on(this.#correlationId, this.arrange.bind(this))
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
_destroy (error, callback) {
|
|
@@ -59,9 +57,8 @@ class ReplyStream extends Readable {
|
|
|
59
57
|
/**
|
|
60
58
|
* @param {unknown} payload
|
|
61
59
|
* @param {comq.amqp.Properties} properties
|
|
62
|
-
* @private
|
|
63
60
|
*/
|
|
64
|
-
|
|
61
|
+
arrange (payload, properties) {
|
|
65
62
|
if (properties.headers.index !== this.#index) {
|
|
66
63
|
this._buffer(payload, properties)
|
|
67
64
|
|
|
@@ -73,12 +70,12 @@ class ReplyStream extends Readable {
|
|
|
73
70
|
if (this.#buffered > 0) {
|
|
74
71
|
let message
|
|
75
72
|
|
|
76
|
-
while ((message = this.#queue
|
|
77
|
-
|
|
73
|
+
while ((message = this.#queue.get(this.#index))) {
|
|
74
|
+
this.#queue.delete(this.#index)
|
|
78
75
|
this._add(message.payload, message.properties)
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
this.#buffered =
|
|
78
|
+
this.#buffered = this.#queue.size
|
|
82
79
|
}
|
|
83
80
|
}
|
|
84
81
|
|
|
@@ -101,7 +98,7 @@ class ReplyStream extends Readable {
|
|
|
101
98
|
if (this.#buffered > MAX_BUFFER_SIZE) this.destroy()
|
|
102
99
|
|
|
103
100
|
this.#buffered++
|
|
104
|
-
this.#queue
|
|
101
|
+
this.#queue.set(properties.headers.index, { payload, properties })
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
/**
|
|
@@ -113,7 +110,7 @@ class ReplyStream extends Readable {
|
|
|
113
110
|
switch (message) {
|
|
114
111
|
case control.ok:
|
|
115
112
|
this.#control = { properties }
|
|
116
|
-
this
|
|
113
|
+
this.confirmation.resolve()
|
|
117
114
|
break
|
|
118
115
|
case control.heartbeat:
|
|
119
116
|
break
|
package/source/channel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { Promex } = require('promex')
|
|
4
4
|
const { failsafe, lazy, recall } = require('./attributes')
|
|
5
5
|
const emitter = require('./emitter')
|
|
6
6
|
|
|
@@ -25,16 +25,16 @@ class Channel {
|
|
|
25
25
|
/** @type {string[]} */
|
|
26
26
|
#tags = []
|
|
27
27
|
|
|
28
|
-
/** @type {
|
|
28
|
+
/** @type {Promex | null} */
|
|
29
29
|
#paused = null
|
|
30
30
|
|
|
31
31
|
/** @type {boolean} */
|
|
32
32
|
#sealed = false
|
|
33
33
|
|
|
34
|
-
/** @type {
|
|
35
|
-
#recovery =
|
|
34
|
+
/** @type {Promex} */
|
|
35
|
+
#recovery = new Promex()
|
|
36
36
|
|
|
37
|
-
/** @type {Set<
|
|
37
|
+
/** @type {Set<Promex>} */
|
|
38
38
|
#confirmations = new Set()
|
|
39
39
|
|
|
40
40
|
#diagnostics = emitter.create()
|
|
@@ -149,10 +149,11 @@ class Channel {
|
|
|
149
149
|
|
|
150
150
|
for (const confirmation of this.#confirmations) confirmation.reject(INTERRUPTION)
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
// handle interruptions
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
153
154
|
|
|
154
155
|
this.#recovery.resolve()
|
|
155
|
-
this.#recovery =
|
|
156
|
+
this.#recovery = new Promex()
|
|
156
157
|
this.#diagnostics.emit('recover')
|
|
157
158
|
}
|
|
158
159
|
|
|
@@ -222,10 +223,10 @@ class Channel {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
/**
|
|
225
|
-
* @return {
|
|
226
|
+
* @return {Promex}
|
|
226
227
|
*/
|
|
227
228
|
#confirmation () {
|
|
228
|
-
const confirmation =
|
|
229
|
+
const confirmation = new Promex()
|
|
229
230
|
|
|
230
231
|
this.#confirmations.add(confirmation)
|
|
231
232
|
|
|
@@ -292,7 +293,7 @@ class Channel {
|
|
|
292
293
|
#pause () {
|
|
293
294
|
if (this.#paused !== null) return
|
|
294
295
|
|
|
295
|
-
this.#paused =
|
|
296
|
+
this.#paused = new Promex()
|
|
296
297
|
this.#diagnostics.emit('flow')
|
|
297
298
|
this.#diagnostics.emit('pause')
|
|
298
299
|
}
|
package/source/connection.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const amqp = require('amqplib')
|
|
4
|
-
const {
|
|
4
|
+
const { Promex } = require('promex')
|
|
5
|
+
const { retry } = require('reretry')
|
|
5
6
|
|
|
6
7
|
const { failsafe } = require('./attributes')
|
|
7
8
|
const presets = require('./topology')
|
|
@@ -21,8 +22,8 @@ class Connection {
|
|
|
21
22
|
/** @type {comq.Channel[]} */
|
|
22
23
|
#channels = []
|
|
23
24
|
|
|
24
|
-
/** @type {
|
|
25
|
-
#recovery =
|
|
25
|
+
/** @type {Promex} */
|
|
26
|
+
#recovery = new Promex()
|
|
26
27
|
|
|
27
28
|
/** @type {boolean} */
|
|
28
29
|
#running = false
|
|
@@ -37,7 +38,7 @@ class Connection {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
async open () {
|
|
40
|
-
await retry(this.#open
|
|
41
|
+
await retry(this.#open)
|
|
41
42
|
|
|
42
43
|
this.#running = true
|
|
43
44
|
}
|
|
@@ -69,14 +70,11 @@ class Connection {
|
|
|
69
70
|
this.#diagnostics.on(event, listener)
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
/**
|
|
73
|
-
* @type {toa.generic.retry.Task}
|
|
74
|
-
*/
|
|
75
73
|
#open = async (retry) => {
|
|
76
74
|
try {
|
|
77
75
|
this.#connection = await amqp.connect(this.#url)
|
|
78
76
|
} catch (exception) {
|
|
79
|
-
if (this.#transient(exception)) return retry
|
|
77
|
+
if (this.#transient(exception)) return retry
|
|
80
78
|
else throw exception
|
|
81
79
|
}
|
|
82
80
|
|
|
@@ -90,7 +88,7 @@ class Connection {
|
|
|
90
88
|
for (const channel of this.#channels) await channel.recover(this.#connection)
|
|
91
89
|
|
|
92
90
|
this.#recovery.resolve()
|
|
93
|
-
this.#recovery =
|
|
91
|
+
this.#recovery = new Promex()
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
/**
|
|
@@ -117,9 +115,4 @@ class Connection {
|
|
|
117
115
|
|
|
118
116
|
function noop () {}
|
|
119
117
|
|
|
120
|
-
/** @type {toa.generic.retry.Options} */
|
|
121
|
-
const RETRY = {
|
|
122
|
-
retries: Infinity
|
|
123
|
-
}
|
|
124
|
-
|
|
125
118
|
exports.Connection = Connection
|
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 { setTimeout } = require('node:timers/promises')
|
|
7
|
+
const { Promex } = require('promex')
|
|
7
8
|
const { memo, failsafe, lazy, track } = require('./attributes')
|
|
8
9
|
|
|
9
10
|
const { decode } = require('./decode')
|
|
@@ -32,9 +33,9 @@ class IO {
|
|
|
32
33
|
#emitters = new Map()
|
|
33
34
|
|
|
34
35
|
/** @type {comq.ReplyEmitter | null} */
|
|
35
|
-
#
|
|
36
|
+
#control = null
|
|
36
37
|
|
|
37
|
-
/** @type {Set<
|
|
38
|
+
/** @type {Set<Promex>} */
|
|
38
39
|
#pendingReplies = new Set()
|
|
39
40
|
|
|
40
41
|
/** @type {Set<comq.Destroyable>} */
|
|
@@ -86,33 +87,12 @@ class IO {
|
|
|
86
87
|
)
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
const { buffer, properties, emitter, reply } = this.#createRequest(queue, payload, encoding)
|
|
90
|
-
|
|
91
|
-
emitter.once(properties.correlationId, reply.resolve)
|
|
92
|
-
|
|
93
|
-
await this.#requests.send(queue, buffer, properties)
|
|
94
|
-
|
|
95
|
-
return reply
|
|
96
|
-
}))
|
|
97
|
-
|
|
98
|
-
fetch = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
|
|
99
|
-
failsafe(this, this.#recover,
|
|
100
|
-
/**
|
|
101
|
-
* @param {string} queue
|
|
102
|
-
* @param {any} payload
|
|
103
|
-
* @param {comq.Encoding} [encoding]
|
|
104
|
-
* @returns {Promise<Readable>}
|
|
105
|
-
*/
|
|
106
|
-
async (queue, payload, encoding) => {
|
|
107
90
|
const request = this.#createRequest(queue, payload, encoding)
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
this.#addReplyStream(stream)
|
|
91
|
+
const reply = this.#createReply(request)
|
|
111
92
|
|
|
112
93
|
await this.#requests.send(queue, request.buffer, request.properties)
|
|
113
|
-
await request.reply // wait for the confirmation
|
|
114
94
|
|
|
115
|
-
return
|
|
95
|
+
return reply
|
|
116
96
|
}))
|
|
117
97
|
|
|
118
98
|
consume = lazy(this, this.#createEventChannel,
|
|
@@ -247,9 +227,9 @@ class IO {
|
|
|
247
227
|
? reply
|
|
248
228
|
: stream.Readable.from(reply)
|
|
249
229
|
|
|
250
|
-
this.#
|
|
230
|
+
this.#control ??= await this.#createControl()
|
|
251
231
|
|
|
252
|
-
const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#
|
|
232
|
+
const pipe = await io.ReplyPipe.create(request, readable, this.#replies, this.#control,
|
|
253
233
|
(message, properties) => this.#reply(request, message, properties))
|
|
254
234
|
|
|
255
235
|
this.#addReplyPipe(pipe)
|
|
@@ -291,19 +271,30 @@ class IO {
|
|
|
291
271
|
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
292
272
|
const emitter = this.#emitters.get(queue)
|
|
293
273
|
const correlationId = randomBytes(8).toString('hex')
|
|
294
|
-
const reply = this.#createReply()
|
|
295
274
|
|
|
296
275
|
/** @type {comq.amqp.Properties} */
|
|
297
276
|
const properties = { contentType, correlationId, replyTo: emitter.queue }
|
|
298
277
|
|
|
299
|
-
return { buffer, emitter,
|
|
278
|
+
return { buffer, emitter, properties }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {comq.Request} request
|
|
283
|
+
* @return {Promex<any>}
|
|
284
|
+
*/
|
|
285
|
+
#createReply (request) {
|
|
286
|
+
const reply = this.#createPendingReply()
|
|
287
|
+
|
|
288
|
+
request.emitter.once(request.properties.correlationId, this.#getReplyResolver(request, reply))
|
|
289
|
+
|
|
290
|
+
return reply
|
|
300
291
|
}
|
|
301
292
|
|
|
302
293
|
/**
|
|
303
|
-
* @return {
|
|
294
|
+
* @return {Promex}
|
|
304
295
|
*/
|
|
305
|
-
#
|
|
306
|
-
const reply =
|
|
296
|
+
#createPendingReply () {
|
|
297
|
+
const reply = new Promex()
|
|
307
298
|
|
|
308
299
|
this.#pendingReplies.add(reply)
|
|
309
300
|
|
|
@@ -314,11 +305,40 @@ class IO {
|
|
|
314
305
|
return reply
|
|
315
306
|
}
|
|
316
307
|
|
|
308
|
+
/**
|
|
309
|
+
* @param {comq.Request} request
|
|
310
|
+
* @param reply
|
|
311
|
+
*/
|
|
312
|
+
#getReplyResolver (request, reply) {
|
|
313
|
+
return async (payload, properties) => {
|
|
314
|
+
const isStream = properties.headers?.index !== undefined
|
|
315
|
+
|
|
316
|
+
if (isStream) {
|
|
317
|
+
const stream = this.#createReplyStream(request, payload, properties)
|
|
318
|
+
|
|
319
|
+
await stream.confirmation
|
|
320
|
+
|
|
321
|
+
reply.resolve(stream)
|
|
322
|
+
} else {
|
|
323
|
+
reply.resolve(payload)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#createReplyStream (request, payload, properties) {
|
|
329
|
+
const stream = new io.ReplyStream(request, this.#reply.bind(this))
|
|
330
|
+
|
|
331
|
+
stream.arrange(payload, properties)
|
|
332
|
+
this.#addReplyStream(/** @type {comq.Destroyable} */ stream)
|
|
333
|
+
|
|
334
|
+
return stream
|
|
335
|
+
}
|
|
336
|
+
|
|
317
337
|
/**
|
|
318
338
|
* @return {Promise<comq.ReplyEmitter>}
|
|
319
339
|
*/
|
|
320
|
-
async #
|
|
321
|
-
const queue = '
|
|
340
|
+
async #createControl () {
|
|
341
|
+
const queue = 'control'
|
|
322
342
|
|
|
323
343
|
await this.#consumeReplies(queue)
|
|
324
344
|
|
|
@@ -364,7 +384,7 @@ class IO {
|
|
|
364
384
|
Even if these messages are lost, the reply stream will be closed anyway,
|
|
365
385
|
either due to missing heartbeat or the deletion of the stream queue.
|
|
366
386
|
*/
|
|
367
|
-
await
|
|
387
|
+
await setTimeout(50)
|
|
368
388
|
}
|
|
369
389
|
|
|
370
390
|
/**
|