comq 0.5.2 → 0.5.3-dev.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.
@@ -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"
@@ -10,6 +10,8 @@ jest.mock('@toa.io/command', () => mock.command)
10
10
 
11
11
  jest.setTimeout(10000)
12
12
 
13
+ global.COMQ_TESTING_HEALTHCHECK_INTERVAL = 0
14
+
13
15
  require('../broker')
14
16
 
15
17
  const context = /** @type {comq.features.Context} */ {}
@@ -172,11 +172,13 @@ describe('Given I\'m publishing {quantity}B events to the {token} exchange at {q
172
172
 
173
173
  step.call(context, bytesQ, exchange, frequencyQ)
174
174
 
175
- await timeout(100 + 11)
175
+ await timeout(100 + 12)
176
176
 
177
- expect(context.io.emit).toHaveBeenCalledTimes(10)
177
+ const callsAmountExpected = [10, 11]
178
+
179
+ expect(callsAmountExpected).toContain(context.io.emit.mock.calls.length)
178
180
  expect(context.publishing).toBeDefined()
179
- expect(context.eventsPublishedCount).toStrictEqual(10)
181
+ expect(callsAmountExpected).toContain(context.eventsPublishedCount)
180
182
 
181
183
  clearInterval(context.publishing)
182
184
  })
@@ -219,11 +219,13 @@ describe('Given I\'m sending {quantity}B requests to the {token} queue at {quant
219
219
 
220
220
  step.call(context, bytesQ, queue, frequencyQ)
221
221
 
222
- await timeout(100 + 11)
222
+ await timeout(100 + 12)
223
223
 
224
- expect(context.io.request).toHaveBeenCalledTimes(10)
224
+ const callsAmountExpected = [10, 11]
225
+
226
+ expect(callsAmountExpected).toContain(context.io.request.mock.calls.length)
225
227
  expect(context.sending).toBeDefined()
226
- expect(context.requestsSent.length).toStrictEqual(10)
228
+ expect(callsAmountExpected).toContain(context.requestsSent.length)
227
229
 
228
230
  clearInterval(context.sending)
229
231
  })
@@ -46,10 +46,11 @@ async function healthy () {
46
46
  let process
47
47
 
48
48
  do {
49
- await timeout(1000)
49
+ await timeout(HEALTHCHECK_INTERVAL)
50
50
 
51
51
  process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-0')
52
52
  } while (process.output !== 'healthy')
53
53
  }
54
54
 
55
55
  const BROKERS_AMOUNT = 2
56
+ const HEALTHCHECK_INTERVAL = global.COMQ_TESTING_HEALTHCHECK_INTERVAL ?? 1000
package/lerna.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "useWorkspaces": true,
3
- "version": "0.5.2",
3
+ "version": "0.5.3-dev.1",
4
4
  "conventionalCommits": true
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.5.2",
3
+ "version": "0.5.3-dev.1",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
@@ -27,7 +27,7 @@
27
27
  "main": "source/index.js",
28
28
  "types": "types/index.d.ts",
29
29
  "devDependencies": {
30
- "@cucumber/cucumber": "9.0.1",
30
+ "@cucumber/cucumber": "9.1.0",
31
31
  "@toa.io/command": "0.3.0",
32
32
  "@toa.io/mock": "0.3.0",
33
33
  "@toa.io/tomato": "0.4.0-dev.2",
@@ -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()
@@ -67,12 +67,13 @@ class IO {
67
67
  * @returns {Promise<void>}
68
68
  */
69
69
  async (queue, payload, encoding, replyToFormatter) => {
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 }
70
+ const [buffer, properties] = this.#encode(payload, encoding)
71
+ const emitter = this.#emitters.get(queue)
75
72
  const reply = this.#createReply()
73
+ const correlationId = randomBytes(8).toString('hex')
74
+
75
+ properties.replyTo = replyToFormatter?.(emitter.queue) ?? emitter.queue
76
+ properties.correlationId = correlationId
76
77
 
77
78
  emitter.once(correlationId, reply.resolve)
78
79
 
@@ -103,8 +104,7 @@ class IO {
103
104
  * @returns {Promise<void>}
104
105
  */
105
106
  async (exchange, payload, encoding) => {
106
- const [buffer, contentType] = this.#encode(payload, encoding)
107
- const properties = { contentType }
107
+ const [buffer, properties] = this.#encode(payload, encoding)
108
108
 
109
109
  await this.#events.publish(exchange, buffer, properties)
110
110
  })
@@ -127,12 +127,16 @@ class IO {
127
127
  // region initializers
128
128
 
129
129
  async #createRequestReplyChannels () {
130
- this.#requests = await this.#createChannel('request')
131
- this.#replies = await this.#createChannel('reply')
130
+ await this.#createRequestChannel()
132
131
 
132
+ this.#replies = await this.#createChannel('reply')
133
133
  this.#setupRetransmission()
134
134
  }
135
135
 
136
+ async #createRequestChannel () {
137
+ this.#requests = await this.#createChannel('request')
138
+ }
139
+
136
140
  async #createEventChannel () {
137
141
  this.#events = await this.#createChannel('event')
138
142
  }
@@ -141,7 +145,7 @@ class IO {
141
145
  const emitter = io.createReplyEmitter(queue)
142
146
  const consumer = this.#getReplyConsumer(queue, emitter)
143
147
 
144
- this.#emitters[queue] = emitter
148
+ this.#emitters.set(queue, emitter)
145
149
 
146
150
  await this.#replies.consume(emitter.queue, consumer)
147
151
  }
