comq 0.16.2 → 0.17.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/features/events.routed.feature +19 -0
- package/features/steps/events.routed.js +55 -0
- package/package.json +1 -1
- package/readme.md +37 -1
- package/source/channel.js +60 -0
- package/source/io.js +59 -9
- package/test/channel.routed.test.js +119 -0
- package/test/connection.mock.js +2 -0
- package/test/io.route.test.js +94 -0
- package/types/channel.d.ts +6 -0
- package/types/io.d.ts +14 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Feature: Routed events
|
|
2
|
+
|
|
3
|
+
An exchange that routes by key, where an event exchange fans out: a message reaches the
|
|
4
|
+
queues bound under the key it was published with, and none of the others.
|
|
5
|
+
|
|
6
|
+
Background:
|
|
7
|
+
Given an active connection to the broker
|
|
8
|
+
|
|
9
|
+
Scenario: Receiving an event by its routing key
|
|
10
|
+
Given that `orders` is bound to the `records` exchange under the `placed` key
|
|
11
|
+
When a message is routed to the `records` exchange under the `placed` key
|
|
12
|
+
Then `orders` receives the event
|
|
13
|
+
|
|
14
|
+
Scenario: Not receiving an event bound under another key
|
|
15
|
+
Given that `orders` is bound to the `records` exchange under the `placed` key
|
|
16
|
+
And `customers` is bound to the `records` exchange under the `shipped` key
|
|
17
|
+
When a message is routed to the `records` exchange under the `placed` key
|
|
18
|
+
Then `orders` receives the event
|
|
19
|
+
And `customers` receives nothing
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { randomBytes } = require('node:crypto')
|
|
5
|
+
const { timeout } = require('../../test/helpers')
|
|
6
|
+
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
7
|
+
|
|
8
|
+
Given('(that ){token} is bound to the {token} exchange under the {token} key',
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} queue
|
|
11
|
+
* @param {string} exchange
|
|
12
|
+
* @param {string} key
|
|
13
|
+
* @this {comq.features.Context}
|
|
14
|
+
*/
|
|
15
|
+
async function (queue, exchange, key) {
|
|
16
|
+
await subscribe.call(this, queue, exchange, key)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
When('a message is routed to the {token} exchange under the {token} key',
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} exchange
|
|
22
|
+
* @param {string} key
|
|
23
|
+
* @this {comq.features.Context}
|
|
24
|
+
*/
|
|
25
|
+
async function (exchange, key) {
|
|
26
|
+
const message = randomBytes(8)
|
|
27
|
+
|
|
28
|
+
await this.io.route(exchange, key, message)
|
|
29
|
+
|
|
30
|
+
this.published = message
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
Then('{token} receives nothing',
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} queue
|
|
36
|
+
* @this {comq.features.Context}
|
|
37
|
+
*/
|
|
38
|
+
async function (queue) {
|
|
39
|
+
await timeout(100) // let it not consume
|
|
40
|
+
|
|
41
|
+
assert.equal(this.consumed[queue], undefined, `'${queue}' has received an event`)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} queue
|
|
46
|
+
* @param {string} exchange
|
|
47
|
+
* @param {string} key
|
|
48
|
+
* @this {comq.features.Context}
|
|
49
|
+
*/
|
|
50
|
+
async function subscribe (queue, exchange, key) {
|
|
51
|
+
await this.io.subscribe(exchange, queue, key, async (payload, properties) => {
|
|
52
|
+
this.consumed[queue] = { payload, properties }
|
|
53
|
+
this.eventsConsumedCount++
|
|
54
|
+
})
|
|
55
|
+
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -7,7 +7,7 @@ for distributed, eventually consistent systems running on Node.js.
|
|
|
7
7
|
|
|
8
8
|
- [Dynamic topology](#topology)
|
|
9
9
|
- [Request](#request)-[reply](#reply) (RPC)
|
|
10
|
-
- Events ([pub](#emission)/[sub](#consumption))
|
|
10
|
+
- Events ([pub](#emission)/[sub](#consumption)), fanned out or [routed](#routing)
|
|
11
11
|
- [Tasks](#tasks)
|
|
12
12
|
- [Pipelines](#pipelines)
|
|
13
13
|
- [Reply streams](#reply-streams)
|
|
@@ -67,6 +67,9 @@ value as in the Request.
|
|
|
67
67
|
|
|
68
68
|
**Event** is an AMQP message published to an exchange.
|
|
69
69
|
|
|
70
|
+
**Key** is the routing key an Event is published with, and the one a queue is bound under. An
|
|
71
|
+
Event published to a fanout exchange carries none.
|
|
72
|
+
|
|
70
73
|
**Task** is an AMQP message sent to a queue without a `replyTo` property set.
|
|
71
74
|
|
|
72
75
|
**Producer** is an application role that receives Requests and Tasks, and produces Replies and Events.
|
|
@@ -158,6 +161,36 @@ asserted.
|
|
|
158
161
|
await io.emit('numbers_added', { a: 1, b: 2 })
|
|
159
162
|
```
|
|
160
163
|
|
|
164
|
+
## Routing
|
|
165
|
+
|
|
166
|
+
`async IO.route(exchange: string, key: string, payload: any, encoding?: string): void`
|
|
167
|
+
|
|
168
|
+
`async IO.subscribe(exchange: string, queue: string, key: string, consumer): void`
|
|
169
|
+
|
|
170
|
+
Publish and consume Events addressed by a Key, where [Emission](#emission)
|
|
171
|
+
and [Consumption](#consumption) fan out.
|
|
172
|
+
|
|
173
|
+
`route` asserts a
|
|
174
|
+
[direct exchange](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchange-direct) (once per
|
|
175
|
+
unique `exchange`) and publishes the encoded Event to it under the `key`. `subscribe` asserts the
|
|
176
|
+
same exchange and a durable `queue`, binds it under the `key` (once per unique `exchange`, `queue`
|
|
177
|
+
and `key`), and starts consuming. **An Event reaches the queues bound under the Key it carries,
|
|
178
|
+
and no others.**
|
|
179
|
+
|
|
180
|
+
The queue is named rather than derived from a Consumer group, because what identifies it here is
|
|
181
|
+
the Key it is bound under rather than the exchange it belongs to. It is also durable, so what is
|
|
182
|
+
published while nothing is consuming is held rather than dropped.
|
|
183
|
+
|
|
184
|
+
### Example
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
await io.subscribe('records', 'records.orders', 'store.orders',
|
|
188
|
+
(record) => console.log(record))
|
|
189
|
+
|
|
190
|
+
await io.route('records', 'store.orders', { id: 1, status: 'paid' })
|
|
191
|
+
await io.route('records', 'store.customers', { id: 2 }) // not delivered to the above
|
|
192
|
+
```
|
|
193
|
+
|
|
161
194
|
## Tasks
|
|
162
195
|
|
|
163
196
|
`async IO.enqueue(queue: string, payload: any, encoding?: string): void`
|
|
@@ -409,6 +442,9 @@ requests and are expecting replies.
|
|
|
409
442
|
|
|
410
443
|
- Exchanges and queues for Events, and queues for Requests
|
|
411
444
|
are _durable_.
|
|
445
|
+
- An exchange is asserted as _fanout_ for [Emission](#emission)
|
|
446
|
+
and [Consumption](#consumption), and as _direct_ for [Routing](#routing). One name is one or
|
|
447
|
+
the other: asserting it as both is what the broker refuses.
|
|
412
448
|
- Queues for Replies are _exclusive_ and _auto deleted_.
|
|
413
449
|
|
|
414
450
|
See [queue assertion options](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue).
|
package/source/channel.js
CHANGED
|
@@ -101,6 +101,24 @@ class Channel {
|
|
|
101
101
|
if (!this.#sealed) await this.#consume(queue, callback)
|
|
102
102
|
})))
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Consumes a named queue bound to a direct exchange under a routing key, where `subscribe`
|
|
106
|
+
* binds to a fanout and takes everything published to it.
|
|
107
|
+
*/
|
|
108
|
+
bound = recall(this,
|
|
109
|
+
failsafe(this, this.#recover,
|
|
110
|
+
lazy(this, [this.#assertRouted, this.#assertKeyedQueue],
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} exchange
|
|
113
|
+
* @param {string} queue
|
|
114
|
+
* @param {string} key
|
|
115
|
+
* @param {comq.channels.Consumer} callback
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async (exchange, queue, key, callback) => {
|
|
119
|
+
if (!this.#sealed) await this.#consume(queue, callback)
|
|
120
|
+
})))
|
|
121
|
+
|
|
104
122
|
send = failsafe(this, this.#recover,
|
|
105
123
|
lazy(this, this.#assertQueue,
|
|
106
124
|
/**
|
|
@@ -123,6 +141,21 @@ class Channel {
|
|
|
123
141
|
await this.#publish(exchange, DEFAULT, buffer, options)
|
|
124
142
|
}))
|
|
125
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Publishes to a direct exchange under a routing key, where `publish` fans out.
|
|
146
|
+
*/
|
|
147
|
+
route = failsafe(this, this.#recover,
|
|
148
|
+
lazy(this, this.#assertRouted,
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} exchange
|
|
151
|
+
* @param {string} key
|
|
152
|
+
* @param {Buffer} buffer
|
|
153
|
+
* @param {comq.amqp.options.Publish} [options]
|
|
154
|
+
*/
|
|
155
|
+
async (exchange, key, buffer, options) => {
|
|
156
|
+
await this.#publish(exchange, key, buffer, options)
|
|
157
|
+
}))
|
|
158
|
+
|
|
126
159
|
async fire (queue, buffer, options) {
|
|
127
160
|
try {
|
|
128
161
|
await this.#publish(DEFAULT, queue, buffer, options)
|
|
@@ -230,6 +263,33 @@ class Channel {
|
|
|
230
263
|
await this.#channel.assertExchange(exchange, 'fanout', options)
|
|
231
264
|
}
|
|
232
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A routed exchange is `direct`: a message reaches the queues bound under the key it was
|
|
268
|
+
* published with, and none of the others.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} exchange
|
|
271
|
+
* @returns {Promise<void>}
|
|
272
|
+
*/
|
|
273
|
+
async #assertRouted (exchange) {
|
|
274
|
+
/** @type {comq.amqp.options.Exchange} */
|
|
275
|
+
const options = { durable: this.#topology.durable }
|
|
276
|
+
|
|
277
|
+
await this.#channel.assertExchange(exchange, 'direct', options)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {string} exchange
|
|
282
|
+
* @param {string} queue
|
|
283
|
+
* @param {string} key
|
|
284
|
+
* @returns {Promise<string[]>}
|
|
285
|
+
*/
|
|
286
|
+
async #assertKeyedQueue (exchange, queue, key) {
|
|
287
|
+
queue = (await this.#assertQueue(queue))[0]
|
|
288
|
+
await this.#channel.bindQueue(queue, exchange, key)
|
|
289
|
+
|
|
290
|
+
return [exchange, queue, key]
|
|
291
|
+
}
|
|
292
|
+
|
|
233
293
|
/**
|
|
234
294
|
*
|
|
235
295
|
* @param {string} exchange
|
package/source/io.js
CHANGED
|
@@ -132,20 +132,46 @@ class IO {
|
|
|
132
132
|
)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
const properties = {}
|
|
135
|
+
const [buffer, properties] = this.#properties(payload, encoding)
|
|
137
136
|
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
await this.#events[method](exchange, buffer, properties)
|
|
138
|
+
})
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Publishes to a routed exchange under a key, where `emit` fans out. The exchange is
|
|
142
|
+
* `direct`, so a message reaches the queues bound under that key and no others.
|
|
143
|
+
*/
|
|
144
|
+
route = lazy(this, this.#createEventChannel,
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} exchange
|
|
147
|
+
* @param {string} key
|
|
148
|
+
* @param {any} payload
|
|
149
|
+
* @param {comq.Encoding | comq.amqp.options.Publish} [encoding]
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async (exchange, key, payload, encoding) => {
|
|
153
|
+
const [buffer, properties] = this.#properties(payload, encoding)
|
|
143
154
|
|
|
144
|
-
|
|
155
|
+
await this.#events.route(exchange, key, buffer, properties)
|
|
156
|
+
})
|
|
145
157
|
|
|
146
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Consumes a named durable queue bound to a routed exchange under a key, where `consume`
|
|
160
|
+
* takes everything published to a fanout. The queue is named rather than derived, because
|
|
161
|
+
* what is bound to it is a key rather than an exchange.
|
|
162
|
+
*/
|
|
163
|
+
subscribe = lazy(this, this.#createEventChannel,
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} exchange
|
|
166
|
+
* @param {string} queue
|
|
167
|
+
* @param {string} key
|
|
168
|
+
* @param {comq.Consumer} callback
|
|
169
|
+
* @returns {Promise<void>}
|
|
170
|
+
*/
|
|
171
|
+
async (exchange, queue, key, callback) => {
|
|
172
|
+
const consumer = this.#getEventConsumer(callback)
|
|
147
173
|
|
|
148
|
-
await this.#events
|
|
174
|
+
await this.#events.bound(exchange, queue, key, consumer)
|
|
149
175
|
})
|
|
150
176
|
|
|
151
177
|
process = lazy(this, this.#createEventChannel,
|
|
@@ -200,6 +226,30 @@ class IO {
|
|
|
200
226
|
this.#events = await this.#createChannel('event')
|
|
201
227
|
}
|
|
202
228
|
|
|
229
|
+
/**
|
|
230
|
+
* What a publication carries, from what its caller passed as an encoding or as properties.
|
|
231
|
+
*
|
|
232
|
+
* @param {any} payload
|
|
233
|
+
* @param {comq.Encoding | comq.amqp.options.Publish} [encoding]
|
|
234
|
+
* @returns {[Buffer, comq.amqp.options.Publish]}
|
|
235
|
+
*/
|
|
236
|
+
#properties (payload, encoding) {
|
|
237
|
+
/** @type {comq.amqp.options.Publish} */
|
|
238
|
+
const properties = {}
|
|
239
|
+
|
|
240
|
+
if (typeof encoding === 'object') { // properties passed
|
|
241
|
+
Object.assign(properties, encoding)
|
|
242
|
+
|
|
243
|
+
encoding = /** @type {comq.Encoding} */ properties.contentType
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const [buffer, contentType] = this.#encode(payload, encoding)
|
|
247
|
+
|
|
248
|
+
properties.contentType = contentType
|
|
249
|
+
|
|
250
|
+
return [buffer, properties]
|
|
251
|
+
}
|
|
252
|
+
|
|
203
253
|
async #consumeReplies (queue) {
|
|
204
254
|
const emitter = io.createReplyEmitter(queue)
|
|
205
255
|
const consumer = this.#getReplyConsumer(queue, emitter)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
const { amqplib } = require('./amqplib.mock')
|
|
6
|
+
const fixtures = require('./channel.fixtures')
|
|
7
|
+
const { create } = require('../source/channel')
|
|
8
|
+
|
|
9
|
+
/** @type {jest.MockedObject<comq.amqp.Connection>} */
|
|
10
|
+
let connection
|
|
11
|
+
|
|
12
|
+
/** @type {comq.Topology} */
|
|
13
|
+
let topology
|
|
14
|
+
|
|
15
|
+
/** @type {comq.Channel} */
|
|
16
|
+
let channel
|
|
17
|
+
|
|
18
|
+
/** @type {jest.MockedObject<comq.amqp.Channel>} */
|
|
19
|
+
let chan
|
|
20
|
+
|
|
21
|
+
const exchange = generate()
|
|
22
|
+
const queue = generate()
|
|
23
|
+
const key = generate()
|
|
24
|
+
|
|
25
|
+
const getCreatedChannel = () => {
|
|
26
|
+
const method = `create${topology.confirms ? 'Confirm' : ''}Channel`
|
|
27
|
+
|
|
28
|
+
return connection[method].mock.results[0].value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
jest.clearAllMocks()
|
|
33
|
+
|
|
34
|
+
connection = await amqplib.connect()
|
|
35
|
+
topology = fixtures.preset()
|
|
36
|
+
channel = await create(connection, topology)
|
|
37
|
+
chan = await getCreatedChannel()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('route', () => {
|
|
41
|
+
const buffer = Buffer.from(generate())
|
|
42
|
+
|
|
43
|
+
it('should assert a direct exchange', async () => {
|
|
44
|
+
await channel.route(exchange, key, buffer)
|
|
45
|
+
|
|
46
|
+
expect(chan.assertExchange).toHaveBeenCalledTimes(1)
|
|
47
|
+
|
|
48
|
+
const [name, type] = chan.assertExchange.mock.calls[0]
|
|
49
|
+
|
|
50
|
+
expect(name).toStrictEqual(exchange)
|
|
51
|
+
expect(type).toStrictEqual('direct')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should publish with the key as the routing key', async () => {
|
|
55
|
+
await channel.route(exchange, key, buffer)
|
|
56
|
+
|
|
57
|
+
const [name, routingKey, published] = chan.publish.mock.calls[0]
|
|
58
|
+
|
|
59
|
+
expect(name).toStrictEqual(exchange)
|
|
60
|
+
expect(routingKey).toStrictEqual(key)
|
|
61
|
+
expect(published).toStrictEqual(buffer)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should assert the exchange once for many keys', async () => {
|
|
65
|
+
await channel.route(exchange, key, buffer)
|
|
66
|
+
await channel.route(exchange, generate(), buffer)
|
|
67
|
+
|
|
68
|
+
expect(chan.assertExchange).toHaveBeenCalledTimes(1)
|
|
69
|
+
expect(chan.publish).toHaveBeenCalledTimes(2)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('bound', () => {
|
|
74
|
+
const consumer = /** @type {comq.channels.Consumer} */ jest.fn(() => undefined)
|
|
75
|
+
|
|
76
|
+
it('should assert a direct exchange', async () => {
|
|
77
|
+
await channel.bound(exchange, queue, key, consumer)
|
|
78
|
+
|
|
79
|
+
const [name, type] = chan.assertExchange.mock.calls[0]
|
|
80
|
+
|
|
81
|
+
expect(name).toStrictEqual(exchange)
|
|
82
|
+
expect(type).toStrictEqual('direct')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should assert the queue by name', async () => {
|
|
86
|
+
await channel.bound(exchange, queue, key, consumer)
|
|
87
|
+
|
|
88
|
+
expect(chan.assertQueue).toHaveBeenCalledWith(queue, expect.anything())
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should bind the queue under the key', async () => {
|
|
92
|
+
await channel.bound(exchange, queue, key, consumer)
|
|
93
|
+
|
|
94
|
+
const { queue: asserted } = await chan.assertQueue.mock.results[0].value
|
|
95
|
+
|
|
96
|
+
expect(chan.bindQueue).toHaveBeenCalledTimes(1)
|
|
97
|
+
expect(chan.bindQueue).toHaveBeenCalledWith(asserted, exchange, key)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should consume the queue', async () => {
|
|
101
|
+
await channel.bound(exchange, queue, key, consumer)
|
|
102
|
+
|
|
103
|
+
expect(chan.consume).toHaveBeenCalledWith(queue, expect.any(Function), expect.anything())
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should bind once per key', async () => {
|
|
107
|
+
await channel.bound(exchange, queue, key, consumer)
|
|
108
|
+
await channel.bound(exchange, queue, key, consumer)
|
|
109
|
+
|
|
110
|
+
expect(chan.bindQueue).toHaveBeenCalledTimes(1)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should bind again for another key', async () => {
|
|
114
|
+
await channel.bound(exchange, queue, key, consumer)
|
|
115
|
+
await channel.bound(exchange, generate(), generate(), consumer)
|
|
116
|
+
|
|
117
|
+
expect(chan.bindQueue).toHaveBeenCalledTimes(2)
|
|
118
|
+
})
|
|
119
|
+
})
|
package/test/connection.mock.js
CHANGED
|
@@ -9,7 +9,9 @@ const channel = /** @type {jest.MockedFunction<(sharded?: boolean, index?: numbe
|
|
|
9
9
|
send: jest.fn(async () => undefined),
|
|
10
10
|
fire: jest.fn(async () => undefined),
|
|
11
11
|
subscribe: jest.fn(async () => undefined),
|
|
12
|
+
bound: jest.fn(async () => undefined),
|
|
12
13
|
publish: jest.fn(async () => undefined),
|
|
14
|
+
route: jest.fn(async () => undefined),
|
|
13
15
|
diagnose: jest.fn(async () => undefined),
|
|
14
16
|
forget: jest.fn(() => undefined),
|
|
15
17
|
seal: jest.fn(async () => undefined),
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
const { decode } = require('../source/decode')
|
|
6
|
+
const { encode } = require('../source/encode')
|
|
7
|
+
|
|
8
|
+
const mock = require('./connection.mock')
|
|
9
|
+
const { IO } = require('../source/io')
|
|
10
|
+
|
|
11
|
+
/** @type {comq.IO} */
|
|
12
|
+
let io
|
|
13
|
+
|
|
14
|
+
/** @type {jest.MockedObject<comq.Connection>} */
|
|
15
|
+
let connection
|
|
16
|
+
|
|
17
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
18
|
+
let events
|
|
19
|
+
|
|
20
|
+
const exchange = generate()
|
|
21
|
+
const queue = generate()
|
|
22
|
+
const key = generate()
|
|
23
|
+
const payload = generate()
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
jest.clearAllMocks()
|
|
27
|
+
|
|
28
|
+
connection = mock.connection()
|
|
29
|
+
io = new IO(connection)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('route', () => {
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
await io.route(exchange, key, payload)
|
|
35
|
+
|
|
36
|
+
events = await connection.createChannel.mock.results[0].value
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should create an event channel', async () => {
|
|
40
|
+
expect(connection.createChannel).toHaveBeenCalledWith('event')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should route to the exchange under the key', async () => {
|
|
44
|
+
expect(events.route).toHaveBeenCalledTimes(1)
|
|
45
|
+
|
|
46
|
+
const [name, routingKey, buffer, properties] = events.route.mock.calls[0]
|
|
47
|
+
|
|
48
|
+
expect(name).toStrictEqual(exchange)
|
|
49
|
+
expect(routingKey).toStrictEqual(key)
|
|
50
|
+
expect(decode({ content: buffer, properties })).toStrictEqual(payload)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should pass properties through', async () => {
|
|
54
|
+
jest.clearAllMocks()
|
|
55
|
+
|
|
56
|
+
const properties = { headers: { 'x-region': 'eu' }, persistent: true }
|
|
57
|
+
|
|
58
|
+
await io.route(exchange, key, payload, properties)
|
|
59
|
+
|
|
60
|
+
expect(events.route.mock.calls[0][3]).toMatchObject(properties)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('subscribe', () => {
|
|
65
|
+
const consumer = jest.fn()
|
|
66
|
+
|
|
67
|
+
it('should bind the queue under the key and consume it', async () => {
|
|
68
|
+
await io.subscribe(exchange, queue, key, consumer)
|
|
69
|
+
|
|
70
|
+
events = await connection.createChannel.mock.results[0].value
|
|
71
|
+
|
|
72
|
+
expect(events.bound).toHaveBeenCalledTimes(1)
|
|
73
|
+
|
|
74
|
+
const [name, bound, routingKey] = events.bound.mock.calls[0]
|
|
75
|
+
|
|
76
|
+
expect(name).toStrictEqual(exchange)
|
|
77
|
+
expect(bound).toStrictEqual(queue)
|
|
78
|
+
expect(routingKey).toStrictEqual(key)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should hand the payload to the consumer', async () => {
|
|
82
|
+
await io.subscribe(exchange, queue, key, consumer)
|
|
83
|
+
|
|
84
|
+
events = await connection.createChannel.mock.results[0].value
|
|
85
|
+
|
|
86
|
+
const deliver = events.bound.mock.calls[0][3]
|
|
87
|
+
const properties = { contentType: 'application/json' }
|
|
88
|
+
const message = { content: encode(payload, properties.contentType), properties }
|
|
89
|
+
|
|
90
|
+
await deliver(message)
|
|
91
|
+
|
|
92
|
+
expect(consumer).toHaveBeenCalledWith(payload, properties)
|
|
93
|
+
})
|
|
94
|
+
})
|
package/types/channel.d.ts
CHANGED
|
@@ -19,10 +19,16 @@ declare namespace comq {
|
|
|
19
19
|
|
|
20
20
|
subscribe (exchange: string, queue: string, consumer: channels.Consumer): Promise<void>
|
|
21
21
|
|
|
22
|
+
/** consumes `queue`, bound to a routed `exchange` under `key` */
|
|
23
|
+
bound (exchange: string, queue: string, key: string, consumer: channels.Consumer): Promise<void>
|
|
24
|
+
|
|
22
25
|
send (queue: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
23
26
|
|
|
24
27
|
publish (exchange: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
25
28
|
|
|
29
|
+
/** publishes to a routed exchange under `key`, where `publish` fans out */
|
|
30
|
+
route (exchange: string, key: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
31
|
+
|
|
26
32
|
fire (queue: string, buffer: Buffer, options?: Options.Publish): Promise<boolean>
|
|
27
33
|
|
|
28
34
|
seal (): Promise<void>
|
package/types/io.d.ts
CHANGED
|
@@ -63,6 +63,20 @@ declare namespace comq {
|
|
|
63
63
|
|
|
64
64
|
enqueue(exchange: string, stream: Readable, properties?: _amqp.Properties): Promise<void>
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Publishes to a routed (`direct`) exchange under a key, where `emit` fans out: the
|
|
68
|
+
* message reaches the queues bound under that key and no others.
|
|
69
|
+
*/
|
|
70
|
+
route(exchange: string, key: string, payload: any, encoding?: _encoding.Encoding): Promise<void>
|
|
71
|
+
|
|
72
|
+
route(exchange: string, key: string, payload: any, properties?: _amqp.Properties): Promise<void>
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Consumes a named durable queue bound to a routed exchange under a key, where `consume`
|
|
76
|
+
* takes everything published to a fanout.
|
|
77
|
+
*/
|
|
78
|
+
subscribe<T = any>(exchange: string, queue: string, key: string, consumer: Consumer<T>): Promise<void>
|
|
79
|
+
|
|
66
80
|
process<T = any>(queue: string, processor: Consumer<T>): Promise<void>
|
|
67
81
|
|
|
68
82
|
seal(): Promise<void>
|