comq 0.5.3-dev.0 → 0.6.0-dev.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 -3
- package/docs/headers.md +17 -0
- package/features/properties.feature +29 -0
- package/features/steps/.test/events.test.js +70 -10
- package/features/steps/.types/context.d.ts +4 -3
- package/features/steps/events.js +55 -12
- package/lerna.json +1 -1
- package/package.json +2 -2
- package/readme.md +3 -3
- package/source/io.js +55 -33
- package/test/channel.test.js +4 -1
- package/test/io.consume.test.js +15 -2
- package/test/io.emit.test.js +26 -0
- package/test/io.reply.test.js +26 -15
- package/test/io.request.test.js +0 -2
- package/types/amqp.d.ts +2 -0
- package/types/diagnostic.d.ts +5 -0
- package/types/io.d.ts +20 -5
package/.github/dependabot.yml
CHANGED
|
@@ -6,12 +6,11 @@ updates:
|
|
|
6
6
|
interval: "weekly"
|
|
7
7
|
target-branch: "dev"
|
|
8
8
|
labels:
|
|
9
|
-
- "
|
|
10
|
-
- "actions"
|
|
9
|
+
- "Bump"
|
|
11
10
|
- package-ecosystem: "npm"
|
|
12
11
|
directory: "/"
|
|
13
12
|
schedule:
|
|
14
13
|
interval: "daily"
|
|
15
14
|
target-branch: "dev"
|
|
16
15
|
labels:
|
|
17
|
-
- "
|
|
16
|
+
- "Bump"
|
package/docs/headers.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Message Properties
|
|
2
|
+
|
|
3
|
+
Events may be emitted and consumed with custom properties using an optional `properties` argument.
|
|
4
|
+
|
|
5
|
+
## Emission
|
|
6
|
+
|
|
7
|
+
`async IO.emit(exchange: string, payload: any, [properties: comq.amqp.Properties]): void`
|
|
8
|
+
|
|
9
|
+
`properties` are passed
|
|
10
|
+
to [amqplib.publish](https://amqp-node.github.io/amqplib/channel_api.html#channel_publish).
|
|
11
|
+
|
|
12
|
+
## Consumption
|
|
13
|
+
|
|
14
|
+
`async IO.consume(exchange: string, [group: string], consumer): void`
|
|
15
|
+
|
|
16
|
+
`consumer` function's signature
|
|
17
|
+
is `async? (payload: any, [properties: comq.amqp.Properties]): void`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Feature: Message properties
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given an active connection to the broker
|
|
5
|
+
|
|
6
|
+
Scenario: Sending and receiving an event with headers
|
|
7
|
+
Given that `checker` is consuming events from the `numbers_added` exchange
|
|
8
|
+
When an event is emitted to the `numbers_added` exchange with properties:
|
|
9
|
+
"""yaml
|
|
10
|
+
headers:
|
|
11
|
+
foo: bar
|
|
12
|
+
"""
|
|
13
|
+
Then `checker` receives the event with properties:
|
|
14
|
+
"""yaml
|
|
15
|
+
contentType: application/octet-stream # standard properties are available
|
|
16
|
+
headers:
|
|
17
|
+
foo: bar
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
Scenario: Sending and receiving an event with AMQP properties
|
|
21
|
+
Given that `checker` is consuming events from the `numbers_added` exchange
|
|
22
|
+
When an event is emitted to the `numbers_added` exchange with properties:
|
|
23
|
+
"""yaml
|
|
24
|
+
appId: test
|
|
25
|
+
"""
|
|
26
|
+
Then `checker` receives the event with properties:
|
|
27
|
+
"""yaml
|
|
28
|
+
appId: test
|
|
29
|
+
"""
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { AssertionError } = require('node:assert')
|
|
4
|
+
const { randomBytes } = require('node:crypto')
|
|
4
5
|
const { generate } = require('randomstring')
|
|
5
6
|
const { promex, timeout, random } = require('@toa.io/generic')
|
|
6
7
|
const tomato = require('@toa.io/tomato')
|
|
8
|
+
const { dump } = require('@toa.io/yaml')
|
|
7
9
|
const { io } = require('./io.mock')
|
|
8
10
|
const mock = { tomato }
|
|
9
11
|
|
|
@@ -47,7 +49,16 @@ describe('Given (that ){token} is consuming events from the {token} exchange', (
|
|
|
47
49
|
|
|
48
50
|
await consumer(payload)
|
|
49
51
|
|
|
50
|
-
expect(context.consumed[group]).toStrictEqual(payload)
|
|
52
|
+
expect(context.consumed[group]).toStrictEqual(expect.objectContaining({ payload }))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should store event properties', async () => {
|
|
56
|
+
const payload = generate()
|
|
57
|
+
const properties = generate()
|
|
58
|
+
|
|
59
|
+
await consumer(payload, properties)
|
|
60
|
+
|
|
61
|
+
expect(context.consumed[group]).toStrictEqual({ payload, properties })
|
|
51
62
|
})
|
|
52
63
|
})
|
|
53
64
|
|
|
@@ -79,13 +90,13 @@ describe('When an event is emitted to the {token} exchange', () => {
|
|
|
79
90
|
})
|
|
80
91
|
|
|
81
92
|
it('should emit event', async () => {
|
|
82
|
-
expect(io.emit).
|
|
93
|
+
expect(io.emit.mock.calls[0][0]).toStrictEqual(exchange)
|
|
83
94
|
})
|
|
84
95
|
|
|
85
96
|
it('should wait for context.expected', async () => {
|
|
86
97
|
jest.clearAllMocks()
|
|
87
98
|
|
|
88
|
-
context.
|
|
99
|
+
context.consumptionPromise = promex()
|
|
89
100
|
|
|
90
101
|
let resolved = false
|
|
91
102
|
|
|
@@ -93,7 +104,7 @@ describe('When an event is emitted to the {token} exchange', () => {
|
|
|
93
104
|
expect(io.emit).not.toHaveBeenCalled()
|
|
94
105
|
|
|
95
106
|
resolved = true
|
|
96
|
-
context.
|
|
107
|
+
context.consumptionPromise.resolve()
|
|
97
108
|
})
|
|
98
109
|
|
|
99
110
|
await step.call(context, exchange)
|
|
@@ -103,20 +114,42 @@ describe('When an event is emitted to the {token} exchange', () => {
|
|
|
103
114
|
})
|
|
104
115
|
})
|
|
105
116
|
|
|
117
|
+
describe('When an event is emitted to the {token} exchange with properties:', () => {
|
|
118
|
+
const step = tomato.steps.Wh('an event is emitted to the {token} exchange with properties:')
|
|
119
|
+
|
|
120
|
+
it('should be', async () => undefined)
|
|
121
|
+
|
|
122
|
+
const exchange = generate()
|
|
123
|
+
const properties = { [generate()]: generate() }
|
|
124
|
+
const yaml = dump(properties)
|
|
125
|
+
|
|
126
|
+
beforeEach(async () => {
|
|
127
|
+
await step.call(context, exchange, yaml)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should store event', async () => {
|
|
131
|
+
expect(context.published).toBeDefined()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should emit event', async () => {
|
|
135
|
+
expect(io.emit).toHaveBeenCalledWith(exchange, expect.anything(), properties)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
106
139
|
describe('Then {token} receives the event', () => {
|
|
107
140
|
const step = tomato.steps.Th('{token} receives the event')
|
|
108
141
|
|
|
109
|
-
const payload =
|
|
142
|
+
const payload = randomBytes(8)
|
|
110
143
|
const group = generate()
|
|
111
144
|
|
|
112
145
|
beforeEach(() => {
|
|
113
|
-
context.consumed = { [group]: payload }
|
|
146
|
+
context.consumed = { [group]: { payload } }
|
|
114
147
|
})
|
|
115
148
|
|
|
116
149
|
it('should be', async () => undefined)
|
|
117
150
|
|
|
118
151
|
it('should throw if not consumed', async () => {
|
|
119
|
-
context.published =
|
|
152
|
+
context.published = randomBytes(8)
|
|
120
153
|
|
|
121
154
|
await expect(step.call(context, group)).rejects.toThrow(AssertionError)
|
|
122
155
|
})
|
|
@@ -128,20 +161,47 @@ describe('Then {token} receives the event', () => {
|
|
|
128
161
|
})
|
|
129
162
|
})
|
|
130
163
|
|
|
164
|
+
describe('Then {token} receives the event with properties:', () => {
|
|
165
|
+
const step = tomato.steps.Th('{token} receives the event with properties:')
|
|
166
|
+
|
|
167
|
+
it('should be', async () => undefined)
|
|
168
|
+
|
|
169
|
+
const payload = randomBytes(8)
|
|
170
|
+
const properties = { headers: { [generate()]: generate() } }
|
|
171
|
+
const yaml = dump(properties)
|
|
172
|
+
const group = generate()
|
|
173
|
+
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
context.consumed = { [group]: { payload, properties } }
|
|
176
|
+
context.published = payload
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should throw if does not match', async () => {
|
|
180
|
+
const properties = { headers: { [generate()]: generate() } }
|
|
181
|
+
const yaml = dump(properties)
|
|
182
|
+
|
|
183
|
+
await expect(step.call(context, group, yaml)).rejects.toThrow(AssertionError)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should pass if matches', async () => {
|
|
187
|
+
await expect(step.call(context, group, yaml)).resolves.not.toThrow()
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
131
191
|
describe('Then the event is received', () => {
|
|
132
192
|
const step = tomato.steps.Th('the event is received')
|
|
133
193
|
|
|
134
|
-
const payload =
|
|
194
|
+
const payload = randomBytes(8)
|
|
135
195
|
const group = undefined
|
|
136
196
|
|
|
137
197
|
beforeEach(() => {
|
|
138
|
-
context.consumed = { [group]: payload }
|
|
198
|
+
context.consumed = { [group]: { payload } }
|
|
139
199
|
})
|
|
140
200
|
|
|
141
201
|
it('should be', async () => undefined)
|
|
142
202
|
|
|
143
203
|
it('should throw if not consumed', async () => {
|
|
144
|
-
context.published =
|
|
204
|
+
context.published = randomBytes(8)
|
|
145
205
|
|
|
146
206
|
await expect(step.call(context, group)).rejects.toThrow(AssertionError)
|
|
147
207
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _diagnostics from '../../../types/diagnostic'
|
|
2
2
|
import * as _io from '../../../types/io'
|
|
3
|
+
import * as _amqp from '../../../types/amqp'
|
|
3
4
|
|
|
4
5
|
declare namespace comq.features {
|
|
5
6
|
|
|
@@ -9,14 +10,14 @@ declare namespace comq.features {
|
|
|
9
10
|
connecting: Promise<any>
|
|
10
11
|
requestsSent: Promise<any>[]
|
|
11
12
|
reply?: Promise<any>
|
|
12
|
-
published?:
|
|
13
|
+
published?: Buffer
|
|
13
14
|
eventsPublishedCount: number
|
|
14
15
|
eventsConsumedCount: number
|
|
15
|
-
consumed?: Record<string, any>
|
|
16
|
+
consumed?: Record<string, { payload: any, properties?: _amqp.Properties }>
|
|
16
17
|
consumedCount: number
|
|
17
18
|
events?: { [K in _diagnostics.event]?: boolean }
|
|
18
19
|
exception?: Error
|
|
19
|
-
|
|
20
|
+
consumptionPromise?: Promise<any>
|
|
20
21
|
sharded: boolean
|
|
21
22
|
shard: number
|
|
22
23
|
sealing: Promise<any>
|
package/features/steps/events.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
-
const { generate } = require('randomstring')
|
|
5
|
-
const { timeout, quantity } = require('@toa.io/generic')
|
|
6
|
-
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
7
4
|
const { randomBytes } = require('node:crypto')
|
|
5
|
+
const { timeout, quantity, match } = require('@toa.io/generic')
|
|
6
|
+
const { parse } = require('@toa.io/yaml')
|
|
7
|
+
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
8
8
|
|
|
9
9
|
Given('(that ){token} is consuming events from the {token} exchange',
|
|
10
10
|
/**
|
|
@@ -25,7 +25,7 @@ Given('{token} consuming events from the {token} exchange is expected',
|
|
|
25
25
|
async function (group, exchange) {
|
|
26
26
|
await timeout(500) // let it crash
|
|
27
27
|
|
|
28
|
-
this.
|
|
28
|
+
this.consumptionPromise = consume.call(this, group, exchange)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
Given('(that )events are exclusively consumed from the {token} exchange',
|
|
@@ -34,7 +34,7 @@ Given('(that )events are exclusively consumed from the {token} exchange',
|
|
|
34
34
|
* @this {comq.features.Context}
|
|
35
35
|
*/
|
|
36
36
|
async function (exchange) {
|
|
37
|
-
this.
|
|
37
|
+
this.consumptionPromise = consume.call(this, undefined, exchange)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
When('an event is emitted to the {token} exchange',
|
|
@@ -43,13 +43,22 @@ When('an event is emitted to the {token} exchange',
|
|
|
43
43
|
* @this {comq.features.Context}
|
|
44
44
|
*/
|
|
45
45
|
async function (exchange) {
|
|
46
|
-
|
|
46
|
+
const message = randomBytes(8)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
await emit.call(this, exchange, message)
|
|
49
|
+
})
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
When('an event is emitted to the {token} exchange with properties:',
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} exchange
|
|
54
|
+
* @param {string} yaml
|
|
55
|
+
* @this {comq.features.Context}
|
|
56
|
+
*/
|
|
57
|
+
async function (exchange, yaml) {
|
|
58
|
+
const message = randomBytes(8)
|
|
59
|
+
const properties = parse(yaml)
|
|
51
60
|
|
|
52
|
-
this
|
|
61
|
+
await emit.call(this, exchange, message, properties)
|
|
53
62
|
})
|
|
54
63
|
|
|
55
64
|
Then('{token} receives the event',
|
|
@@ -61,6 +70,21 @@ Then('{token} receives the event',
|
|
|
61
70
|
await consumed.call(this, group)
|
|
62
71
|
})
|
|
63
72
|
|
|
73
|
+
Then('{token} receives the event with properties:',
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} group
|
|
76
|
+
* @param {string} yaml
|
|
77
|
+
* @this {comq.features.Context}
|
|
78
|
+
*/
|
|
79
|
+
async function (group, yaml) {
|
|
80
|
+
await consumed.call(this, group)
|
|
81
|
+
|
|
82
|
+
const properties = parse(yaml)
|
|
83
|
+
const matches = match(this.consumed[group].properties, properties)
|
|
84
|
+
|
|
85
|
+
assert.equal(matches, true, 'Consumed event properties doesn\'t match')
|
|
86
|
+
})
|
|
87
|
+
|
|
64
88
|
Then('the event is received',
|
|
65
89
|
/**
|
|
66
90
|
* @this {comq.features.Context}
|
|
@@ -115,8 +139,8 @@ Then('all events have been received',
|
|
|
115
139
|
async function consume (group, exchange) {
|
|
116
140
|
this.consumed ??= {}
|
|
117
141
|
|
|
118
|
-
const consumer = async (payload) => {
|
|
119
|
-
this.consumed[group] = payload
|
|
142
|
+
const consumer = async (payload, properties) => {
|
|
143
|
+
this.consumed[group] = { payload, properties }
|
|
120
144
|
this.eventsConsumedCount++
|
|
121
145
|
}
|
|
122
146
|
|
|
@@ -133,5 +157,24 @@ async function consumed (group) {
|
|
|
133
157
|
await timeout(100) // let it consume
|
|
134
158
|
|
|
135
159
|
assert.notEqual(this.published, undefined, 'No event has been published')
|
|
136
|
-
|
|
160
|
+
|
|
161
|
+
const consumed = this.consumed[group]
|
|
162
|
+
const equals = this.published.equals(consumed.payload)
|
|
163
|
+
|
|
164
|
+
assert.equal(equals, true, 'Consumed event doesn\'t match')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string} exchange
|
|
169
|
+
* @param {any} message
|
|
170
|
+
* @param {import('amqplib').Options.Publish} [properties]
|
|
171
|
+
* @return {Promise<void>}
|
|
172
|
+
* @this {comq.features.Context}
|
|
173
|
+
*/
|
|
174
|
+
async function emit (exchange, message, properties) {
|
|
175
|
+
if (this.consumptionPromise) await this.consumptionPromise
|
|
176
|
+
|
|
177
|
+
await this.io.emit(exchange, message, properties)
|
|
178
|
+
|
|
179
|
+
this.published = message
|
|
137
180
|
}
|
package/lerna.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-dev.0",
|
|
4
4
|
"description": "ComQ",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Artem Gurtovoi",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"prepare": "husky install",
|
|
50
50
|
"test": "standard && jest",
|
|
51
|
-
"
|
|
51
|
+
"features": "cucumber-js",
|
|
52
52
|
"lint": "standard --fix --verbose | snazzy"
|
|
53
53
|
},
|
|
54
54
|
"standard": {
|
package/readme.md
CHANGED
|
@@ -83,11 +83,11 @@ The Reply message is encoded using the same encoding format as the Request messa
|
|
|
83
83
|
`application/octet-stream` and the `producer` function returns something other than a `Buffer`, an
|
|
84
84
|
exception will be thrown.
|
|
85
85
|
|
|
86
|
-
> If the incoming message does not have a `replyTo` property, an exception is thrown without
|
|
87
|
-
> calling `producer`.
|
|
88
|
-
|
|
89
86
|
> The `replyTo` queue is not asserted, as it is expected to be done by the Consumer.
|
|
90
87
|
|
|
88
|
+
> If the incoming message does not have a `replyTo` property, the result of the `producer` is
|
|
89
|
+
> ignored.
|
|
90
|
+
|
|
91
91
|
### Example
|
|
92
92
|
|
|
93
93
|
```javascript
|
package/source/io.js
CHANGED
|
@@ -25,8 +25,8 @@ class IO {
|
|
|
25
25
|
/** @type {comq.Channel} */
|
|
26
26
|
#events
|
|
27
27
|
|
|
28
|
-
/** @type {
|
|
29
|
-
#emitters =
|
|
28
|
+
/** @type {Map<string, comq.ReplyEmitter>} */
|
|
29
|
+
#emitters = new Map()
|
|
30
30
|
|
|
31
31
|
/** @type {Set<toa.generic.Promex>} */
|
|
32
32
|
#pendingReplies = new Set()
|
|
@@ -68,11 +68,12 @@ class IO {
|
|
|
68
68
|
*/
|
|
69
69
|
async (queue, payload, encoding, replyToFormatter) => {
|
|
70
70
|
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
71
|
-
const
|
|
72
|
-
const emitter = this.#emitters[queue]
|
|
73
|
-
const replyTo = replyToFormatter?.(emitter.queue) ?? emitter.queue
|
|
74
|
-
const properties = { contentType, correlationId, replyTo }
|
|
71
|
+
const emitter = this.#emitters.get(queue)
|
|
75
72
|
const reply = this.#createReply()
|
|
73
|
+
const correlationId = randomBytes(8).toString('hex')
|
|
74
|
+
const properties = { contentType, correlationId }
|
|
75
|
+
|
|
76
|
+
properties.replyTo = replyToFormatter?.(emitter.queue) ?? emitter.queue
|
|
76
77
|
|
|
77
78
|
emitter.once(correlationId, reply.resolve)
|
|
78
79
|
|
|
@@ -99,12 +100,22 @@ class IO {
|
|
|
99
100
|
/**
|
|
100
101
|
* @param {string} exchange
|
|
101
102
|
* @param {any} payload
|
|
102
|
-
* @param {comq.encoding} [encoding]
|
|
103
|
+
* @param {comq.encoding | comq.amqp.options.Publish} [encoding]
|
|
103
104
|
* @returns {Promise<void>}
|
|
104
105
|
*/
|
|
105
106
|
async (exchange, payload, encoding) => {
|
|
107
|
+
/** @type {comq.amqp.options.Publish} */
|
|
108
|
+
const properties = {}
|
|
109
|
+
|
|
110
|
+
if (typeof encoding === 'object') { // properties passed
|
|
111
|
+
Object.assign(properties, encoding)
|
|
112
|
+
|
|
113
|
+
encoding = /** @type {comq.encoding} */ properties.contentType
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
107
|
-
|
|
117
|
+
|
|
118
|
+
properties.contentType = contentType
|
|
108
119
|
|
|
109
120
|
await this.#events.publish(exchange, buffer, properties)
|
|
110
121
|
})
|
|
@@ -141,7 +152,7 @@ class IO {
|
|
|
141
152
|
const emitter = io.createReplyEmitter(queue)
|
|
142
153
|
const consumer = this.#getReplyConsumer(queue, emitter)
|
|
143
154
|
|
|
144
|
-
this.#emitters
|
|
155
|
+
this.#emitters.set(queue, emitter)
|
|
145
156
|
|
|
146
157
|
await this.#replies.consume(emitter.queue, consumer)
|
|
147
158
|
}
|
|
@@ -173,24 +184,36 @@ class IO {
|
|
|
173
184
|
* @returns {comq.channels.consumer}
|
|
174
185
|
*/
|
|
175
186
|
#getRequestConsumer = (producer) =>
|
|
176
|
-
track(this,
|
|
177
|
-
|
|
187
|
+
track(this,
|
|
188
|
+
/**
|
|
189
|
+
* @param {comq.amqp.Message} request
|
|
190
|
+
* @returns {Promise<void>}
|
|
191
|
+
*/
|
|
192
|
+
async (request) => {
|
|
193
|
+
const payload = decode(request)
|
|
194
|
+
const reply = await producer(payload)
|
|
178
195
|
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
if ('replyTo' in request.properties) await this.#reply(request, reply)
|
|
197
|
+
})
|
|
181
198
|
|
|
182
|
-
|
|
199
|
+
/**
|
|
200
|
+
* @param {comq.amqp.Message} request
|
|
201
|
+
* @param {any} reply
|
|
202
|
+
* @returns {Promise<void>}
|
|
203
|
+
*/
|
|
204
|
+
async #reply (request, reply) {
|
|
205
|
+
if (reply === undefined) throw new Error('The `producer` function must return a value')
|
|
183
206
|
|
|
184
|
-
|
|
207
|
+
let { replyTo, correlationId, contentType } = request.properties
|
|
185
208
|
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
if (Buffer.isBuffer(reply)) contentType = OCTETS
|
|
210
|
+
if (contentType === undefined) throw new Error('Reply to a Request without the `contentType` property must be of type `Buffer`')
|
|
188
211
|
|
|
189
|
-
|
|
190
|
-
|
|
212
|
+
const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
|
|
213
|
+
const properties = { contentType, correlationId }
|
|
191
214
|
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
await this.#replies.fire(replyTo, buffer, properties)
|
|
216
|
+
}
|
|
194
217
|
|
|
195
218
|
/**
|
|
196
219
|
* @param {string} queue
|
|
@@ -198,9 +221,6 @@ class IO {
|
|
|
198
221
|
* @returns {comq.channels.consumer}
|
|
199
222
|
*/
|
|
200
223
|
#getReplyConsumer = (queue, emitter) =>
|
|
201
|
-
/**
|
|
202
|
-
* @param {comq.amqp.Message} message
|
|
203
|
-
*/
|
|
204
224
|
(message) => {
|
|
205
225
|
const payload = decode(message)
|
|
206
226
|
|
|
@@ -215,7 +235,7 @@ class IO {
|
|
|
215
235
|
track(this, async (message) => {
|
|
216
236
|
const payload = decode(message)
|
|
217
237
|
|
|
218
|
-
await callback(payload)
|
|
238
|
+
await callback(payload, message.properties)
|
|
219
239
|
})
|
|
220
240
|
|
|
221
241
|
/**
|
|
@@ -226,7 +246,9 @@ class IO {
|
|
|
226
246
|
|
|
227
247
|
this.#pendingReplies.add(reply)
|
|
228
248
|
|
|
229
|
-
reply
|
|
249
|
+
reply
|
|
250
|
+
.catch(noop)
|
|
251
|
+
.finally(() => this.#pendingReplies.delete(reply))
|
|
230
252
|
|
|
231
253
|
return reply
|
|
232
254
|
}
|
|
@@ -236,7 +258,7 @@ class IO {
|
|
|
236
258
|
}
|
|
237
259
|
|
|
238
260
|
#retransmit = () => {
|
|
239
|
-
for (const emitter of
|
|
261
|
+
for (const emitter of this.#emitters.values()) emitter.clear()
|
|
240
262
|
|
|
241
263
|
// trigger failsafe attribute
|
|
242
264
|
for (const reply of this.#pendingReplies) reply.reject(RETRANSMISSION)
|
|
@@ -244,17 +266,17 @@ class IO {
|
|
|
244
266
|
|
|
245
267
|
/**
|
|
246
268
|
* @param {any} payload
|
|
247
|
-
* @param {comq.encoding} [
|
|
248
|
-
* @returns [Buffer,
|
|
269
|
+
* @param {comq.encoding} [contentType]
|
|
270
|
+
* @returns [Buffer, comq.encoding]
|
|
249
271
|
*/
|
|
250
|
-
#encode (payload,
|
|
272
|
+
#encode (payload, contentType) {
|
|
251
273
|
const raw = Buffer.isBuffer(payload)
|
|
252
274
|
|
|
253
|
-
|
|
275
|
+
contentType ??= raw ? OCTETS : DEFAULT
|
|
254
276
|
|
|
255
|
-
const buffer = raw ? payload : encode(payload,
|
|
277
|
+
const buffer = raw ? payload : encode(payload, contentType)
|
|
256
278
|
|
|
257
|
-
return [buffer,
|
|
279
|
+
return [buffer, contentType]
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
282
|
|
package/test/channel.test.js
CHANGED
|
@@ -712,6 +712,9 @@ describe('recovery', () => {
|
|
|
712
712
|
|
|
713
713
|
const repl = await getCreatedChannel(replacement)
|
|
714
714
|
|
|
715
|
+
// await recovery effect
|
|
716
|
+
await timeout(5)
|
|
717
|
+
|
|
715
718
|
expect(repl.publish).toHaveBeenCalled()
|
|
716
719
|
})
|
|
717
720
|
|
|
@@ -747,7 +750,7 @@ describe('recovery', () => {
|
|
|
747
750
|
|
|
748
751
|
const repl = await getCreatedChannel(replacement)
|
|
749
752
|
|
|
750
|
-
await timeout(
|
|
753
|
+
await timeout(5)
|
|
751
754
|
|
|
752
755
|
expect(repl.publish).toHaveBeenCalledWith(
|
|
753
756
|
exchange, '', buffer, expect.objectContaining(options), expect.any(Function)
|
package/test/io.consume.test.js
CHANGED
|
@@ -68,7 +68,20 @@ describe('group consumption', () => {
|
|
|
68
68
|
|
|
69
69
|
await callback(message)
|
|
70
70
|
|
|
71
|
-
expect(consumer).
|
|
71
|
+
expect(consumer.mock.calls[0][0]).toStrictEqual(payload)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should pass properties', async () => {
|
|
75
|
+
const payload = generate()
|
|
76
|
+
const contentType = 'text/plain'
|
|
77
|
+
const content = encode(payload, contentType)
|
|
78
|
+
const properties = { contentType }
|
|
79
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
80
|
+
const callback = events.subscribe.mock.calls[0][2]
|
|
81
|
+
|
|
82
|
+
await callback(message)
|
|
83
|
+
|
|
84
|
+
expect(consumer).toHaveBeenCalledWith(payload, properties)
|
|
72
85
|
})
|
|
73
86
|
})
|
|
74
87
|
|
|
@@ -94,7 +107,7 @@ describe.each(['omitted', 'undefined'])('exclusive consumption (group is %s)', (
|
|
|
94
107
|
|
|
95
108
|
await emit(message)
|
|
96
109
|
|
|
97
|
-
expect(consumer).
|
|
110
|
+
expect(consumer.mock.calls[0][0]).toStrictEqual(message.content)
|
|
98
111
|
})
|
|
99
112
|
})
|
|
100
113
|
|
package/test/io.emit.test.js
CHANGED
|
@@ -83,12 +83,38 @@ it('should publish Buffer with specified encoding format', async () => {
|
|
|
83
83
|
const encoding = 'application/json'
|
|
84
84
|
|
|
85
85
|
await io.emit(exchange, payload, encoding)
|
|
86
|
+
|
|
86
87
|
const [, buffer, properties] = events.publish.mock.calls[1]
|
|
87
88
|
|
|
88
89
|
expect(buffer).toStrictEqual(payload)
|
|
89
90
|
expect(properties.contentType).toStrictEqual(encoding)
|
|
90
91
|
})
|
|
91
92
|
|
|
93
|
+
it('should publish a message with specified properties', async () => {
|
|
94
|
+
const payload = randomBytes(8)
|
|
95
|
+
|
|
96
|
+
/** @type {comq.amqp.Properties} */
|
|
97
|
+
const properties = { headers: { [generate()]: generate() } }
|
|
98
|
+
|
|
99
|
+
await io.emit(exchange, payload, properties)
|
|
100
|
+
|
|
101
|
+
const [, , options] = events.publish.mock.calls[1]
|
|
102
|
+
|
|
103
|
+
expect(options).toStrictEqual(expect.objectContaining(properties))
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should publish a message with headers', async () => {
|
|
107
|
+
const message = generate()
|
|
108
|
+
const headers = { [generate()]: generate() }
|
|
109
|
+
const properties = { headers }
|
|
110
|
+
|
|
111
|
+
await io.emit(exchange, message, properties)
|
|
112
|
+
|
|
113
|
+
const [, , options] = events.publish.mock.calls[1]
|
|
114
|
+
|
|
115
|
+
expect(options.headers).toStrictEqual(headers)
|
|
116
|
+
})
|
|
117
|
+
|
|
92
118
|
/**
|
|
93
119
|
* @param {comq.topology.type} type
|
|
94
120
|
* @returns {jest.MockedObject<comq.Channel>}
|
package/test/io.reply.test.js
CHANGED
|
@@ -70,15 +70,6 @@ describe('queues', () => {
|
|
|
70
70
|
it('should consume queue', async () => {
|
|
71
71
|
expect(requests.consume).toHaveBeenCalledWith(queue, expect.any(Function))
|
|
72
72
|
})
|
|
73
|
-
|
|
74
|
-
it('should throw if message is missing replyTo', async () => {
|
|
75
|
-
const content = randomBytes(10)
|
|
76
|
-
const properties = {}
|
|
77
|
-
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
78
|
-
const producer = requests.consume.mock.calls[0][1]
|
|
79
|
-
|
|
80
|
-
await expect(producer(message)).rejects.toThrow('is missing the `replyTo` property')
|
|
81
|
-
})
|
|
82
73
|
})
|
|
83
74
|
|
|
84
75
|
describe('reply', () => {
|
|
@@ -89,14 +80,13 @@ describe('reply', () => {
|
|
|
89
80
|
let replies
|
|
90
81
|
|
|
91
82
|
beforeEach(async () => {
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('should `fire` reply', async () => {
|
|
95
83
|
await io.reply(queue, produce)
|
|
96
84
|
|
|
97
85
|
requests = await findChannel('request')
|
|
98
86
|
replies = await findChannel('reply')
|
|
87
|
+
})
|
|
99
88
|
|
|
89
|
+
it('should `fire` reply', async () => {
|
|
100
90
|
const content = randomBytes(10)
|
|
101
91
|
const properties = { replyTo: generate(), contentType: 'text/plain' }
|
|
102
92
|
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
@@ -111,18 +101,39 @@ describe('reply', () => {
|
|
|
111
101
|
})
|
|
112
102
|
|
|
113
103
|
it('should throw if producer returned undefined', async () => {
|
|
104
|
+
jest.clearAllMocks()
|
|
105
|
+
|
|
114
106
|
const produce = () => undefined
|
|
115
107
|
|
|
116
108
|
await io.reply(queue, produce)
|
|
117
109
|
|
|
118
|
-
const requests = await findChannel('request')
|
|
119
110
|
const callback = requests.consume.mock.calls[0][1]
|
|
120
|
-
const content =
|
|
121
|
-
const properties = { replyTo: generate() }
|
|
111
|
+
const content = generate()
|
|
112
|
+
const properties = { replyTo: generate(), contentType: 'text/plain' }
|
|
122
113
|
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
123
114
|
|
|
124
115
|
await expect(callback(message)).rejects.toThrow('must return a value')
|
|
125
116
|
})
|
|
117
|
+
|
|
118
|
+
it('should not throw if message is missing replyTo', async () => {
|
|
119
|
+
const content = generate()
|
|
120
|
+
const properties = { contentType: 'text/plain' }
|
|
121
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
122
|
+
const producer = requests.consume.mock.calls[0][1]
|
|
123
|
+
|
|
124
|
+
await expect(producer(message)).resolves.not.toThrow()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should not send reply if request has no `replyTo` property', async () => {
|
|
128
|
+
const content = generate()
|
|
129
|
+
const properties = { contentType: 'text/plain' }
|
|
130
|
+
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
131
|
+
const producer = requests.consume.mock.calls[0][1]
|
|
132
|
+
|
|
133
|
+
await producer(message)
|
|
134
|
+
|
|
135
|
+
expect(replies.fire).not.toHaveBeenCalled()
|
|
136
|
+
})
|
|
126
137
|
})
|
|
127
138
|
|
|
128
139
|
describe('encoding', () => {
|
package/test/io.request.test.js
CHANGED
package/types/amqp.d.ts
CHANGED
|
@@ -12,7 +12,9 @@ declare namespace comq.amqp {
|
|
|
12
12
|
type Connection = amqplib.Connection
|
|
13
13
|
type Channel = amqplib.Channel | amqplib.ConfirmChannel
|
|
14
14
|
type Message = amqplib.Message
|
|
15
|
+
type Properties = Partial<amqplib.MessageProperties>
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export type Connection = comq.amqp.Connection
|
|
18
19
|
export type Channel = comq.amqp.Channel
|
|
20
|
+
export type Properties = comq.amqp.Properties
|
package/types/diagnostic.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ declare namespace comq.diagnostics {
|
|
|
2
2
|
|
|
3
3
|
type event = 'open' | 'close' | 'flow' | 'drain' | 'remove' | 'recover' | 'discard'
|
|
4
4
|
|
|
5
|
+
interface Diagnosable {
|
|
6
|
+
diagnose(event: event, listener: Function): void
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
export type event = comq.diagnostics.event
|
|
12
|
+
export type Diagnosable = comq.diagnostics.Diagnosable
|
package/types/io.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as _diagnostics from './diagnostic'
|
|
2
2
|
import * as _encoding from './encoding'
|
|
3
|
+
import * as _topology from './topology'
|
|
4
|
+
import * as _amqp from './amqp'
|
|
3
5
|
|
|
4
6
|
declare namespace comq {
|
|
5
7
|
|
|
6
|
-
type producer = (message: any) => Promise<any>
|
|
7
|
-
type consumer = (message: any) => Promise<void>
|
|
8
|
+
type producer = (message: any) => any | Promise<any>
|
|
9
|
+
type consumer = (message: any, headers?: _amqp.Properties) => void | Promise<void>
|
|
8
10
|
|
|
9
11
|
interface ReplyEmitter {
|
|
10
12
|
queue: string
|
|
@@ -18,7 +20,7 @@ declare namespace comq {
|
|
|
18
20
|
|
|
19
21
|
type ReplyToPropertyFormatter = (queue: string) => string
|
|
20
22
|
|
|
21
|
-
interface IO {
|
|
23
|
+
interface IO extends _diagnostics.Diagnosable {
|
|
22
24
|
reply(queue: string, produce: producer): Promise<void>
|
|
23
25
|
|
|
24
26
|
request(
|
|
@@ -34,13 +36,26 @@ declare namespace comq {
|
|
|
34
36
|
|
|
35
37
|
emit(exchange: string, payload: any, encoding?: _encoding.encoding): Promise<void>
|
|
36
38
|
|
|
39
|
+
emit(exchange: string, payload: any, properties?: _amqp.Properties): Promise<void>
|
|
40
|
+
|
|
37
41
|
seal(): Promise<void>
|
|
38
42
|
|
|
39
43
|
close(): Promise<void>
|
|
40
44
|
|
|
41
|
-
diagnose(event:
|
|
42
|
-
|
|
45
|
+
diagnose(event: 'open', listener: (index?: number) => void)
|
|
46
|
+
|
|
47
|
+
diagnose(event: 'close', listener: (index?: number) => void)
|
|
48
|
+
|
|
49
|
+
diagnose(event: 'flow', listener: (channel: _topology.type, index?: number) => void)
|
|
43
50
|
|
|
51
|
+
diagnose(event: 'drain', listener: (channel: _topology.type, index?: number) => void)
|
|
52
|
+
|
|
53
|
+
diagnose(event: 'remove', listener: (index?: number) => void)
|
|
54
|
+
|
|
55
|
+
diagnose(event: 'recover', listener: (channel: _topology.type, index?: number) => void)
|
|
56
|
+
|
|
57
|
+
diagnose(event: 'discard', listener: (channel: _topology.type, message: any, index?: number) => void)
|
|
58
|
+
}
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
export type producer = comq.producer
|