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.
@@ -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@v2
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@v2
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@v2
76
+ uses: github/codeql-action/analyze@v3
77
77
  with:
78
78
  category: "/language:${{matrix.language}}"
@@ -23,7 +23,7 @@ jobs:
23
23
  - name: Setup Node
24
24
  uses: actions/setup-node@v4
25
25
  with:
26
- node-version: 16
26
+ node-version: 20
27
27
  registry-url: 'https://registry.npmjs.org'
28
28
 
29
29
  - name: Checkout
@@ -1,4 +1,3 @@
1
- version: "3.9"
2
1
  services:
3
2
  rabbitmq-0:
4
3
  image: rabbitmq:3.10.0-management
@@ -23,4 +23,4 @@ Feature: Events
23
23
  Scenario: Streaming requests
24
24
  Given that events are exclusively consumed from the `numbers_added` exchange
25
25
  When a stream of 1k events is emitted to the `numbers_added` exchange
26
- Then 1k events is received
26
+ Then 1k events have been received
@@ -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?: Record<string, { payload: any, properties?: _amqp.Properties }>
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
@@ -112,7 +112,7 @@ Then('the event is received',
112
112
  await consumed.call(this)
113
113
  })
114
114
 
115
- Then('{quantity} events is received',
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
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.10.2",
2
+ "version": "0.11.0",
3
3
  "conventionalCommits": true
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.10.2",
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": "^8.0.3",
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.3",
44
- "msgpackr": "^1.9.5",
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 that is published to an exchange.
68
+ **Event** is an AMQP message published to an exchange.
68
69
 
69
- **Producer** is an application role that receives Requests and produces Replies and Events.
70
+ **Task** is an AMQP message sent to a queue without a `replyTo` property set.
70
71
 
71
- **Consumer** is an application role that sends Requests and consumes Replies and Events.
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.publish(exchange, buffer, properties)
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>