comq 0.4.1 → 0.5.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.
Files changed (61) hide show
  1. package/.github/labels.yaml +15 -0
  2. package/.github/workflows/labels.yaml +18 -0
  3. package/.github/workflows/release.yaml +20 -8
  4. package/docker-compose.yaml +20 -2
  5. package/docs/notes.md +14 -8
  6. package/examples/const.js +1 -3
  7. package/examples/events/consumer.js +5 -6
  8. package/examples/events/producer.js +3 -3
  9. package/examples/package.json +3 -0
  10. package/examples/rpc/consumer.js +3 -3
  11. package/examples/rpc/producer.js +3 -3
  12. package/features/connection.feature +3 -0
  13. package/features/events.feature +1 -1
  14. package/features/shards.feature +61 -0
  15. package/features/steps/.test/backpressure.test.js +2 -2
  16. package/features/steps/.test/broker.test.js +32 -6
  17. package/features/steps/.test/connection.test.js +105 -1
  18. package/features/steps/.test/context.mock.js +2 -2
  19. package/features/steps/.test/context.test.js +13 -5
  20. package/features/steps/.test/events.test.js +66 -3
  21. package/features/steps/.test/rpc.test.js +99 -1
  22. package/features/steps/.types/context.d.ts +11 -1
  23. package/features/steps/backpressure.js +1 -1
  24. package/features/steps/broker.js +26 -9
  25. package/features/steps/connection.js +69 -2
  26. package/features/steps/context.js +28 -12
  27. package/features/steps/events.js +41 -2
  28. package/features/steps/rpc.js +58 -1
  29. package/lerna.json +1 -1
  30. package/package.json +5 -5
  31. package/readme.md +75 -25
  32. package/source/channel.js +56 -32
  33. package/source/connect.js +28 -2
  34. package/source/connection.js +24 -17
  35. package/source/decode.js +1 -1
  36. package/source/events.js +7 -0
  37. package/source/io.js +21 -14
  38. package/source/shards/channel.js +219 -0
  39. package/source/shards/connection.js +56 -0
  40. package/source/shards/index.js +5 -0
  41. package/test/channel.test.js +240 -145
  42. package/test/connect.test.js +39 -11
  43. package/test/connection.mock.js +17 -13
  44. package/test/connection.test.js +65 -12
  45. package/test/decode.test.js +5 -5
  46. package/test/io.close.test.js +2 -2
  47. package/test/io.consume.test.js +1 -1
  48. package/test/io.reply.test.js +7 -7
  49. package/test/io.request.test.js +27 -1
  50. package/test/presets.test.js +1 -1
  51. package/test/shards/channel.test.js +367 -0
  52. package/test/shards/connection.test.js +106 -0
  53. package/types/amqp.d.ts +3 -1
  54. package/types/channel.d.ts +8 -5
  55. package/types/connection.d.ts +3 -1
  56. package/types/diagnostic.d.ts +1 -1
  57. package/types/topology.d.ts +4 -0
  58. /package/source/{presets → topology}/event.json +0 -0
  59. /package/source/{presets → topology}/index.js +0 -0
  60. /package/source/{presets → topology}/reply.json +0 -0
  61. /package/source/{presets → topology}/request.json +0 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  const { generate } = require('randomstring')
6
6
 
7
- const { timeout } = require('@toa.io/generic')
7
+ const { timeout, promex, random } = require('@toa.io/generic')
8
8
  const { amqplib } = require('./amqplib.mock')
9
9
  const { channel: create } = require('./connection.mock')
10
10
  const mock = { amqplib, channel: { create } }
@@ -12,7 +12,7 @@ const mock = { amqplib, channel: { create } }
12
12
  jest.mock('amqplib', () => mock.amqplib)
13
13
  jest.mock('../source/channel', () => mock.channel)
14
14
 
15
- const presets = require('../source/presets')
15
+ const presets = require('../source/topology')
16
16
  const { Connection } = require('../source/connection')
17
17
 
