comq 0.10.2 → 0.11.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/workflows/codeql.yml +3 -3
- package/.github/workflows/release.yaml +1 -1
- package/docker-compose.yaml +0 -1
- package/features/events.feature +1 -1
- package/features/rpc.feature +13 -0
- package/features/steps/.types/context.d.ts +4 -1
- package/features/steps/context.js +4 -1
- package/features/steps/events.js +1 -3
- package/features/steps/tasks.js +70 -0
- package/features/tasks.feature +14 -0
- package/features/tasks.shards.feature +14 -0
- package/lerna.json +1 -1
- package/package.json +4 -4
- package/readme.md +22 -3
- package/source/io.js +16 -4
- package/types/io.d.ts +10 -0
|
@@ -46,7 +46,7 @@ jobs:
|
|
|
46
46
|
|
|
47
47
|
# Initializes the CodeQL tools for scanning.
|
|
48
48
|
- name: Initialize CodeQL
|
|
49
|
-
uses: github/codeql-action/init@
|
|
49
|
+
uses: github/codeql-action/init@v3
|
|
50
50
|
with:
|
|
51
51
|
languages: ${{ matrix.language }}
|
|
52
52
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
@@ -60,7 +60,7 @@ jobs:
|
|
|
60
60
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
|
61
61
|
# If this step fails, then you should remove it and run the build manually (see below)
|
|
62
62
|
- name: Autobuild
|
|
63
|
-
uses: github/codeql-action/autobuild@
|
|
63
|
+
uses: github/codeql-action/autobuild@v3
|
|
64
64
|
|
|
65
65
|
# ℹ️ Command-line programs to run using the OS shell.
|
|
66
66
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
|
@@ -73,6 +73,6 @@ jobs:
|
|
|
73
73
|
# ./location_of_script_within_repo/buildscript.sh
|
|
74
74
|
|
|
75
75
|
- name: Perform CodeQL Analysis
|
|
76
|
-
uses: github/codeql-action/analyze@
|
|
76
|
+
uses: github/codeql-action/analyze@v3
|
|
77
77
|
with:
|
|
78
78
|
category: "/language:${{matrix.language}}"
|
package/docker-compose.yaml
CHANGED
package/features/events.feature
CHANGED
package/features/rpc.feature
CHANGED
|
@@ -18,6 +18,19 @@ Feature: Request-reply (RPC)
|
|
|
18
18
|
3
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
Scenario: Getting `null` reply
|
|
22
|
+
Given function replying `add_numbers` queue:
|
|
23
|
+
"""
|
|
24
|
+
() => { return null }
|
|
25
|
+
"""
|
|
26
|
+
When the consumer sends the following request to the `add_numbers` queue:
|
|
27
|
+
"""yaml
|
|
28
|
+
"""
|
|
29
|
+
Then the consumer receives the reply:
|
|
30
|
+
"""yaml
|
|
31
|
+
null
|
|
32
|
+
"""
|
|
33
|
+
|
|
21
34
|
Scenario: Request causing an exception is discarded
|
|
22
35
|
Given function replying `throw_error` queue:
|
|
23
36
|
"""
|
|
@@ -14,9 +14,12 @@ declare namespace comq.features {
|
|
|
14
14
|
published?: Buffer
|
|
15
15
|
eventsPublishedCount: number
|
|
16
16
|
eventsConsumedCount: number
|
|
17
|
-
consumed
|
|
17
|
+
consumed: Record<string, { payload: any, properties?: _amqp.Properties }>
|
|
18
18
|
consumedCount: number
|
|
19
19
|
events?: { [K in _diagnostics.Event]?: boolean }
|
|
20
|
+
enqueued?: any
|
|
21
|
+
processed: any
|
|
22
|
+
tasksProcessedCount: number
|
|
20
23
|
exception?: Error
|
|
21
24
|
consumptionPromise?: Promise<any>
|
|
22
25
|
sharded: boolean
|
|
@@ -12,11 +12,14 @@ class Context extends World {
|
|
|
12
12
|
connecting
|
|
13
13
|
requestsSent = []
|
|
14
14
|
reply
|
|
15
|
-
consumed
|
|
15
|
+
consumed = {}
|
|
16
16
|
published
|
|
17
17
|
eventsPublishedCount = 0
|
|
18
18
|
eventsConsumedCount = 0
|
|
19
19
|
events = {}
|
|
20
|
+
processed
|
|
21
|
+
enqueued
|
|
22
|
+
tasksProcessedCount = 0
|
|
20
23
|
exception
|
|
21
24
|
expected
|
|
22
25
|
sharded
|
package/features/steps/events.js
CHANGED
|
@@ -112,7 +112,7 @@ Then('the event is received',
|
|
|
112
112
|
await consumed.call(this)
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
Then('{quantity} events
|
|
115
|
+
Then('{quantity} events ha(ve)(s) been received',
|
|
116
116
|
/**
|
|
117
117
|
* @this {comq.features.Context}
|
|
118
118
|
*/
|
|
@@ -167,8 +167,6 @@ Then('all events have been received',
|
|
|
167
167
|
* @return {Promise<void>}
|
|
168
168
|
*/
|
|
169
169
|
async function consume (group, exchange) {
|
|
170
|
-
this.consumed ??= {}
|
|
171
|
-
|
|
172
170
|
const consumer = async (payload, properties) => {
|
|
173
171
|
this.consumed[group] = { payload, properties }
|
|
174
172
|
this.eventsConsumedCount++
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { Given, Then, When } = require('@cucumber/cucumber')
|
|
5
|
+
const { quantity, timeout } = require('@toa.io/generic')
|
|
6
|
+
const { randomBytes } = require('node:crypto')
|
|
7
|
+
const stream = require('node:stream')
|
|
8
|
+
|
|
9
|
+
Given('tasks from the {token} queue are being processed',
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} queue
|
|
12
|
+
* @this {comq.features.Context}
|
|
13
|
+
*/
|
|
14
|
+
async function (queue) {
|
|
15
|
+
const process = (message) => {
|
|
16
|
+
this.processed = message
|
|
17
|
+
this.tasksProcessedCount++
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await this.io.process(queue, process)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
Given('a task is sent to the {token} queue',
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} queue
|
|
26
|
+
* @this {comq.features.Context}
|
|
27
|
+
*/
|
|
28
|
+
async function (queue) {
|
|
29
|
+
this.enqueued = 'hello'
|
|
30
|
+
|
|
31
|
+
await this.io.enqueue(queue, this.enqueued)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
When('a stream of {quantity} tasks is sent to the {token} queue',
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} amountQ
|
|
37
|
+
* @param {string} exchange
|
|
38
|
+
* @this {comq.features.Context}
|
|
39
|
+
*/
|
|
40
|
+
async function (amountQ, exchange) {
|
|
41
|
+
const amount = quantity(amountQ)
|
|
42
|
+
|
|
43
|
+
function * generate () {
|
|
44
|
+
for (let i = 0; i < amount; i++) yield randomBytes(8)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tasks = stream.Readable.from(generate())
|
|
48
|
+
|
|
49
|
+
await this.io.enqueue(exchange, tasks)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
Then('the task has been received',
|
|
53
|
+
/**
|
|
54
|
+
* @this {comq.features.Context}
|
|
55
|
+
*/
|
|
56
|
+
function () {
|
|
57
|
+
assert.notEqual(this.enqueued, undefined, 'Task was not enqueued')
|
|
58
|
+
assert.equal(this.processed, this.enqueued, 'Task was not processed')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
Then('{quantity} tasks ha(ve)(s) been processed',
|
|
62
|
+
/**
|
|
63
|
+
* @this {comq.features.Context}
|
|
64
|
+
*/
|
|
65
|
+
async function (expectedQ) {
|
|
66
|
+
const expected = quantity(expectedQ)
|
|
67
|
+
|
|
68
|
+
await timeout(100) // let it process
|
|
69
|
+
assert.equal(this.tasksProcessedCount, expected, 'Not all tasks have been processed')
|
|
70
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Feature: Tasks
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given an active connection to the broker
|
|
5
|
+
|
|
6
|
+
Scenario: Sending and receiving a task
|
|
7
|
+
Given tasks from the `hello` queue are being processed
|
|
8
|
+
When a task is sent to the `hello` queue
|
|
9
|
+
Then the task has been received
|
|
10
|
+
|
|
11
|
+
Scenario: Streaming tasks
|
|
12
|
+
Given tasks from the `stream` queue are being processed
|
|
13
|
+
When a stream of 1k tasks is sent to the `stream` queue
|
|
14
|
+
Then 1k tasks have been processed
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Feature: Tasks over sharded connection
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given an active sharded connection
|
|
5
|
+
|
|
6
|
+
Scenario: Sending a task using sharded connection
|
|
7
|
+
Given tasks from the `hello` queue are being processed
|
|
8
|
+
When a task is sent to the `hello` queue
|
|
9
|
+
Then the task has been received
|
|
10
|
+
|
|
11
|
+
Scenario: Streaming tasks using sharded connection
|
|
12
|
+
Given tasks from the `sharded` queue are being processed
|
|
13
|
+
When a stream of 1k tasks is sent to the `sharded` queue
|
|
14
|
+
Then 1k tasks have been processed
|
package/lerna.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@toa.io/generic": "^1.0.0-alpha.0",
|
|
33
33
|
"@types/amqplib": "^0.10.3",
|
|
34
34
|
"@types/jest": "^29.5.8",
|
|
35
|
-
"husky": "^
|
|
35
|
+
"husky": "^9.0.11",
|
|
36
36
|
"jest": "^29.5.0",
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"randomstring": "^1.3.0",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"standard": "^17.1.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"amqplib": "^0.10.
|
|
44
|
-
"msgpackr": "^1.
|
|
43
|
+
"amqplib": "^0.10.4",
|
|
44
|
+
"msgpackr": "^1.10.2",
|
|
45
45
|
"promex": "^0.1.1",
|
|
46
46
|
"reretry": "^0.1.0"
|
|
47
47
|
},
|
package/readme.md
CHANGED
|
@@ -8,6 +8,7 @@ for distributed, eventually consistent systems running on Node.js.
|
|
|
8
8
|
- [Dynamic topology](#topology)
|
|
9
9
|
- [Request](#request)-[reply](#reply) (RPC)
|
|
10
10
|
- Events ([pub](#emission)/[sub](#consumption))
|
|
11
|
+
- [Tasks](#tasks)
|
|
11
12
|
- [Pipelines](#pipelines)
|
|
12
13
|
- [Reply streams](#reply-streams)
|
|
13
14
|
- [Content encoding](#encoding)
|
|
@@ -64,11 +65,13 @@ properties set.
|
|
|
64
65
|
the `replyTo` property of the Request. The `correlationId` property of the Reply is set to the same
|
|
65
66
|
value as in the Request.
|
|
66
67
|
|
|
67
|
-
**Event** is an AMQP message
|
|
68
|
+
**Event** is an AMQP message published to an exchange.
|
|
68
69
|
|
|
69
|
-
**
|
|
70
|
+
**Task** is an AMQP message sent to a queue without a `replyTo` property set.
|
|
70
71
|
|
|
71
|
-
**
|
|
72
|
+
**Producer** is an application role that receives Requests and Tasks, and produces Replies and Events.
|
|
73
|
+
|
|
74
|
+
**Consumer** is an application role that sends Requests and Tasks, and consumes Replies and Events.
|
|
72
75
|
|
|
73
76
|
## Reply
|
|
74
77
|
|
|
@@ -155,6 +158,22 @@ asserted.
|
|
|
155
158
|
await io.emit('numbers_added', { a: 1, b: 2 })
|
|
156
159
|
```
|
|
157
160
|
|
|
161
|
+
## Tasks
|
|
162
|
+
|
|
163
|
+
`async IO.enqueue(queue: string, payload: any, encoding?: string): void`
|
|
164
|
+
|
|
165
|
+
Publish encoded Task to the `queue`.
|
|
166
|
+
|
|
167
|
+
On the initial call, the `queue` is asserted on the Events channel using Event topology.
|
|
168
|
+
|
|
169
|
+
`async IO.process(queue: string, processor): void`
|
|
170
|
+
|
|
171
|
+
`processor` function's signature is `async? (payload: any): void`
|
|
172
|
+
|
|
173
|
+
Process decoded Task from the `queue`.
|
|
174
|
+
|
|
175
|
+
The `queue` is asserted on the Events channel using Event topology.
|
|
176
|
+
|
|
158
177
|
## Pipelines
|
|
159
178
|
|
|
160
179
|
Payloads for Requests and Events can be passed as a readable stream
|
package/source/io.js
CHANGED
|
@@ -114,13 +114,14 @@ class IO {
|
|
|
114
114
|
* @param {string} exchange
|
|
115
115
|
* @param {any} payload
|
|
116
116
|
* @param {comq.Encoding | comq.amqp.options.Publish} [encoding]
|
|
117
|
+
* @param {'publish' | 'send'} method
|
|
117
118
|
* @returns {Promise<void>}
|
|
118
119
|
*/
|
|
119
|
-
async (exchange, payload, encoding) => {
|
|
120
|
+
async (exchange, payload, encoding, method = 'publish') => {
|
|
120
121
|
if (payload instanceof stream.Readable) {
|
|
121
122
|
return transform(
|
|
122
123
|
payload,
|
|
123
|
-
(payload) => this.emit(exchange, payload, encoding),
|
|
124
|
+
(payload) => this.emit(exchange, payload, encoding, method),
|
|
124
125
|
this.#events
|
|
125
126
|
)
|
|
126
127
|
}
|
|
@@ -138,9 +139,20 @@ class IO {
|
|
|
138
139
|
|
|
139
140
|
properties.contentType = contentType
|
|
140
141
|
|
|
141
|
-
await this.#events
|
|
142
|
+
await this.#events[method](exchange, buffer, properties)
|
|
142
143
|
})
|
|
143
144
|
|
|
145
|
+
process = lazy(this, this.#createEventChannel,
|
|
146
|
+
async (queue, callback) => {
|
|
147
|
+
const consumer = this.#getEventConsumer(callback)
|
|
148
|
+
|
|
149
|
+
await this.#events.consume(queue, consumer)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
enqueue (queue, payload, encoding) {
|
|
153
|
+
return this.emit(queue, payload, encoding, 'send')
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
seal = memo(async () => {
|
|
145
157
|
await this.#requests?.seal()
|
|
146
158
|
await this.#events?.seal()
|
|
@@ -218,7 +230,7 @@ class IO {
|
|
|
218
230
|
|
|
219
231
|
if (request.properties.replyTo === undefined) return
|
|
220
232
|
|
|
221
|
-
const iterator = typeof reply === 'object' &&
|
|
233
|
+
const iterator = typeof reply === 'object' && reply !== null &&
|
|
222
234
|
(Symbol.asyncIterator in reply ||
|
|
223
235
|
(Symbol.iterator in reply && !Array.isArray(reply) && !Buffer.isBuffer(reply)))
|
|
224
236
|
|
package/types/io.d.ts
CHANGED
|
@@ -43,6 +43,16 @@ declare namespace comq {
|
|
|
43
43
|
|
|
44
44
|
emit (exchange: string, stream: Readable, properties?: _amqp.Properties): Promise<void>
|
|
45
45
|
|
|
46
|
+
enqueue (exchange: string, payload: any, encoding?: _encoding.Encoding): Promise<void>
|
|
47
|
+
|
|
48
|
+
enqueue (exchange: string, payload: any, properties?: _amqp.Properties): Promise<void>
|
|
49
|
+
|
|
50
|
+
enqueue (exchange: string, stream: Readable, encoding?: _encoding.Encoding): Promise<void>
|
|
51
|
+
|
|
52
|
+
enqueue (exchange: string, stream: Readable, properties?: _amqp.Properties): Promise<void>
|
|
53
|
+
|
|
54
|
+
process<T = any> (queue: string, consumer: Consumer<T>): Promise<void>
|
|
55
|
+
|
|
46
56
|
seal (): Promise<void>
|
|
47
57
|
|
|
48
58
|
close (): Promise<void>
|