comq 0.15.6 → 0.15.8
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.
- package/.releaserc +1 -8
- package/features/recovery.feature +30 -0
- package/features/silence.feature +46 -0
- package/features/steps/.types/context.d.ts +17 -0
- package/features/steps/backpressure.js +1 -1
- package/features/steps/broker.js +1 -1
- package/features/steps/brokers.js +1 -1
- package/features/steps/common.js +1 -1
- package/features/steps/connection.js +41 -1
- package/features/steps/context.js +12 -2
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +1 -0
- package/features/steps/network.js +50 -0
- package/features/steps/networks.js +90 -0
- package/features/steps/parameters.js +6 -0
- package/features/steps/poison.js +1 -1
- package/features/steps/rpc.js +1 -1
- package/features/steps/tasks.js +1 -1
- package/package.json +3 -6
- package/readme.md +21 -6
- package/source/attributes/lazy.test.js +1 -1
- package/source/attributes/track.test.js +3 -3
- package/source/connection.js +137 -12
- package/source/emitter.js +23 -1
- package/source/encoders/index.js +0 -2
- package/source/events.js +2 -2
- package/source/io.js +6 -5
- package/source/shards/channel.js +34 -17
- package/source/shards/connection.js +0 -5
- package/test/amqplib.mock.js +16 -4
- package/test/channel.fixtures.js +1 -1
- package/test/channel.test.js +1 -1
- package/test/connection.test.js +176 -6
- package/test/decode.test.js +0 -13
- package/test/emitter.test.js +71 -0
- package/test/encode.test.js +0 -9
- package/test/encodings.js +1 -1
- package/test/helpers.js +77 -0
- package/test/io.close.test.js +4 -3
- package/test/io.diagnostics.test.js +2 -2
- package/test/io.reply.test.js +2 -2
- package/test/io.request.test.js +16 -2
- package/test/pipeline.test.js +1 -1
- package/test/shards/channel.test.js +97 -9
- package/test/shards/connection.test.js +4 -3
- package/types/diagnostic.d.ts +2 -2
- package/types/encoding.d.ts +1 -2
- package/types/io.d.ts +2 -0
- package/AGENTS.md +0 -35
- package/CHANGELOG.md +0 -76
- package/source/encoders/msgpack.js +0 -6
package/test/connection.test.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
const { generate } = require('randomstring')
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { Promex } = require('promex')
|
|
8
|
+
const { timeout, random } = require('./helpers')
|
|
8
9
|
const { amqplib, connect } = require('./amqplib.mock')
|
|
9
10
|
const { channel: create } = require('./connection.mock')
|
|
10
11
|
const mock = { amqplib, channel: { create } }
|
|
@@ -37,7 +38,8 @@ describe('initial connection', () => {
|
|
|
37
38
|
it('should connect', async () => {
|
|
38
39
|
await connection.open()
|
|
39
40
|
|
|
40
|
-
expect(amqplib.connect).toHaveBeenCalledWith(
|
|
41
|
+
expect(amqplib.connect).toHaveBeenCalledWith(expect.stringContaining(url),
|
|
42
|
+
expect.objectContaining({ timeout: expect.any(Number) }))
|
|
41
43
|
})
|
|
42
44
|
|
|
43
45
|
it.each(/** @type {[string, Partial<Error>][]} */[
|
|
@@ -138,6 +140,41 @@ describe('reconnection', () => {
|
|
|
138
140
|
expect(amqplib.connect.mock.calls.length).toBeGreaterThanOrEqual(3)
|
|
139
141
|
}, 15000)
|
|
140
142
|
|
|
143
|
+
it('should keep an error sink on a dropped connection', async () => {
|
|
144
|
+
const channel = await connection.createChannel('request')
|
|
145
|
+
|
|
146
|
+
channel.recover
|
|
147
|
+
.mockRejectedValueOnce(new Error('Channel closed'))
|
|
148
|
+
.mockResolvedValue(undefined)
|
|
149
|
+
|
|
150
|
+
conn.emit('close', new Error('lost'))
|
|
151
|
+
|
|
152
|
+
await timeout(10)
|
|
153
|
+
|
|
154
|
+
const dropped = await amqplib.connect.mock.results[1].value
|
|
155
|
+
|
|
156
|
+
// amqplib goes on emitting on a connection it does not know is gone, and an
|
|
157
|
+
// 'error' with no listener left takes the process down
|
|
158
|
+
expect(() => dropped.emit('error', new Error('Heartbeat timeout'))).not.toThrow()
|
|
159
|
+
|
|
160
|
+
// let the attempt that follows the failed recovery settle
|
|
161
|
+
const start = Date.now()
|
|
162
|
+
|
|
163
|
+
while (channel.recover.mock.calls.length < 2 && Date.now() - start < 10000) await timeout(50)
|
|
164
|
+
}, 15000)
|
|
165
|
+
|
|
166
|
+
it('should reconnect despite a listener that throws', async () => {
|
|
167
|
+
// a diagnostic listener has no business breaking the reconnection it reports
|
|
168
|
+
connection.diagnose('reconnect', () => { throw new Error('listener') })
|
|
169
|
+
connection.diagnose('open', () => { throw new Error('listener') })
|
|
170
|
+
|
|
171
|
+
conn.emit('close', new Error('lost'))
|
|
172
|
+
|
|
173
|
+
await expect(connection.createChannel('event')).resolves.toBeDefined()
|
|
174
|
+
|
|
175
|
+
expect(amqplib.connect).toHaveBeenCalledTimes(2)
|
|
176
|
+
}, 10000)
|
|
177
|
+
|
|
141
178
|
it('should emit error when reconnect open fails', async () => {
|
|
142
179
|
const errors = []
|
|
143
180
|
const unhandled = jest.fn()
|
|
@@ -211,6 +248,41 @@ describe('reconnection', () => {
|
|
|
211
248
|
expect(errors).toContain(exception)
|
|
212
249
|
}, 10000)
|
|
213
250
|
|
|
251
|
+
it('should emit reconnect when restoring a lost connection', async () => {
|
|
252
|
+
const reconnect = jest.fn()
|
|
253
|
+
|
|
254
|
+
connection.diagnose('reconnect', reconnect)
|
|
255
|
+
conn.emit('close', new Error('lost'))
|
|
256
|
+
|
|
257
|
+
await connection.createChannel('event')
|
|
258
|
+
|
|
259
|
+
expect(reconnect).toHaveBeenCalled()
|
|
260
|
+
}, 10000)
|
|
261
|
+
|
|
262
|
+
it('should bound a connect that never settles', async () => {
|
|
263
|
+
jest.useFakeTimers()
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const errors = []
|
|
267
|
+
|
|
268
|
+
connection.diagnose('error', (exception) => errors.push(exception))
|
|
269
|
+
amqplib.connect.mockImplementation(() => new Promise(() => undefined))
|
|
270
|
+
|
|
271
|
+
conn.emit('close', new Error('lost'))
|
|
272
|
+
|
|
273
|
+
await jest.advanceTimersByTimeAsync(0)
|
|
274
|
+
await jest.advanceTimersByTimeAsync(30_000)
|
|
275
|
+
|
|
276
|
+
expect(errors).toEqual([expect.objectContaining({ code: 'ETIMEDOUT' })])
|
|
277
|
+
|
|
278
|
+
await jest.advanceTimersByTimeAsync(2_000)
|
|
279
|
+
|
|
280
|
+
expect(amqplib.connect.mock.calls.length).toBeGreaterThan(2)
|
|
281
|
+
} finally {
|
|
282
|
+
jest.useRealTimers()
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
214
286
|
it('should ignore close from a stale connection', async () => {
|
|
215
287
|
const stale = conn
|
|
216
288
|
const closeHandler = stale.on.mock.calls.find(([event]) => event === 'close')[1]
|
|
@@ -304,7 +376,7 @@ describe('create channel', () => {
|
|
|
304
376
|
|
|
305
377
|
jest.clearAllMocks()
|
|
306
378
|
|
|
307
|
-
const promise =
|
|
379
|
+
const promise = new Promex()
|
|
308
380
|
|
|
309
381
|
amqplib.connect.mockImplementationOnce(() => promise)
|
|
310
382
|
|
|
@@ -338,11 +410,23 @@ describe('watchdog', () => {
|
|
|
338
410
|
|
|
339
411
|
const conn = await amqplib.connect.mock.results[0].value
|
|
340
412
|
|
|
341
|
-
await jest.advanceTimersByTimeAsync(
|
|
413
|
+
await jest.advanceTimersByTimeAsync(46_000)
|
|
342
414
|
|
|
343
415
|
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
344
416
|
})
|
|
345
417
|
|
|
418
|
+
it('should reconnect after the watchdog destroys a silent connection', async () => {
|
|
419
|
+
jest.useFakeTimers()
|
|
420
|
+
|
|
421
|
+
await connection.open()
|
|
422
|
+
|
|
423
|
+
await jest.advanceTimersByTimeAsync(46_000)
|
|
424
|
+
|
|
425
|
+
// a socket destroyed without an error is a socket amqplib never reports,
|
|
426
|
+
// which leaves the connection silently dead instead of recovering
|
|
427
|
+
expect(amqplib.connect).toHaveBeenCalledTimes(2)
|
|
428
|
+
})
|
|
429
|
+
|
|
346
430
|
it('should not let a replaced socket disarm the watchdog', async () => {
|
|
347
431
|
jest.useFakeTimers()
|
|
348
432
|
|
|
@@ -372,11 +456,11 @@ describe('watchdog', () => {
|
|
|
372
456
|
|
|
373
457
|
const conn = await amqplib.connect.mock.results[0].value
|
|
374
458
|
|
|
375
|
-
await jest.advanceTimersByTimeAsync(
|
|
459
|
+
await jest.advanceTimersByTimeAsync(40_000)
|
|
376
460
|
|
|
377
461
|
conn.connection.stream.emit('data', Buffer.alloc(0))
|
|
378
462
|
|
|
379
|
-
await jest.advanceTimersByTimeAsync(
|
|
463
|
+
await jest.advanceTimersByTimeAsync(40_000)
|
|
380
464
|
|
|
381
465
|
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
382
466
|
|
|
@@ -384,6 +468,77 @@ describe('watchdog', () => {
|
|
|
384
468
|
|
|
385
469
|
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
386
470
|
})
|
|
471
|
+
|
|
472
|
+
it('should derive the watchdog from the negotiated heartbeat', async () => {
|
|
473
|
+
jest.useFakeTimers()
|
|
474
|
+
|
|
475
|
+
const connection = new Connection('amqp://localhost?heartbeat=30')
|
|
476
|
+
|
|
477
|
+
await connection.open()
|
|
478
|
+
|
|
479
|
+
const conn = await amqplib.connect.mock.results[0].value
|
|
480
|
+
|
|
481
|
+
// the broker is only expected to say something every 15 seconds
|
|
482
|
+
await jest.advanceTimersByTimeAsync(60_000)
|
|
483
|
+
|
|
484
|
+
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
485
|
+
|
|
486
|
+
await jest.advanceTimersByTimeAsync(31_000)
|
|
487
|
+
|
|
488
|
+
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('should not arm the watchdog when heartbeats are disabled', async () => {
|
|
492
|
+
jest.useFakeTimers()
|
|
493
|
+
|
|
494
|
+
const connection = new Connection('amqp://localhost?heartbeat=0')
|
|
495
|
+
|
|
496
|
+
await connection.open()
|
|
497
|
+
|
|
498
|
+
const conn = await amqplib.connect.mock.results[0].value
|
|
499
|
+
|
|
500
|
+
// an idle connection that has agreed to no heartbeats is silent by design
|
|
501
|
+
await jest.advanceTimersByTimeAsync(600_000)
|
|
502
|
+
|
|
503
|
+
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
describe('heartbeat', () => {
|
|
508
|
+
it('should ask for a heartbeat', async () => {
|
|
509
|
+
const connection = new Connection('amqp://developer@localhost')
|
|
510
|
+
|
|
511
|
+
await connection.open()
|
|
512
|
+
|
|
513
|
+
expect(amqplib.connect).toHaveBeenCalledWith('amqp://developer@localhost?heartbeat=15',
|
|
514
|
+
expect.anything())
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('should append the heartbeat to an existing query', async () => {
|
|
518
|
+
const connection = new Connection('amqp://localhost?frameMax=8192')
|
|
519
|
+
|
|
520
|
+
await connection.open()
|
|
521
|
+
|
|
522
|
+
expect(amqplib.connect).toHaveBeenCalledWith('amqp://localhost?frameMax=8192&heartbeat=15',
|
|
523
|
+
expect.anything())
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it.each(['amqp://localhost?heartbeat=5', 'amqp://localhost?heartbeat=0'])(
|
|
527
|
+
'should keep the heartbeat requested by the caller (%s)',
|
|
528
|
+
async (url) => {
|
|
529
|
+
const connection = new Connection(url)
|
|
530
|
+
|
|
531
|
+
await connection.open()
|
|
532
|
+
|
|
533
|
+
expect(amqplib.connect).toHaveBeenCalledWith(url, expect.anything())
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('should keep the socket alive', async () => {
|
|
537
|
+
await connection.open()
|
|
538
|
+
|
|
539
|
+
expect(amqplib.connect)
|
|
540
|
+
.toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ keepAlive: true }))
|
|
541
|
+
})
|
|
387
542
|
})
|
|
388
543
|
|
|
389
544
|
describe('close', () => {
|
|
@@ -476,6 +631,21 @@ describe('diagnostics', () => {
|
|
|
476
631
|
expect(captured).toStrictEqual(true)
|
|
477
632
|
})
|
|
478
633
|
|
|
634
|
+
it('should not emit reconnect on the initial open', async () => {
|
|
635
|
+
const reconnect = jest.fn()
|
|
636
|
+
|
|
637
|
+
connection.diagnose('reconnect', reconnect)
|
|
638
|
+
|
|
639
|
+
jest.clearAllMocks()
|
|
640
|
+
|
|
641
|
+
connection = new Connection(url)
|
|
642
|
+
connection.diagnose('reconnect', reconnect)
|
|
643
|
+
|
|
644
|
+
await connection.open()
|
|
645
|
+
|
|
646
|
+
expect(reconnect).not.toHaveBeenCalled()
|
|
647
|
+
})
|
|
648
|
+
|
|
479
649
|
it('should re-emit `close` event', async () => {
|
|
480
650
|
let captured
|
|
481
651
|
|
package/test/decode.test.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomBytes } = require('node:crypto')
|
|
4
4
|
const { generate } = require('randomstring')
|
|
5
|
-
const { pack } = require('msgpackr')
|
|
6
5
|
|
|
7
6
|
const { decode } = require('../source/decode')
|
|
8
7
|
|
|
@@ -23,18 +22,6 @@ it('should decode application/json', async () => {
|
|
|
23
22
|
expect(decoded).toStrictEqual(object)
|
|
24
23
|
})
|
|
25
24
|
|
|
26
|
-
it('should decode application/msgpack', async () => {
|
|
27
|
-
const object = { [generate()]: generate() }
|
|
28
|
-
const content = pack(object)
|
|
29
|
-
const contentType = 'application/msgpack'
|
|
30
|
-
const properties = { contentType }
|
|
31
|
-
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
|
32
|
-
|
|
33
|
-
const decoded = decode(message)
|
|
34
|
-
|
|
35
|
-
expect(decoded).toStrictEqual(object)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
25
|
it('should return buffer if content type is not defined', async () => {
|
|
39
26
|
const object = { [generate()]: generate() }
|
|
40
27
|
const json = JSON.stringify(object)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const emitter = require('../source/emitter')
|
|
5
|
+
|
|
6
|
+
/** @type {import('node:events').EventEmitter} */
|
|
7
|
+
let instance
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
instance = emitter.create()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should be', async () => {
|
|
14
|
+
expect(emitter.create).toBeInstanceOf(Function)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should emit', async () => {
|
|
18
|
+
const listener = jest.fn()
|
|
19
|
+
const event = generate()
|
|
20
|
+
const args = [generate(), generate()]
|
|
21
|
+
|
|
22
|
+
instance.on(event, listener)
|
|
23
|
+
|
|
24
|
+
expect(instance.emit(event, ...args)).toStrictEqual(true)
|
|
25
|
+
expect(listener).toHaveBeenCalledWith(...args)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should report an event no one listens to', async () => {
|
|
29
|
+
expect(instance.emit(generate())).toStrictEqual(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should not throw on error without listeners', async () => {
|
|
33
|
+
expect(() => instance.emit('error', new Error(generate()))).not.toThrow()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should not let a listener break the emission', async () => {
|
|
37
|
+
const event = generate()
|
|
38
|
+
const listener = jest.fn()
|
|
39
|
+
|
|
40
|
+
instance.on(event, () => { throw new Error(generate()) })
|
|
41
|
+
instance.on(event, listener)
|
|
42
|
+
|
|
43
|
+
expect(() => instance.emit(event)).not.toThrow()
|
|
44
|
+
|
|
45
|
+
// the listeners that follow a broken one are still called
|
|
46
|
+
expect(listener).toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should call a `once` listener once', async () => {
|
|
50
|
+
const event = generate()
|
|
51
|
+
const listener = jest.fn()
|
|
52
|
+
|
|
53
|
+
instance.once(event, listener)
|
|
54
|
+
|
|
55
|
+
instance.emit(event)
|
|
56
|
+
instance.emit(event)
|
|
57
|
+
|
|
58
|
+
expect(listener).toHaveBeenCalledTimes(1)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should not call a listener that has been removed', async () => {
|
|
62
|
+
const event = generate()
|
|
63
|
+
const listener = jest.fn()
|
|
64
|
+
|
|
65
|
+
instance.on(event, listener)
|
|
66
|
+
instance.off(event, listener)
|
|
67
|
+
|
|
68
|
+
instance.emit(event)
|
|
69
|
+
|
|
70
|
+
expect(listener).not.toHaveBeenCalled()
|
|
71
|
+
})
|
package/test/encode.test.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { generate } = require('randomstring')
|
|
4
|
-
const { pack } = require('msgpackr')
|
|
5
4
|
|
|
6
5
|
const { encode } = require('../source/encode')
|
|
7
6
|
|
|
@@ -26,14 +25,6 @@ it('should encode as json', async () => {
|
|
|
26
25
|
expect(buffer).toStrictEqual(expected)
|
|
27
26
|
})
|
|
28
27
|
|
|
29
|
-
it('should encode as msgpack', async () => {
|
|
30
|
-
const value = { [generate()]: generate() }
|
|
31
|
-
const packed = pack(value)
|
|
32
|
-
const buffer = encode(value, 'application/msgpack')
|
|
33
|
-
|
|
34
|
-
expect(buffer).toStrictEqual(packed)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
28
|
/** @type {[comq.Encoding, string, Buffer][]} */
|
|
38
29
|
const samples = [
|
|
39
30
|
['text/plain', 'some string', Buffer.from('some string')]
|
package/test/encodings.js
CHANGED
package/test/helpers.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
4
|
+
|
|
5
|
+
const immediate = () => new Promise((resolve) => setImmediate(resolve))
|
|
6
|
+
|
|
7
|
+
const random = (max = 100) => Math.floor(Math.random() * max)
|
|
8
|
+
|
|
9
|
+
const flip = () => Math.random() < 0.5
|
|
10
|
+
|
|
11
|
+
const sample = (array) => array[Math.floor(Math.random() * array.length)]
|
|
12
|
+
|
|
13
|
+
const quantity = (input) => {
|
|
14
|
+
const matched = String(input).match(/^(\d+(?:\.\d+)?)([^\d\W]*)$/)
|
|
15
|
+
|
|
16
|
+
if (matched === null) {
|
|
17
|
+
throw new Error(`'${input}' doesn't look like a quantity of something`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const number = +matched[1]
|
|
21
|
+
const suffix = matched[2]
|
|
22
|
+
|
|
23
|
+
if (suffix.length === 0) {
|
|
24
|
+
return number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pair = MULTIPLIERS.find(([unit]) => suffix.startsWith(unit))
|
|
28
|
+
|
|
29
|
+
if (pair === undefined) {
|
|
30
|
+
throw new Error(`'${suffix}' doesn't look like a quantity unit`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return number * pair[1]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const match = (reference, candidate) => {
|
|
37
|
+
if (typeof candidate !== typeof reference) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(candidate)) {
|
|
42
|
+
return Array.isArray(reference) &&
|
|
43
|
+
candidate.every((value) => reference.some((item) => match(item, value)))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof candidate === 'object') {
|
|
47
|
+
if (candidate === null) {
|
|
48
|
+
return reference === null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (reference === null) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Object.entries(candidate).every(([key, value]) => match(reference[key], value))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return reference === candidate
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const MULTIPLIERS = Object.entries({
|
|
62
|
+
k: 1000,
|
|
63
|
+
Ki: 1024,
|
|
64
|
+
K: 1024,
|
|
65
|
+
Mi: 1024 ** 2,
|
|
66
|
+
M: 1000 ** 2,
|
|
67
|
+
Gi: 1024 ** 3,
|
|
68
|
+
G: 1000 ** 3
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
exports.timeout = timeout
|
|
72
|
+
exports.immediate = immediate
|
|
73
|
+
exports.random = random
|
|
74
|
+
exports.flip = flip
|
|
75
|
+
exports.sample = sample
|
|
76
|
+
exports.quantity = quantity
|
|
77
|
+
exports.match = match
|
package/test/io.close.test.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomBytes } = require('node:crypto')
|
|
4
4
|
const { generate } = require('randomstring')
|
|
5
|
-
const {
|
|
5
|
+
const { Promex } = require('promex')
|
|
6
|
+
const { immediate } = require('./helpers')
|
|
6
7
|
|
|
7
8
|
const mock = require('./connection.mock')
|
|
8
9
|
const { IO } = require('../source/io')
|
|
@@ -93,7 +94,7 @@ describe('close', () => {
|
|
|
93
94
|
})
|
|
94
95
|
|
|
95
96
|
it('should wait for event processing completion', async () => {
|
|
96
|
-
const promise = /** @type {Promise<void>} */
|
|
97
|
+
const promise = /** @type {Promise<void>} */ new Promex()
|
|
97
98
|
const queue = generate()
|
|
98
99
|
const group = generate()
|
|
99
100
|
const consumer = jest.fn(async () => promise)
|
|
@@ -128,7 +129,7 @@ describe('close', () => {
|
|
|
128
129
|
})
|
|
129
130
|
|
|
130
131
|
it('should wait for request processing completion', async () => {
|
|
131
|
-
const promise = /** @type {Promise<void>} */
|
|
132
|
+
const promise = /** @type {Promise<void>} */ new Promex()
|
|
132
133
|
const producer = jest.fn(async () => promise)
|
|
133
134
|
|
|
134
135
|
await reply(/** @type {Function} */ producer)
|
|
@@ -28,7 +28,7 @@ describe.each(['request', 'reply', 'event'])('%s channel events',
|
|
|
28
28
|
* @param {comq.topology.type} type
|
|
29
29
|
*/
|
|
30
30
|
(type) => {
|
|
31
|
-
it.each(['flow', 'drain', 'recover', 'discard', 'pause', 'resume'])('should re-emit %s',
|
|
31
|
+
it.each(['flow', 'drain', 'recover', 'discard', 'pause', 'resume', 'lost', 'remove'])('should re-emit %s',
|
|
32
32
|
/**
|
|
33
33
|
* @param {comq.diagnostics.Event} event
|
|
34
34
|
*/
|
|
@@ -61,7 +61,7 @@ it('should not throw on error without listeners', async () => {
|
|
|
61
61
|
expect(() => emit(new Error(generate()))).not.toThrow()
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it.each(['open', 'close'])('should re-emit %s from connection',
|
|
64
|
+
it.each(['open', 'close', 'error', 'reconnect'])('should re-emit %s from connection',
|
|
65
65
|
/**
|
|
66
66
|
* @param {comq.diagnostics.Event} event
|
|
67
67
|
*/
|
package/test/io.reply.test.js
CHANGED
|
@@ -194,12 +194,12 @@ describe('encoding', () => {
|
|
|
194
194
|
|
|
195
195
|
it('should set octet-stream for Buffer reply', async () => {
|
|
196
196
|
const request = { [generate()]: generate() }
|
|
197
|
-
const content = encode(request, 'application/
|
|
197
|
+
const content = encode(request, 'application/json')
|
|
198
198
|
|
|
199
199
|
const properties = {
|
|
200
200
|
replyTo: generate(),
|
|
201
201
|
correlationId: generate(),
|
|
202
|
-
contentType: 'application/
|
|
202
|
+
contentType: 'application/json'
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
const message = /** @type {comq.amqp.Message} */ { content, properties }
|
package/test/io.request.test.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const stream = require('node:stream')
|
|
6
6
|
const { randomBytes } = require('node:crypto')
|
|
7
7
|
const { generate } = require('randomstring')
|
|
8
|
-
const { immediate } = require('
|
|
8
|
+
const { immediate } = require('./helpers')
|
|
9
9
|
const { encode } = require('../source/encode')
|
|
10
10
|
|
|
11
11
|
const mock = require('./connection.mock')
|
|
@@ -182,6 +182,20 @@ describe('send', () => {
|
|
|
182
182
|
expect(requests.send).toHaveBeenCalledTimes(2)
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
+
// a reply that could not be routed has been dropped by the broker
|
|
186
|
+
it('should resend unanswered Requests when the reply channel recovers', async () => {
|
|
187
|
+
expect(replies.diagnose).toHaveBeenCalledWith('recover', expect.any(Function))
|
|
188
|
+
|
|
189
|
+
const calls = replies.diagnose.mock.calls.filter((call) => call[0] === 'recover')
|
|
190
|
+
const listeners = calls.map((call) => call[1])
|
|
191
|
+
|
|
192
|
+
for (const listener of listeners) listener()
|
|
193
|
+
|
|
194
|
+
await immediate()
|
|
195
|
+
|
|
196
|
+
expect(requests.send).toHaveBeenCalledTimes(2)
|
|
197
|
+
})
|
|
198
|
+
|
|
185
199
|
it('should resend unanswered Requests on sharded connection', async () => {
|
|
186
200
|
jest.clearAllMocks()
|
|
187
201
|
|
|
@@ -338,7 +352,7 @@ describe('reply', () => {
|
|
|
338
352
|
expect(output).toStrictEqual(content)
|
|
339
353
|
})
|
|
340
354
|
|
|
341
|
-
const encodings = ['application/
|
|
355
|
+
const encodings = ['application/json']
|
|
342
356
|
|
|
343
357
|
it.each(encodings)('should decode %s',
|
|
344
358
|
/**
|
package/test/pipeline.test.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { EventEmitter } = require('node:events')
|
|
4
4
|
const stream = require('node:stream')
|
|
5
5
|
const { pipeline, transform } = require('../source/pipeline')
|
|
6
|
-
const { timeout } = require('
|
|
6
|
+
const { timeout } = require('./helpers')
|
|
7
7
|
|
|
8
8
|
/** @type {Channel} */
|
|
9
9
|
let channel
|