18
18
  it('should be', async () => {
@@ -64,7 +64,7 @@ describe('initial connection', () => {
64
64
  })
65
65
 
66
66
  describe('reconnection', () => {
67
- /** @type {jest.MockedObject<import('amqplib').Connection>} */
67
+ /** @type {jest.MockedObject<comq.amqp.Connection>} */
68
68
  let conn
69
69
 
70
70
  beforeEach(async () => {
@@ -115,7 +115,7 @@ describe('reconnection', () => {
115
115
  })
116
116
 
117
117
  describe('create channel', () => {
118
- /** @type {jest.MockedObject<import('amqplib').Connection>} */
118
+ /** @type {jest.MockedObject<comq.amqp.Connection>} */
119
119
  let conn
120
120
 
121
121
  beforeEach(async () => {
@@ -126,7 +126,7 @@ describe('create channel', () => {
126
126
 
127
127
  it.each(
128
128
  /** @type {comq.topology.type[]} */
129
- ['request', 'reply', 'event'])('should create channel of %s type',
129
+ ['request', 'reply', 'event'])('should create failsafe channel of %s type',
130
130
  async (type) => {
131
131
  // noinspection JSCheckFunctionSignatures
132
132
  create.mockImplementationOnce(async () => generate())
@@ -134,10 +134,19 @@ describe('create channel', () => {
134
134
  const preset = presets[type]
135
135
  const channel = await connection.createChannel(type)
136
136
 
137
- expect(create).toHaveBeenCalledWith(conn, preset)
137
+ expect(create).toHaveBeenCalledWith(conn, preset, undefined)
138
138
  expect(channel).toStrictEqual(await create.mock.results[0].value)
139
139
  })
140
140
 
141
+ it('should create failfast channel', async () => {
142
+ const type = 'request'
143
+ const index = random()
144
+
145
+ await connection.createChannel(type, index)
146
+
147
+ expect(create).toHaveBeenCalledWith(expect.anything(), expect.anything(), index)
148
+ })
149
+
141
150
  it('should create channel after exception', async () => {
142
151
  create.mockImplementation(async () => { throw new Error() })
143
152
 
@@ -152,6 +161,49 @@ describe('create channel', () => {
152
161
 
153
162
  expect(channel).toStrictEqual(await create.mock.results[1].value)
154
163
  })
164
+
165
+ it('should wait for initial connection', async () => {
166
+ jest.clearAllMocks()
167
+ expect.assertions(2)
168
+
169
+ connection = new Connection(url)
170
+
171
+ setImmediate(async () => {
172
+ expect(create).not.toHaveBeenCalled()
173
+
174
+ await connection.open()
175
+ })
176
+
177
+ await connection.createChannel('request')
178
+
179
+ expect(create).toHaveBeenCalled()
180
+ })
181
+
182
+ it('should wait for reconnection', async () => {
183
+ expect.assertions(3)
184
+
185
+ jest.clearAllMocks()
186
+
187
+ const promise = promex()
188
+
189
+ amqplib.connect.mockImplementationOnce(() => promise)
190
+
191
+ conn.emit('close', new Error())
192
+
193
+ expect(amqplib.connect).toHaveBeenCalled()
194
+
195
+ setImmediate(async () => {
196
+ expect(create).not.toHaveBeenCalled()
197
+
198
+ const conn = await amqplib.connect()
199
+
200
+ promise.resolve(conn)
201
+ })
202
+
203
+ await connection.createChannel('request')
204
+
205
+ expect(create).toHaveBeenCalled()
206
+ })
155
207
  })
156
208
 
157
209
  describe('close', () => {
@@ -164,15 +216,16 @@ describe('close', () => {
164
216
  expect(amqp.close).toHaveBeenCalled()
165
217
  })
166
218
 
167
- it('should ignore exceptions', async () => {
168
- await connection.open()
219
+ it('should close after connection is (re)established', async () => {
220
+ // don't wait for completion
221
+ connection.open().then()
169
222
 
170
- /** @type {jest.MockedObject<import('amqplib').Connection>} */
171
- const amqp = await amqplib.connect.mock.results[0].value
223
+ await connection.close()
172
224
 
173
- amqp.close.mockImplementation(async () => { throw new Error() })
225
+ /** @type {jest.MockedObject<comq.amqp.Connection>} */
226
+ const conn = await amqplib.connect.mock.results[0].value
174
227
 
175
- await expect(connection.close()).resolves.not.toThrow()
228
+ expect(conn.close).toHaveBeenCalled()
176
229
  })
177
230
  })
178
231
 
@@ -16,7 +16,7 @@ it('should decode application/json', async () => {
16
16
  const content = Buffer.from(json)
17
17
  const contentType = 'application/json'
18
18
  const properties = { contentType }
19
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
19
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
20
20
 
21
21
  const decoded = decode(message)
22
22
 
@@ -28,7 +28,7 @@ it('should decode application/msgpack', async () => {
28
28
  const content = pack(object)
29
29
  const contentType = 'application/msgpack'
30
30
  const properties = { contentType }
31
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
31
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
32
32
 
33
33
  const decoded = decode(message)
34
34
 
@@ -40,7 +40,7 @@ it('should return buffer if content type is not defined', async () => {
40
40
  const json = JSON.stringify(object)
41
41
  const content = Buffer.from(json)
42
42
  const properties = {}
43
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
43
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
44
44
 
45
45
  const buffer = decode(message)
46
46
  const string = buffer.toString()
@@ -55,7 +55,7 @@ it('should return buffer if content type is application/octet-stream', async ()
55
55
  const content = Buffer.from(json)
56
56
  const contentType = 'application/octet-stream'
57
57
  const properties = { contentType }
58
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
58
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
59
59
 
60
60
  const buffer = decode(message)
61
61
  const string = buffer.toString()
@@ -68,7 +68,7 @@ it('should throw if content-type is not supported', async () => {
68
68
  const content = randomBytes(8)
69
69
  const contentType = 'wtf/' + generate()
70
70
  const properties = { contentType }
71
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
71
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
72
72
 
73
73
  expect(() => decode(message)).toThrow('is not supported')
74
74
  })
@@ -104,7 +104,7 @@ describe('close', () => {
104
104
  const callback = channel.subscribe.mock.calls[0][2]
105
105
  const content = randomBytes(8)
106
106
  const properties = {}
107
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
107
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
108
108
 
109
109
  callback(message)
110
110
 
@@ -137,7 +137,7 @@ describe('close', () => {
137
137
  const callback = channel.consume.mock.calls[0][1]
138
138
  const content = generate()
139
139
  const properties = { replyTo: generate(), contentType: 'text/plain' }
140
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
140
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
141
141
 
142
142
  callback(message)
143
143
 
@@ -63,7 +63,7 @@ describe('group consumption', () => {
63
63
  const payload = generate()
64
64
  const content = encode(payload, contentType)
65
65
  const properties = { contentType }
66
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
66
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
67
67
  const callback = events.subscribe.mock.calls[0][2]
68
68
 
69
69
  await callback(message)
@@ -74,7 +74,7 @@ describe('queues', () => {
74
74
  it('should throw if message is missing replyTo', async () => {
75
75
  const content = randomBytes(10)
76
76
  const properties = {}
77
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
77
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
78
78
  const producer = requests.consume.mock.calls[0][1]
79
79
 
80
80
  await expect(producer(message)).rejects.toThrow('is missing the `replyTo` property')
@@ -99,7 +99,7 @@ describe('reply', () => {
99
99
 
100
100
  const content = randomBytes(10)
101
101
  const properties = { replyTo: generate(), contentType: 'text/plain' }
102
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
102
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
103
103
  const producer = requests.consume.mock.calls[0][1]
104
104
 
105
105
  await producer(message)
@@ -119,7 +119,7 @@ describe('reply', () => {
119
119
  const callback = requests.consume.mock.calls[0][1]
120
120
  const content = randomBytes(10)
121
121
  const properties = { replyTo: generate() }
122
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
122
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
123
123
 
124
124
  await expect(callback(message)).rejects.toThrow('must return a value')
125
125
  })
@@ -143,7 +143,7 @@ describe('encoding', () => {
143
143
  const value = { [generate()]: generate() }
144
144
  const content = encode(value, encoding)
145
145
  const properties = { contentType: encoding, replyTo: generate() }
146
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
146
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
147
147
  const producer = requests.consume.mock.calls[0][1]
148
148
 
149
149
  await producer(message)
@@ -161,7 +161,7 @@ describe('encoding', () => {
161
161
  contentType: encoding
162
162
  }
163
163
 
164
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
164
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
165
165
  const producer = requests.consume.mock.calls[0][1]
166
166
 
167
167
  await producer(message)
@@ -190,7 +190,7 @@ describe('encoding', () => {
190
190
  contentType: 'application/msgpack'
191
191
  }
192
192
 
193
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
193
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
194
194
  const producer = requests.consume.mock.calls[0][1]
195
195
  const buffer = randomBytes(8)
196
196
 
@@ -214,7 +214,7 @@ describe('encoding', () => {
214
214
  correlationId: generate()
215
215
  }
216
216
 
217
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
217
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
218
218
  const producer = requests.consume.mock.calls[0][1]
219
219
 
220
220
  await expect(producer(message)).rejects.toThrow('must be of type `Buffer`')
@@ -174,6 +174,32 @@ describe('send', () => {
174
174
 
175
175
  expect(requests.send).toHaveBeenCalledTimes(2)
176
176
  })
177
+
178
+ it('should resend unanswered Requests on sharded connection', async () => {
179
+ jest.clearAllMocks()
180
+
181
+ connection = mock.connection(true)
182
+ io = new IO(connection)
183
+
184
+ promise = io.request(queue, payload)
185
+
186
+ // allows initializers to run
187
+ await immediate()
188
+
189
+ requests = await findChannel('request')
190
+
191
+ expect(requests.sharded).toStrictEqual(true)
192
+ expect(requests.diagnose).toHaveBeenCalledWith('remove', expect.any(Function))
193
+
194
+ const calls = requests.diagnose.mock.calls.filter((call) => call[0] === 'remove')
195
+ const listeners = calls.map((call) => call[1])
196
+
197
+ for (const listener of listeners) listener()
198
+
199
+ await immediate()
200
+
201
+ expect(requests.send).toHaveBeenCalledTimes(2)
202
+ })
177
203
  })
178
204
 
179
205
  describe('reply', () => {
@@ -210,7 +236,7 @@ const reply = async (content = randomBytes(8), contentType = undefined) => {
210
236
  const correlationId = requests.send.mock.calls[0][2].correlationId
211
237
  const properties = { correlationId, contentType }
212
238
  const callback = replies.consume.mock.calls[0][1]
213
- const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties }
239
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
214
240
 
215
241
  await callback(message)
216
242
  }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const presets = require('../source/presets')
3
+ const presets = require('../source/topology')
4
4
 
5
5
  it('should define request preset', async () => {
6
6
  expect(presets.request).toStrictEqual({
@@ -0,0 +1,367 @@
1
+ 'use strict'
2
+
3
+ const { randomBytes } = require('node:crypto')
4
+ const { generate } = require('randomstring')
5
+ const { promex, sample, immediate, random, each } = require('@toa.io/generic')
6
+
7
+ const mock = require('../connection.mock')
8
+
9
+ const { create } = require('../../source/shards/channel')
10
+
11
+ it('should be', async () => {
12
+ expect(create).toBeInstanceOf(Function)
13
+ })
14
+
15
+ /** @type {Array<jest.MockedObject<comq.Connection>>} */
16
+ const connections = [mock.connection(), mock.connection()]
17
+ const type = /** @type {comq.topology.type} */ generate()
18
+
19
+ /** @type {comq.Channel} */
20
+ let channel
21
+
22
+ beforeEach(() => {
23
+ jest.clearAllMocks()
24
+ })
25
+
26
+ it('should expose `sharded`', async () => {
27
+ channel = await create(connections, type)
28
+
29
+ expect(channel.sharded).toStrictEqual(true)
30
+ })
31
+
32
+ it('should resolve when one of the connections has created a channel', async () => {
33
+ /** @type {toa.generic.Promex[]} */
34
+ const promises = []
35
+
36
+ for (const conn of connections) {
37
+ const promise = promex()
38
+
39
+ conn.createChannel.mockImplementationOnce(() => promise)
40
+
41
+ promises.push(promise)
42
+ }
43
+
44
+ let any
45
+ const chan = mock.channel()
46
+
47
+ setImmediate(() => {
48
+ any = sample(promises)
49
+ any.resolve(chan)
50
+ })
51
+
52
+ await create(connections, type)
53
+ await immediate()
54
+
55
+ expect(any).toBeDefined()
56
+
57
+ // resolve pending promises
58
+ for (const promise of promises) if (promise !== any) promise.resolve(chan)
59
+
60
+ await Promise.all(promises)
61
+ })
62
+
63
+ it('should create failfast channels of given type', async () => {
64
+ await create(connections, type)
65
+
66
+ each(connections, (conn, index) => {
67
+ expect(conn.createChannel).toHaveBeenCalledWith(type, index)
68
+ })
69
+ })
70
+
71
+ describe.each(['consume', 'subscribe'])('%s', (method) => {
72
+ const exchange = generate()
73
+ const queue = generate()
74
+ const group = generate()
75
+ const processor = jest.fn()
76
+ const args = method === 'consume' ? [queue, processor] : [exchange, group, processor]
77
+
78
+ it(`should ${method} using all channels`, async () => {
79
+ channel = await create(connections, type)
80
+
81
+ const channels = await getCreatedChannels()
82
+
83
+ expect(channels.length).toStrictEqual(2)
84
+
85
+ await channel[method](...args)
86
+
87
+ for (const chan of channels) expect(chan[method]).toHaveBeenCalledWith(...args)
88
+ })
89
+
90
+ it(`should should ${method} using available and pending channels`, async () => {
91
+ const promise = promex()
92
+
93
+ connections[0].createChannel.mockImplementationOnce(() => promise)
94
+
95
+ channel = await create(connections, type)
96
+
97
+ await channel[method](...args)
98
+
99
+ /** @type {jest.MockedObject<comq.Channel>} */
100
+ const chan1 = await connections[1].createChannel.mock.results[0].value
101
+
102
+ expect(chan1[method]).toHaveBeenCalled()
103
+
104
+ /** @type {jest.MockedObject<comq.Channel>} */
105
+ const chan0 = mock.channel()
106
+
107
+ promise.resolve(chan0)
108
+
109
+ await immediate()
110
+
111
+ expect(chan0[method]).toHaveBeenCalled()
112
+ })
113
+
114
+ it(`should ${method} using a channel removed from the pool once it is recovered`, async () => {
115
+ channel = await create(connections, type)
116
+
117
+ const channels = await getCreatedChannels()
118
+ const chan = sample(channels)
119
+ const buffer = randomBytes(8)
120
+ const fail = /** @type {jest.MockedFunction} */ jest.fn(async () => { throw new Error() })
121
+
122
+ chan.publish.mockImplementationOnce(fail)
123
+
124
+ // `chan` has failed and removed from the pool
125
+ while (fail.mock.calls.length === 0) await channel.publish(exchange, buffer)
126
+
127
+ await channel[method](...args)
128
+
129
+ expect(chan[method]).not.toHaveBeenCalled()
130
+
131
+ const calls = chan.diagnose.mock.calls.filter((call) => call[0] === 'recover')
132
+ const listeners = calls.map((call) => call[1])
133
+
134
+ // emit `recover` event
135
+ for (const listener of listeners) await listener()
136
+
137
+ await immediate()
138
+
139
+ expect(chan[method]).toHaveBeenCalled()
140
+ })
141
+ })
142
+
143
+ describe.each(/** @type {string[]} */['send', 'publish', 'throw'])('%s', (method) => {
144
+ const label = generate()
145
+ const buffer = randomBytes(8)
146
+ const options = { contentType: generate() }
147
+
148
+ /** @type {jest.MockedObject<comq.Channel>[]} */
149
+ let channels
150
+
151
+ beforeEach(async () => {
152
+ channel = await create(connections, type)
153
+ channels = await getCreatedChannels()
154
+ })
155
+
156
+ it('should send to single channel', async () => {
157
+ await channel[method](label, buffer, options)
158
+
159
+ let used = 0
160
+
161
+ for (const chan of channels) {
162
+ if (chan[method].mock.calls.length === 0) continue
163
+
164
+ used++
165
+
166
+ expect(chan[method]).toHaveBeenCalledWith(label, buffer, options)
167
+ }
168
+
169
+ expect(used).toStrictEqual(1)
170
+ })
171
+
172
+ it('should route pending messages among remaining shards', async () => {
173
+ const b = random(channels.length)
174
+ const broken = channels[b]
175
+ const options = { contentType: generate() }
176
+ let thrown = false
177
+
178
+ broken[method].mockImplementationOnce(async () => {
179
+ thrown = true
180
+ throw new Error()
181
+ })
182
+
183
+ let i = 0
184
+
185
+ // eslint-disable-next-line no-unmodified-loop-condition
186
+ while (!thrown) await channel[method](label + ++i, buffer, options)
187
+
188
+ const last = label + i
189
+
190
+ // now channel has thrown an exception
191
+ expect(broken[method]).toHaveBeenCalledWith(last, buffer, options)
192
+
193
+ const r = (b + 1) % 2
194
+ const remaining = channels[r]
195
+
196
+ expect(remaining[method]).toHaveBeenCalledWith(last, buffer, options)
197
+ })
198
+
199
+ it('should wait for recovery', async () => {
200
+ const reject = async () => { throw new Error() }
201
+
202
+ /** @type {jest.MockedObject<comq.Channel>} */
203
+ let recovered
204
+
205
+ // both shards rejects and will be removed from the pool
206
+ channels[0][method].mockImplementationOnce(reject)
207
+ channels[1][method].mockImplementationOnce(reject)
208
+
209
+ setImmediate(() => {
210
+ const r = random(channels.length)
211
+
212
+ recovered = channels[r]
213
+
214
+ // subscriptions to the 'recover' event
215
+ const calls = recovered.diagnose.mock.calls.filter(
216
+ (call) => call[0] === 'recover')
217
+
218
+ expect(calls.length).toBeGreaterThan(0)
219
+
220
+ // emit 'recover' event to return shard to the pool
221
+ for (const call of calls) call[1]()
222
+ })
223
+
224
+ await channel[method](label, buffer, options)
225
+
226
+ expect(recovered).toBeDefined()
227
+ expect(recovered[method]).toHaveBeenCalledTimes(2) // failed and succeeded
228
+ })
229
+ })
230
+
231
+ describe('seal', () => {
232
+ it('should seal channels', async () => {
233
+ channel = await create(connections, type)
234
+
235
+ await channel.seal()
236
+
237
+ const channels = await getCreatedChannels()
238
+
239
+ for (const chan of channels) expect(chan.seal).toHaveBeenCalled()
240
+ })
241
+
242
+ it('should seal channels eventually', async () => {
243
+ expect.assertions(connections.length + 1)
244
+
245
+ channel = await create(connections, type)
246
+
247
+ const channels = await getCreatedChannels()
248
+ const chan = sample(channels)
249
+ const promise = promex()
250
+ let sealed = false
251
+
252
+ chan.seal.mockImplementationOnce(() => promise)
253
+
254
+ setImmediate(() => {
255
+ expect(sealed).toStrictEqual(false)
256
+
257
+ promise.resolve()
258
+ })
259
+
260
+ await channel.seal()
261
+
262
+ sealed = true
263
+
264
+ for (const chan of channels) expect(chan.seal).toHaveBeenCalled()
265
+ })
266
+
267
+ it('should seal pending channel', async () => {
268
+ const connection = sample(connections)
269
+ const promise = promex()
270
+
271
+ connection.createChannel.mockImplementationOnce(() => promise)
272
+
273
+ channel = await create(connections, type)
274
+
275
+ /** @type {jest.MockedObject<comq.Channel>} */
276
+ let chan
277
+
278
+ setImmediate(() => {
279
+ chan = mock.channel()
280
+
281
+ promise.resolve(chan)
282
+ })
283
+
284
+ await channel.seal()
285
+
286
+ expect(chan).toBeDefined()
287
+ expect(chan.seal).toHaveBeenCalled()
288
+ })
289
+ })
290
+
291
+ describe('diagnose', () => {
292
+ const events = /** @type {comq.diagnostics.event[]} */ ['flow', 'drain', 'recover', 'discard']
293
+
294
+ /** @type {jest.MockedObject<comq.Channel>[]} */
295
+ let channels
296
+
297
+ beforeEach(async () => {
298
+ channel = await create(connections, type)
299
+ channels = await getCreatedChannels()
300
+ })
301
+
302
+ it.each(events)('should re-emit `%s` event',
303
+ async (event) => {
304
+ const listener = /** @type {Function} */ jest.fn()
305
+ const index = random(channels.length)
306
+ const chan = channels[index]
307
+
308
+ channel.diagnose(event, listener)
309
+
310
+ expect(chan.diagnose).toHaveBeenCalledWith(event, expect.any(Function))
311
+
312
+ const call = chan.diagnose.mock.calls.find((call) => call[0] === event)
313
+ const emit = call[1]
314
+ const args = [generate(), generate()]
315
+
316
+ emit(...args)
317
+
318
+ expect(listener).toHaveBeenCalledWith(...args, index)
319
+ })
320
+
321
+ it('should emit `remove` event', async () => {
322
+ const queue = generate()
323
+ const buffer = randomBytes(8)
324
+ const chan = sample(channels)
325
+ const listener = /** @type {jest.MockedFunction} */ jest.fn()
326
+
327
+ let thrown = false
328
+
329
+ channel.diagnose('remove', listener)
330
+
331
+ chan.send.mockImplementationOnce(() => {
332
+ thrown = true
333
+ throw new Error()
334
+ })
335
+
336
+ // eslint-disable-next-line no-unmodified-loop-condition
337
+ while (!thrown) await channel.send(queue, buffer)
338
+
339
+ expect(listener).toHaveBeenCalledWith(expect.any(Number))
340
+ })
341
+ })
342
+
343
+ it('should not throw on recovery (if the "bench" is empty)', async () => {
344
+ channel = await create(connections, type)
345
+
346
+ const channels = await getCreatedChannels()
347
+ const chan = sample(channels)
348
+
349
+ expect(chan.diagnose).toHaveBeenCalledWith('recover', expect.any(Function))
350
+
351
+ const calls = chan.diagnose.mock.calls.filter((call) => call[0] === 'recover')
352
+ const listeners = calls.map((call) => call[1])
353
+
354
+ // emit 'recover'
355
+ const emit = () => { for (const listener of listeners) listener() }
356
+
357
+ expect(emit).not.toThrow()
358
+ })
359
+
360
+ /**
361
+ * @return {Promise<jest.MockedObject<comq.Channel>[]>}
362
+ */
363
+ async function getCreatedChannels () {
364
+ const promises = connections.map((connection) => connection.createChannel.mock.results[0].value)
365
+
366
+ return await Promise.all(promises)
367
+ }