comq 0.10.1 → 0.10.2

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.
@@ -21,7 +21,7 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - name: Setup Node
24
- uses: actions/setup-node@v3
24
+ uses: actions/setup-node@v4
25
25
  with:
26
26
  node-version: 16
27
27
  registry-url: 'https://registry.npmjs.org'
@@ -32,7 +32,7 @@ Given('one of the brokers is/has {status}',
32
32
  const actions = {
33
33
  up: async (n = 0) => {
34
34
  await execute('docker start comq-rmq-' + n)
35
- await healthy()
35
+ await healthy(n)
36
36
  },
37
37
  down: async (n = 0) => {
38
38
  await execute('docker stop comq-rmq-' + n)
@@ -42,16 +42,14 @@ const actions = {
42
42
  }
43
43
  }
44
44
 
45
- async function healthy () {
45
+ async function healthy (n) {
46
46
  let process
47
47
 
48
48
  do {
49
49
  await timeout(HEALTHCHECK_INTERVAL)
50
50
 
51
- process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-0')
51
+ process = await execute('docker inspect -f {{.State.Health.Status}} comq-rmq-' + n)
52
52
  } while (process.output !== 'healthy')
53
-
54
- await timeout(500)
55
53
  }
56
54
 
57
55
  const BROKERS_AMOUNT = 2
@@ -4,7 +4,7 @@ const stream = require('node:stream')
4
4
  const assert = require('node:assert')
5
5
  const { randomBytes } = require('node:crypto')
6
6
  const { timeout, quantity, match } = require('@toa.io/generic')
7
- const { parse } = require('@toa.io/yaml')
7
+ const { load: parse } = require('js-yaml')
8
8
  const { Given, When, Then } = require('@cucumber/cucumber')
9
9
 
10
10
  Given('(that ){token} is consuming events from the {token} exchange',
@@ -64,6 +64,7 @@ When('an event is emitted to the {token} exchange with properties:',
64
64
 
65
65
  When('a stream of {quantity} events is emitted to the {token} exchange',
66
66
  /**
67
+ * @param {string} amountQ
67
68
  * @param {string} exchange
68
69
  * @this {comq.features.Context}
69
70
  */
@@ -3,7 +3,7 @@
3
3
  const stream = require('node:stream')
4
4
  const assert = require('node:assert')
5
5
  const { randomBytes } = require('node:crypto')
6
- const { parse } = require('@toa.io/yaml')
6
+ const { load: parse } = require('js-yaml')
7
7
  const { match, timeout, quantity } = require('@toa.io/generic')
8
8
  const { Given, When, Then } = require('@cucumber/cucumber')
9
9
  const { Readable } = require('node:stream')
package/lerna.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.10.1",
2
+ "version": "0.10.2",
3
3
  "conventionalCommits": true
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
@@ -27,27 +27,28 @@
27
27
  "main": "source/index.js",
28
28
  "types": "types/index.d.ts",
29
29
  "devDependencies": {
30
- "@cucumber/cucumber": "^9.1.2",
30
+ "@cucumber/cucumber": "^10.0.1",
31
31
  "@toa.io/command": "^0.4.0",
32
- "@toa.io/tomato": "^0.4.0",
33
- "@toa.io/yaml": "^0.4.0",
34
- "@types/amqplib": "^0.10.1",
35
- "@types/jest": "^29.5.2",
32
+ "@toa.io/generic": "^1.0.0-alpha.0",
33
+ "@types/amqplib": "^0.10.3",
34
+ "@types/jest": "^29.5.8",
36
35
  "husky": "^8.0.3",
37
36
  "jest": "^29.5.0",
37
+ "js-yaml": "^4.1.0",
38
38
  "randomstring": "^1.3.0",
39
39
  "snazzy": "^9.0.0",
40
40
  "standard": "^17.1.0"
41
41
  },
42
42
  "dependencies": {
43
- "@toa.io/generic": "^0.4.0",
44
43
  "amqplib": "^0.10.3",
45
- "msgpackr": "^1.9.5"
44
+ "msgpackr": "^1.9.5",
45
+ "promex": "^0.1.1",
46
+ "reretry": "^0.1.0"
46
47
  },
47
48
  "scripts": {
48
49
  "prepare": "echo run: husky install",
49
50
  "test": "standard && jest",
50
- "features": "cucumber-js --publish-quiet",
51
+ "features": "cucumber-js --fail-fast",
51
52
  "lint": "standard --fix --verbose | snazzy"
52
53
  },
53
54
  "standard": {
package/readme.md CHANGED
@@ -137,12 +137,6 @@ auto-generated name.
137
137
  // with a consumer function
138
138
  await io.consume('numbers_added', 'logger',
139
139
  ({ a, b }) => console.log(`${a} was added to ${b}`))
140
-
141
- // as a stream
142
- const events = await io.consume('numbers_added', 'logger')
143
-
144
- for await (const { a, b } of events)
145
- console.log(`${a} was added to ${b}`)
146
140
  ```
147
141
 
148
142
  ## Emission
@@ -350,9 +344,6 @@ and it will be closed when the last instance of `IO` using that connection is [d
350
344
  Topology is designed to deliver maximum performance while ensuring that the **at least once**
351
345
  guarantee provided by RabbitMQ is maintained.
352
346
 
353
- > Topology design assumes that unanswered Requests and unconfirmed Events will be retransmitted
354
- > upon reconnection.
355
-
356
347
  ### Dynamic
357
348
 
358
349
  Static topology refers to the process of defining the complete topology declaration along with the
@@ -1,11 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const { Readable } = require('node:stream')
4
- const { promex } = require('@toa.io/generic')
4
+ const { Promex } = require('promex')
5
5
  const { IDLE_INTERVAL, control } = require('./const')
6
6
 
7
7
  class ReplyStream extends Readable {
8
- confirmation = promex()
8
+ confirmation = new Promex()
9
9
 
10
10
  /** @type {ReturnType<setTimeout> | null} */
11
11
  #timeout = null
@@ -26,8 +26,8 @@ class ReplyStream extends Readable {
26
26
 
27
27
  #buffered = 0
28
28
 
29
- /** @type {Record<number, unknown>} */
30
- #queue = {}
29
+ /** @type {Map<number, unknown>} */
30
+ #queue = new Map()
31
31
 
32
32
  /**
33
33
  * @param {comq.Request} request
@@ -70,12 +70,12 @@ class ReplyStream extends Readable {
70
70
  if (this.#buffered > 0) {
71
71
  let message
72
72
 
73
- while ((message = this.#queue[this.#index])) {
74
- delete this.#queue[this.#index]
73
+ while ((message = this.#queue.get(this.#index))) {
74
+ this.#queue.delete(this.#index)
75
75
  this._add(message.payload, message.properties)
76
76
  }
77
77
 
78
- this.#buffered = Object.keys(this.#queue).length
78
+ this.#buffered = this.#queue.size
79
79
  }
80
80
  }
81
81
 
@@ -98,7 +98,7 @@ class ReplyStream extends Readable {
98
98
  if (this.#buffered > MAX_BUFFER_SIZE) this.destroy()
99
99
 
100
100
  this.#buffered++
101
- this.#queue[properties.headers.index] = { payload, properties }
101
+ this.#queue.set(properties.headers.index, { payload, properties })
102
102
  }
103
103
 
104
104
  /**
package/source/channel.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { promex, timeout } = require('@toa.io/generic')
3
+ const { Promex } = require('promex')
4
4
  const { failsafe, lazy, recall } = require('./attributes')
5
5
  const emitter = require('./emitter')
6
6
 
@@ -25,16 +25,16 @@ class Channel {
25
25
  /** @type {string[]} */
26
26
  #tags = []
27
27
 
28
- /** @type {toa.generic.Promex | null} */
28
+ /** @type {Promex | null} */
29
29
  #paused = null
30
30
 
31
31
  /** @type {boolean} */
32
32
  #sealed = false
33
33
 
34
- /** @type {toa.generic.Promex} */
35
- #recovery = promex()
34
+ /** @type {Promex} */
35
+ #recovery = new Promex()
36
36
 
37
- /** @type {Set<toa.generic.Promex>} */
37
+ /** @type {Set<Promex>} */
38
38
  #confirmations = new Set()
39
39
 
40
40
  #diagnostics = emitter.create()
@@ -149,10 +149,11 @@ class Channel {
149
149
 
150
150
  for (const confirmation of this.#confirmations) confirmation.reject(INTERRUPTION)
151
151
 
152
- await timeout(0) // handle interruptions
152
+ // handle interruptions
153
+ await new Promise(resolve => setTimeout(resolve, 0))
153
154
 
154
155
  this.#recovery.resolve()
155
- this.#recovery = promex()
156
+ this.#recovery = new Promex()
156
157
  this.#diagnostics.emit('recover')
157
158
  }
158
159
 
@@ -222,10 +223,10 @@ class Channel {
222
223
  }
223
224
 
224
225
  /**
225
- * @return {toa.generic.Promex}
226
+ * @return {Promex}
226
227
  */
227
228
  #confirmation () {
228
- const confirmation = promex()
229
+ const confirmation = new Promex()
229
230
 
230
231
  this.#confirmations.add(confirmation)
231
232
 
@@ -292,7 +293,7 @@ class Channel {
292
293
  #pause () {
293
294
  if (this.#paused !== null) return
294
295
 
295
- this.#paused = promex()
296
+ this.#paused = new Promex()
296
297
  this.#diagnostics.emit('flow')
297
298
  this.#diagnostics.emit('pause')
298
299
  }
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const amqp = require('amqplib')
4
- const { retry, promex } = require('@toa.io/generic')
4
+ const { Promex } = require('promex')
5
+ const { retry } = require('reretry')
5
6
 
6
7
  const { failsafe } = require('./attributes')
7
8
  const presets = require('./topology')
@@ -21,8 +22,8 @@ class Connection {
21
22
  /** @type {comq.Channel[]} */
22
23
  #channels = []
23
24
 
24
- /** @type {toa.generic.Promex} */
25
- #recovery = promex()
25
+ /** @type {Promex} */
26
+ #recovery = new Promex()
26
27
 
27
28
  /** @type {boolean} */
28
29
  #running = false
@@ -37,7 +38,7 @@ class Connection {
37
38
  }
38
39
 
39
40
  async open () {
40
- await retry(this.#open, RETRY)
41
+ await retry(this.#open)
41
42
 
42
43
  this.#running = true
43
44
  }
@@ -69,14 +70,11 @@ class Connection {
69
70
  this.#diagnostics.on(event, listener)
70
71
  }
71
72
 
72
- /**
73
- * @type {toa.generic.retry.Task}
74
- */
75
73
  #open = async (retry) => {
76
74
  try {
77
75
  this.#connection = await amqp.connect(this.#url)
78
76
  } catch (exception) {
79
- if (this.#transient(exception)) return retry()
77
+ if (this.#transient(exception)) return retry
80
78
  else throw exception
81
79
  }
82
80
 
@@ -90,7 +88,7 @@ class Connection {
90
88
  for (const channel of this.#channels) await channel.recover(this.#connection)
91
89
 
92
90
  this.#recovery.resolve()
93
- this.#recovery = promex()
91
+ this.#recovery = new Promex()
94
92
  }
95
93
 
96
94
  /**
@@ -117,9 +115,4 @@ class Connection {
117
115
 
118
116
  function noop () {}
119
117
 
120
- /** @type {toa.generic.retry.Options} */
121
- const RETRY = {
122
- retries: Infinity
123
- }
124
-
125
118
  exports.Connection = Connection
package/source/io.js CHANGED
@@ -3,7 +3,8 @@
3
3
  const stream = require('node:stream')
4
4
  const { EventEmitter } = require('node:events')
5
5
  const { randomBytes } = require('node:crypto')
6
- const { promex, timeout } = require('@toa.io/generic')
6
+ const { setTimeout } = require('node:timers/promises')
7
+ const { Promex } = require('promex')
7
8
  const { memo, failsafe, lazy, track } = require('./attributes')
8
9
 
9
10
  const { decode } = require('./decode')
@@ -34,7 +35,7 @@ class IO {
34
35
  /** @type {comq.ReplyEmitter | null} */
35
36
  #control = null
36
37
 
37
- /** @type {Set<toa.generic.Promex>} */
38
+ /** @type {Set<Promex>} */
38
39
  #pendingReplies = new Set()
39
40
 
40
41
  /** @type {Set<comq.Destroyable>} */
@@ -279,7 +280,7 @@ class IO {
279
280
 
280
281
  /**
281
282
  * @param {comq.Request} request
282
- * @return {Promise<any>}
283
+ * @return {Promex<any>}
283
284
  */
284
285
  #createReply (request) {
285
286
  const reply = this.#createPendingReply()
@@ -290,10 +291,10 @@ class IO {
290
291
  }
291
292
 
292
293
  /**
293
- * @return {toa.generic.Promex}
294
+ * @return {Promex}
294
295
  */
295
296
  #createPendingReply () {
296
- const reply = promex()
297
+ const reply = new Promex()
297
298
 
298
299
  this.#pendingReplies.add(reply)
299
300
 
@@ -383,7 +384,7 @@ class IO {
383
384
  Even if these messages are lost, the reply stream will be closed anyway,
384
385
  either due to missing heartbeat or the deletion of the stream queue.
385
386
  */
386
- await timeout(50)
387
+ await setTimeout(50)
387
388
  }
388
389
 
389
390
  /**
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { promex, sample } = require('@toa.io/generic')
3
+ const { Promex } = require('promex')
4
4
  const events = require('../events')
5
5
  const emitter = require('../emitter')
6
6
 
@@ -22,13 +22,13 @@ class Channel {
22
22
  /** @type {Set<Promise<comq.Channel>>} */
23
23
  #pending = new Set()
24
24
 
25
- /** @type {Map<comq.Channel, toa.generic.Promex>} */
25
+ /** @type {Map<comq.Channel, Promex>} */
26
26
  #bench = new Map()
27
27
 
28
28
  /** @type {comq.topology.type} */
29
29
  #type
30
30
 
31
- #recovery = promex()
31
+ #recovery = new Promex()
32
32
 
33
33
  #diagnostics = emitter.create()
34
34
 
@@ -134,7 +134,7 @@ class Channel {
134
134
  #remove (channel) {
135
135
  if (!this.#channels.has(channel)) return
136
136
 
137
- this.#bench.set(channel, promex())
137
+ this.#bench.set(channel, new Promex())
138
138
  this.#channels.delete(channel)
139
139
  this.#update()
140
140
  this.#diagnostics.emit('remove', channel.index)
@@ -162,7 +162,7 @@ class Channel {
162
162
  this.#add(channel)
163
163
 
164
164
  this.#recovery.resolve()
165
- this.#recovery = promex()
165
+ this.#recovery = new Promex()
166
166
  }
167
167
 
168
168
  #comeback (channel) {
@@ -209,7 +209,7 @@ class Channel {
209
209
  async #one (fn) {
210
210
  if (this.#pool.length === 0) await this.#recovery
211
211
 
212
- const channel = sample(this.#pool)
212
+ const channel = this.#pool[Math.floor(Math.random() * this.#pool.length)]
213
213
 
214
214
  try {
215
215
  return await fn(channel)
@@ -1,79 +0,0 @@
1
- 'use strict'
2
-
3
- const { AssertionError } = require('node:assert')
4
- const { generate } = require('randomstring')
5
- const { random, quantity } = require('@toa.io/generic')
6
- const tomato = require('@toa.io/tomato')
7
- const { io } = require('./io.mock')
8
- const mock = { tomato }
9
-
10
- jest.mock('@cucumber/cucumber', () => mock.tomato)
11
-
12
- require('../backpressure')
13
-
14
- /** @type {comq.features.Context} */
15
- let context
16
-
17
- describe('When I\'m sending {quantity}B requests to the {token} queue at {quantity}Hz for {number} second(s)', () => {
18
- const step = tomato.steps.Wh('I\'m sending {quantity}B requests to the {token} queue at {quantity}Hz for {number} second(s)')
19
-
20
- it('should be', async () => undefined)
21
-
22
- const bytesQ = (random(5) + 1) + 'k'
23
- const frequencyQ = String((random(5) + 1) / 10) + 'k' // not too high
24
- const seconds = (random(3) + 1) / 10
25
- const queue = generate()
26
-
27
- const bytes = quantity(bytesQ)
28
- const frequency = quantity(frequencyQ)
29
-
30
- beforeAll(async () => {
31
- jest.clearAllMocks()
32
-
33
- context = /** @type {comq.features.Context} */ { io, requestCount: 0, replyCount: 0 }
34
-
35
- await step.call(context, bytesQ, queue, frequencyQ, seconds)
36
- })
37
-
38
- it('should send request', async () => {
39
- expect(io.request).toHaveBeenCalled()
40
- })
41
-
42
- it('should sent request to specified queue', async () => {
43
- const call = io.request.mock.calls[0]
44
-
45
- expect(call[0]).toStrictEqual(queue)
46
- })
47
-
48
- it('should send buffer of given size', async () => {
49
- const buffer = io.request.mock.calls[0][1]
50
-
51
- expect(Buffer.byteLength(buffer)).toStrictEqual(bytes)
52
- })
53
-
54
- it('should send expected amount of requests', async () => {
55
- const times = Math.ceil(seconds * frequency)
56
-
57
- expect(io.request).toHaveBeenCalledTimes(times)
58
- })
59
- })
60
-
61
- describe('Then back pressure was applied', () => {
62
- const step = tomato.steps.Th('back pressure was applied')
63
-
64
- beforeEach(() => {
65
- context = /** @type {comq.features.Context} */ { io, events: {} }
66
- })
67
-
68
- it('should be', async () => undefined)
69
-
70
- it('should fail if flag is not set', async () => {
71
- expect(() => step.call(context)).toThrow(AssertionError)
72
- })
73
-
74
- it('should pass if flag is true', async () => {
75
- context.events = { flow: true }
76
-
77
- expect(() => step.call(context)).not.toThrow()
78
- })
79
- })
@@ -1,101 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const tomato = require('@toa.io/tomato')
5
- const { command } = require('./command.mock')
6
- const mock = { tomato, command }
7
-
8
- jest.mock('@cucumber/cucumber', () => mock.tomato)
9
- jest.mock('@toa.io/command', () => mock.command)
10
-
11
- jest.setTimeout(10000)
12
-
13
- global.COMQ_TESTING_HEALTHCHECK_INTERVAL = 0
14
-
15
- require('../broker')
16
-
17
- const context = /** @type {comq.features.Context} */ {}
18
-
19
- beforeEach(() => {
20
- jest.clearAllMocks()
21
- })
22
-
23
- describe.each([false, true])('Given the broker is/has {status} #shards:%s', (sharded) => {
24
- const step = tomato.steps.Gi('the broker is/has {status}')
25
-
26
- const containerNumber = sharded ? 1 : 0
27
-
28
- it('should be', async () => undefined)
29
-
30
- beforeEach(() => {
31
- context.io = generate()
32
-
33
- if (sharded) {
34
- context.sharded = true
35
- context.shard = containerNumber
36
- }
37
- })
38
-
39
- it('should start comq-rmq container', async () => {
40
- command.execute.mockImplementationOnce(async () => undefined)
41
- command.execute.mockImplementationOnce(async () => ({ output: 'healthy' }))
42
-
43
- await step.call(context, 'up')
44
-
45
- expect(command.execute).toHaveBeenCalledWith('docker start comq-rmq-' + containerNumber)
46
- })
47
-
48
- it('should wait for healthy state', async () => {
49
- let starting = false
50
- let healthy = false
51
-
52
- command.execute.mockImplementationOnce(async () => undefined)
53
-
54
- command.execute.mockImplementationOnce(async () => {
55
- starting = true
56
-
57
- return { output: 'starting' }
58
- })
59
-
60
- command.execute.mockImplementationOnce(async () => {
61
- healthy = true
62
-
63
- return { output: 'healthy' }
64
- })
65
-
66
- await step.call(context, 'up')
67
-
68
- expect(command.execute).toHaveBeenCalledTimes(3)
69
- })
70
-
71
- it('should stop comq-rmq container', async () => {
72
- await step.call(context, 'down')
73
-
74
- expect(command.execute).toHaveBeenCalledWith('docker stop comq-rmq-' + containerNumber)
75
- })
76
-
77
- it('should kill comq-rmq-0 container', async () => {
78
- await step.call(context, 'crashed')
79
-
80
- expect(command.execute).toHaveBeenCalledWith('docker kill comq-rmq-' + containerNumber)
81
- })
82
- })
83
-
84
- describe('Given one of the brokers is/has {status}', () => {
85
- const step = tomato.steps.Gi('one of the brokers is/has {status}')
86
-
87
- it('should store picked broker', async () => {
88
- await step.call(context, 'down')
89
-
90
- expect(context.shard).toStrictEqual(expect.any(Number))
91
- })
92
-
93
- it('should start one of the containers', async () => {
94
- command.execute.mockImplementationOnce(async () => undefined)
95
- command.execute.mockImplementationOnce(async () => ({ output: 'healthy' }))
96
-
97
- await step.call(context, 'up')
98
-
99
- expect(command.execute).toHaveBeenCalledWith('docker start comq-rmq-' + context.shard)
100
- })
101
- })
@@ -1,5 +0,0 @@
1
- 'use strict'
2
-
3
- const execute = jest.fn(async () => undefined)
4
-
5
- exports.command = { execute }
@@ -1,10 +0,0 @@
1
- 'use strict'
2
-
3
- const io = () => ({
4
- diagnose: jest.fn(() => undefined),
5
- close: jest.fn(async () => undefined)
6
- })
7
-
8
- const connect = jest.fn(async () => io())
9
-
10
- exports.comq = { connect }