@@ -173,24 +177,36 @@ class IO {
173
177
  * @returns {comq.channels.consumer}
174
178
  */
175
179
  #getRequestConsumer = (producer) =>
176
- track(this, async (message) => {
177
- if (!('replyTo' in message.properties)) throw new Error('Request is missing the `replyTo` property')
180
+ track(this,
181
+ /**
182
+ * @param {comq.amqp.Message} request
183
+ * @returns {Promise<void>}
184
+ */
185
+ async (request) => {
186
+ const payload = decode(request)
187
+ const reply = await producer(payload)
178
188
 
179
- const payload = decode(message)
180
- const reply = await producer(payload)
189
+ if ('replyTo' in request.properties) await this.#reply(request, reply)
190
+ })
181
191
 
182
- if (reply === undefined) throw new Error('The `producer` function must return a value')
192
+ /**
193
+ * @param {comq.amqp.Message} request
194
+ * @param {any} reply
195
+ * @returns {Promise<void>}
196
+ */
197
+ async #reply (request, reply) {
198
+ if (reply === undefined) throw new Error('The `producer` function must return a value')
183
199
 
184
- let { correlationId, contentType } = message.properties
200
+ let { replyTo, correlationId, contentType } = request.properties
185
201
 
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`')
202
+ if (Buffer.isBuffer(reply)) contentType = OCTETS
203
+ if (contentType === undefined) throw new Error('Reply to a Request without the `contentType` property must be of type `Buffer`')
188
204
 
189
- const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
190
- const properties = { contentType, correlationId }
205
+ const buffer = contentType === OCTETS ? reply : encode(reply, contentType)
206
+ const properties = { contentType, correlationId }
191
207
 
192
- await this.#replies.fire(message.properties.replyTo, buffer, properties)
193
- })
208
+ await this.#replies.fire(replyTo, buffer, properties)
209
+ }
194
210
 
195
211
  /**
196
212
  * @param {string} queue
@@ -198,9 +214,6 @@ class IO {
198
214
  * @returns {comq.channels.consumer}
199
215
  */
200
216
  #getReplyConsumer = (queue, emitter) =>
201
- /**
202
- * @param {comq.amqp.Message} message
203
- */
204
217
  (message) => {
205
218
  const payload = decode(message)
206
219
 
@@ -226,7 +239,9 @@ class IO {
226
239
 
227
240
  this.#pendingReplies.add(reply)
228
241
 
229
- reply.catch(noop).finally(() => this.#pendingReplies.delete(reply))
242
+ reply
243
+ .catch(noop)
244
+ .finally(() => this.#pendingReplies.delete(reply))
230
245
 
231
246
  return reply
232
247
  }
@@ -236,7 +251,7 @@ class IO {
236
251
  }
237
252
 
238
253
  #retransmit = () => {
239
- for (const emitter of Object.values(this.#emitters)) emitter.clear()
254
+ for (const emitter of this.#emitters.values()) emitter.clear()
240
255
 
241
256
  // trigger failsafe attribute
242
257
  for (const reply of this.#pendingReplies) reply.reject(RETRANSMISSION)
@@ -244,17 +259,18 @@ class IO {
244
259
 
245
260
  /**
246
261
  * @param {any} payload
247
- * @param {comq.encoding} [encoding]
248
- * @returns [Buffer, string]
262
+ * @param {comq.encoding} [contentType]
263
+ * @returns [Buffer, comq.amqp.options.Publish]
249
264
  */
250
- #encode (payload, encoding) {
265
+ #encode (payload, contentType) {
251
266
  const raw = Buffer.isBuffer(payload)
252
267
 
253
- encoding ??= raw ? OCTETS : DEFAULT
268
+ contentType ??= raw ? OCTETS : DEFAULT
254
269
 
255
- const buffer = raw ? payload : encode(payload, encoding)
270
+ const buffer = raw ? payload : encode(payload, contentType)
271
+ const properties = { contentType }
256
272
 
257
- return [buffer, encoding]
273
+ return [buffer, properties]
258
274
  }
259
275
  }
260
276
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  const { randomBytes } = require('node:crypto')
6
6
  const { generate } = require('randomstring')
7
- const { flip, random } = require('@toa.io/generic')
7
+ const { flip, random, timeout } = require('@toa.io/generic')
8
8
 
9
9
  const backpressure = require('./backpressure')
10
10
  const { amqplib } = require('./amqplib.mock')
@@ -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
 
@@ -730,7 +733,7 @@ describe('recovery', () => {
730
733
 
731
734
  const chan = await getCreatedChannel()
732
735
 
733
- chan.publish.mockImplementation(() => true)
736
+ chan.publish.mockImplementation(() => true) // back pressure
734
737
 
735
738
  /** @type {jest.MockedObject<comq.amqp.Connection>} */
736
739
  let replacement
@@ -747,6 +750,8 @@ describe('recovery', () => {
747
750
 
748
751
  const repl = await getCreatedChannel(replacement)
749
752
 
753
+ await timeout(5)
754
+
750
755
  expect(repl.publish).toHaveBeenCalledWith(
751
756
  exchange, '', buffer, expect.objectContaining(options), expect.any(Function)
752
757
  )
@@ -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