comq 0.16.2 → 0.17.1

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.
@@ -0,0 +1,25 @@
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
20
+
21
+ Scenario: Routing over a sharded connection
22
+ Given an active sharded connection
23
+ And that `orders` is bound to the `records` exchange under the `placed` key
24
+ When a message is routed to the `records` exchange under the `placed` key
25
+ Then `orders` receives the event
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.16.2",
3
+ "version": "0.17.1",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
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,39 @@ 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
+ Over a [sharded connection](#sharded-connection) they behave as the rest does: `route` publishes
185
+ to one shard, and `subscribe` consumes the queue on every one of them.
186
+
187
+ ### Example
188
+
189
+ ```javascript
190
+ await io.subscribe('records', 'records.orders', 'store.orders',
191
+ (record) => console.log(record))
192
+
193
+ await io.route('records', 'store.orders', { id: 1, status: 'paid' })
194
+ await io.route('records', 'store.customers', { id: 2 }) // not delivered to the above
195
+ ```
196
+
161
197
  ## Tasks
162
198
 
163
199
  `async IO.enqueue(queue: string, payload: any, encoding?: string): void`
@@ -409,6 +445,9 @@ requests and are expecting replies.
409
445
 
410
446
  - Exchanges and queues for Events, and queues for Requests
411
447
  are _durable_.
448
+ - An exchange is asserted as _fanout_ for [Emission](#emission)
449
+ and [Consumption](#consumption), and as _direct_ for [Routing](#routing). One name is one or
450
+ the other: asserting it as both is what the broker refuses.
412
451
  - Queues for Replies are _exclusive_ and _auto deleted_.
413
452
 
414
453
  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
- /** @type {comq.amqp.options.Publish} */
136
- const properties = {}
135
+ const [buffer, properties] = this.#properties(payload, encoding)
137
136
 
138
- if (typeof encoding === 'object') { // properties passed
139
- Object.assign(properties, encoding)
137
+ await this.#events[method](exchange, buffer, properties)
138
+ })
140
139
 
141
- encoding = /** @type {comq.Encoding} */ properties.contentType
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
- const [buffer, contentType] = this.#encode(payload, encoding)
155
+ await this.#events.route(exchange, key, buffer, properties)
156
+ })
145
157
 
146
- properties.contentType = contentType
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[method](exchange, buffer, properties)
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)
@@ -64,6 +64,10 @@ class Channel {
64
64
  await this.#every((channel) => channel.subscribe(queue, group, consumer))
65
65
  }
66
66
 
67
+ async bound (exchange, queue, key, consumer) {
68
+ await this.#every((channel) => channel.bound(exchange, queue, key, consumer))
69
+ }
70
+
67
71
  async send (queue, buffer, options) {
68
72
  await this.#one((channel) => channel.send(queue, buffer, options))
69
73
  }
@@ -72,6 +76,10 @@ class Channel {
72
76
  await this.#one((channel) => channel.publish(exchange, buffer, options))
73
77
  }
74
78
 
79
+ async route (exchange, key, buffer, options) {
80
+ await this.#one((channel) => channel.route(exchange, key, buffer, options))
81
+ }
82
+
75
83
  async fire (queue, buffer, options) {
76
84
  // noinspection JSValidateTypes
77
85
  return await this.#one((channel) => channel.fire(queue, buffer, options))
@@ -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
+ })
@@ -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
+ })
@@ -718,3 +718,36 @@ async function getCreatedChannels () {
718
718
 
719
719
  return await Promise.all(promises)
720
720
  }
721
+
722
+ describe('routed', () => {
723
+ const exchange = generate()
724
+ const queue = generate()
725
+ const key = generate()
726
+ const buffer = randomBytes(8)
727
+ const consumer = jest.fn()
728
+
729
+ /** @type {jest.MockedObject<comq.Channel>[]} */
730
+ let channels
731
+
732
+ beforeEach(async () => {
733
+ channel = await create(connections, type)
734
+ channels = await getCreatedChannels()
735
+ })
736
+
737
+ it('should bind using all channels', async () => {
738
+ await channel.bound(exchange, queue, key, consumer)
739
+
740
+ for (const chan of channels) {
741
+ expect(chan.bound).toHaveBeenCalledWith(exchange, queue, key, consumer)
742
+ }
743
+ })
744
+
745
+ it('should route to a single channel', async () => {
746
+ await channel.route(exchange, key, buffer)
747
+
748
+ const used = channels.filter((chan) => chan.route.mock.calls.length > 0)
749
+
750
+ expect(used).toHaveLength(1)
751
+ expect(used[0].route).toHaveBeenCalledWith(exchange, key, buffer, undefined)
752
+ })
753
+ })
@@ -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>