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.
@@ -6,12 +6,11 @@ updates:
6
6
  interval: "weekly"
7
7
  target-branch: "dev"
8
8
  labels:
9
- - "dependency"
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
- - "dependency"
16
+ - "Bump"
@@ -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).toHaveBeenCalledWith(exchange, expect.anything())
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.expected = promex()
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.expected.resolve()
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 = generate()
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 = generate()
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 = generate()
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 = generate()
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?: any
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
- expected?: Promise<any>
20
+ consumptionPromise?: Promise<any>
20
21
  sharded: boolean
21
22
  shard: number
22
23
  sealing: Promise<any>
@@ -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.expected = consume.call(this, group, exchange)
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.expected = consume.call(this, undefined, exchange)
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
- if (this.expected) await this.expected
46
+ const message = randomBytes(8)
47
47
 
48
- const message = generate()
48
+ await emit.call(this, exchange, message)
49
+ })
49
50
 
50
- await this.io.emit(exchange, message)
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.published = message
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
- assert.equal(this.published, this.consumed[group], 'Event hasn\'t been exclusively consumed')
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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "useWorkspaces": true,
3
- "version": "0.5.3-dev.0",
3
+ "version": "0.6.0-dev.0",
4
4
  "conventionalCommits": true
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.5.3-dev.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
- "test:features": "cucumber-js",
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 {Record<string, comq.ReplyEmitter>} */
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 correlationId = randomBytes(8).toString('hex')
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
- const properties = { contentType }
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[queue] = emitter
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, async (message) => {
177
- if (!('replyTo' in message.properties)) throw new Error('Request is missing the `replyTo` property')
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
- const payload = decode(message)
180
- const reply = await producer(payload)
196
+ if ('replyTo' in request.properties) await this.#reply(request, reply)
197
+ })
181
198
 
182
- if (reply === undefined) throw new Error('The `producer` function must return a value')
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
- let { correlationId, contentType } = message.properties
207
+ let { replyTo, correlationId, contentType } = request.properties
185
208
 
186
- if (Buffer.isBuffer(reply)) contentType = OCTETS
187
- if (contentType === undefined) throw new Error('Reply to a Request without the `contentType` property must be of type `Buffer`')
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
- const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
190
- const properties = { contentType, correlationId }
212
+ const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
213
+ const properties = { contentType, correlationId }
191
214
 
192
- await this.#replies.fire(message.properties.replyTo, buffer, properties)
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.catch(noop).finally(() => this.#pendingReplies.delete(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 Object.values(this.#emitters)) emitter.clear()
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} [encoding]
248
- * @returns [Buffer, string]
269
+ * @param {comq.encoding} [contentType]
270
+ * @returns [Buffer, comq.encoding]
249
271
  */
250
- #encode (payload, encoding) {
272
+ #encode (payload, contentType) {
251
273
  const raw = Buffer.isBuffer(payload)
252
274
 
253
- encoding ??= raw ? OCTETS : DEFAULT
275
+ contentType ??= raw ? OCTETS : DEFAULT
254
276
 
255
- const buffer = raw ? payload : encode(payload, encoding)
277
+ const buffer = raw ? payload : encode(payload, contentType)
256
278
 
257
- return [buffer, encoding]
279
+ return [buffer, contentType]
258
280
  }
259
281
  }
260
282
 
@@ -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(1)
753
+ await timeout(5)
751
754
 
752
755
  expect(repl.publish).toHaveBeenCalledWith(
753
756
  exchange, '', buffer, expect.objectContaining(options), expect.any(Function)
@@ -68,7 +68,20 @@ describe('group consumption', () => {
68
68
 
69
69
  await callback(message)
70
70
 
71
- expect(consumer).toHaveBeenCalledWith(payload)
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).toHaveBeenCalledWith(message.content)
110
+ expect(consumer.mock.calls[0][0]).toStrictEqual(message.content)
98
111
  })
99
112
  })
100
113
 
@@ -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>}
@@ -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 = randomBytes(10)
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', () => {
@@ -43,8 +43,6 @@ let promise
43
43
  const REPLY = new RegExp(`^${queue}..[0-9a-f]+$`)
44
44
 
45
45
  beforeEach(async () => {
46
- jest.clearAllMocks()
47
-
48
46
  promise = io.request(queue, payload)
49
47
 
50
48
  // allows initializers to run
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
@@ -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: _diagnostics.event, listener: Function): void
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