comq 0.15.7 → 0.16.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.
- package/.releaserc +15 -6
- package/features/channels.feature +14 -0
- 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/channels.js +45 -0
- package/features/steps/common.js +1 -1
- package/features/steps/connection.js +41 -1
- package/features/steps/context.js +24 -4
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +2 -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 +8 -9
- package/readme.md +17 -2
- package/source/attributes/lazy.test.js +1 -1
- package/source/attributes/track.test.js +3 -3
- package/source/channel.js +21 -0
- package/source/connection.js +112 -12
- package/source/emitter.js +23 -1
- package/source/encoders/index.js +0 -2
- package/source/events.js +2 -2
- package/source/io.js +12 -5
- package/source/shards/channel.js +28 -7
- package/source/shards/connection.js +0 -5
- package/test/amqplib.mock.js +20 -4
- package/test/channel.fixtures.js +1 -1
- package/test/channel.test.js +44 -1
- package/test/connection.mock.js +2 -0
- package/test/connection.test.js +226 -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 +25 -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 +63 -9
- package/test/shards/connection.test.js +4 -3
- package/types/channel.d.ts +4 -0
- 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 -85
- 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()
|
|
@@ -339,7 +376,7 @@ describe('create channel', () => {
|
|
|
339
376
|
|
|
340
377
|
jest.clearAllMocks()
|
|
341
378
|
|
|
342
|
-
const promise =
|
|
379
|
+
const promise = new Promex()
|
|
343
380
|
|
|
344
381
|
amqplib.connect.mockImplementationOnce(() => promise)
|
|
345
382
|
|
|
@@ -361,6 +398,106 @@ describe('create channel', () => {
|
|
|
361
398
|
})
|
|
362
399
|
})
|
|
363
400
|
|
|
401
|
+
describe('closed channels', () => {
|
|
402
|
+
/** @type {jest.MockedObject<comq.amqp.Connection>} */
|
|
403
|
+
let conn
|
|
404
|
+
|
|
405
|
+
beforeEach(async () => {
|
|
406
|
+
// an earlier case leaves an implementation of its own behind
|
|
407
|
+
create.mockImplementation(async () => ({ recover: jest.fn(async () => undefined), closed: false }))
|
|
408
|
+
|
|
409
|
+
await connection.open()
|
|
410
|
+
|
|
411
|
+
conn = await amqplib.connect.mock.results[0].value
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
afterEach(async () => {
|
|
415
|
+
await connection.close()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
// a channel given back is not one this connection has any more
|
|
419
|
+
it('should not recover one that was given back', async () => {
|
|
420
|
+
const channel = await connection.createChannel('event')
|
|
421
|
+
|
|
422
|
+
channel.closed = true
|
|
423
|
+
|
|
424
|
+
conn.emit('close', new Error())
|
|
425
|
+
|
|
426
|
+
await timeout(50)
|
|
427
|
+
|
|
428
|
+
expect(channel.recover).not.toHaveBeenCalled()
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should recover one that is still in use', async () => {
|
|
432
|
+
const channel = await connection.createChannel('event')
|
|
433
|
+
|
|
434
|
+
conn.emit('close', new Error())
|
|
435
|
+
|
|
436
|
+
await timeout(50)
|
|
437
|
+
|
|
438
|
+
expect(channel.recover).toHaveBeenCalled()
|
|
439
|
+
})
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
describe('channel exhaustion', () => {
|
|
443
|
+
const EXHAUSTED = 'No channels left to allocate'
|
|
444
|
+
|
|
445
|
+
/** @type {jest.MockedObject<comq.amqp.Connection>} */
|
|
446
|
+
let conn
|
|
447
|
+
|
|
448
|
+
beforeEach(async () => {
|
|
449
|
+
await connection.open()
|
|
450
|
+
|
|
451
|
+
conn = await amqplib.connect.mock.results[0].value
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('should reject rather than wait for a recovery that is not coming', async () => {
|
|
455
|
+
create.mockImplementation(async () => { throw new Error(EXHAUSTED) })
|
|
456
|
+
|
|
457
|
+
// the regression is a hang, so the assertion is that it settles at all
|
|
458
|
+
const settled = await Promise.race([
|
|
459
|
+
connection.createChannel('event').then(() => 'resolved', (e) => e.message),
|
|
460
|
+
timeout(100).then(() => 'pending')
|
|
461
|
+
])
|
|
462
|
+
|
|
463
|
+
expect(settled).toStrictEqual(EXHAUSTED)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('should not retry', async () => {
|
|
467
|
+
create.mockImplementation(async () => { throw new Error(EXHAUSTED) })
|
|
468
|
+
|
|
469
|
+
await connection.createChannel('event').catch(() => undefined)
|
|
470
|
+
|
|
471
|
+
expect(create).toHaveBeenCalledTimes(1)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('should report the limit the broker negotiated', async () => {
|
|
475
|
+
const listener = jest.fn()
|
|
476
|
+
|
|
477
|
+
await connection.diagnose('exhausted', listener)
|
|
478
|
+
|
|
479
|
+
create.mockImplementation(async () => { throw new Error(EXHAUSTED) })
|
|
480
|
+
|
|
481
|
+
await connection.createChannel('event').catch(() => undefined)
|
|
482
|
+
|
|
483
|
+
expect(listener).toHaveBeenCalledWith(conn.connection.channelMax)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('should leave every other failure its recovery', async () => {
|
|
487
|
+
create.mockImplementation(async () => { throw new Error() })
|
|
488
|
+
|
|
489
|
+
setTimeout(() => {
|
|
490
|
+
create.mockImplementation(async () => generate())
|
|
491
|
+
|
|
492
|
+
conn.emit('close', new Error())
|
|
493
|
+
}, 1)
|
|
494
|
+
|
|
495
|
+
const channel = await connection.createChannel('request')
|
|
496
|
+
|
|
497
|
+
expect(channel).toStrictEqual(await create.mock.results[1].value)
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
364
501
|
describe('watchdog', () => {
|
|
365
502
|
afterEach(() => {
|
|
366
503
|
jest.useRealTimers()
|
|
@@ -373,11 +510,23 @@ describe('watchdog', () => {
|
|
|
373
510
|
|
|
374
511
|
const conn = await amqplib.connect.mock.results[0].value
|
|
375
512
|
|
|
376
|
-
await jest.advanceTimersByTimeAsync(
|
|
513
|
+
await jest.advanceTimersByTimeAsync(46_000)
|
|
377
514
|
|
|
378
515
|
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
379
516
|
})
|
|
380
517
|
|
|
518
|
+
it('should reconnect after the watchdog destroys a silent connection', async () => {
|
|
519
|
+
jest.useFakeTimers()
|
|
520
|
+
|
|
521
|
+
await connection.open()
|
|
522
|
+
|
|
523
|
+
await jest.advanceTimersByTimeAsync(46_000)
|
|
524
|
+
|
|
525
|
+
// a socket destroyed without an error is a socket amqplib never reports,
|
|
526
|
+
// which leaves the connection silently dead instead of recovering
|
|
527
|
+
expect(amqplib.connect).toHaveBeenCalledTimes(2)
|
|
528
|
+
})
|
|
529
|
+
|
|
381
530
|
it('should not let a replaced socket disarm the watchdog', async () => {
|
|
382
531
|
jest.useFakeTimers()
|
|
383
532
|
|
|
@@ -407,11 +556,11 @@ describe('watchdog', () => {
|
|
|
407
556
|
|
|
408
557
|
const conn = await amqplib.connect.mock.results[0].value
|
|
409
558
|
|
|
410
|
-
await jest.advanceTimersByTimeAsync(
|
|
559
|
+
await jest.advanceTimersByTimeAsync(40_000)
|
|
411
560
|
|
|
412
561
|
conn.connection.stream.emit('data', Buffer.alloc(0))
|
|
413
562
|
|
|
414
|
-
await jest.advanceTimersByTimeAsync(
|
|
563
|
+
await jest.advanceTimersByTimeAsync(40_000)
|
|
415
564
|
|
|
416
565
|
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
417
566
|
|
|
@@ -419,6 +568,77 @@ describe('watchdog', () => {
|
|
|
419
568
|
|
|
420
569
|
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
421
570
|
})
|
|
571
|
+
|
|
572
|
+
it('should derive the watchdog from the negotiated heartbeat', async () => {
|
|
573
|
+
jest.useFakeTimers()
|
|
574
|
+
|
|
575
|
+
const connection = new Connection('amqp://localhost?heartbeat=30')
|
|
576
|
+
|
|
577
|
+
await connection.open()
|
|
578
|
+
|
|
579
|
+
const conn = await amqplib.connect.mock.results[0].value
|
|
580
|
+
|
|
581
|
+
// the broker is only expected to say something every 15 seconds
|
|
582
|
+
await jest.advanceTimersByTimeAsync(60_000)
|
|
583
|
+
|
|
584
|
+
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
585
|
+
|
|
586
|
+
await jest.advanceTimersByTimeAsync(31_000)
|
|
587
|
+
|
|
588
|
+
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('should not arm the watchdog when heartbeats are disabled', async () => {
|
|
592
|
+
jest.useFakeTimers()
|
|
593
|
+
|
|
594
|
+
const connection = new Connection('amqp://localhost?heartbeat=0')
|
|
595
|
+
|
|
596
|
+
await connection.open()
|
|
597
|
+
|
|
598
|
+
const conn = await amqplib.connect.mock.results[0].value
|
|
599
|
+
|
|
600
|
+
// an idle connection that has agreed to no heartbeats is silent by design
|
|
601
|
+
await jest.advanceTimersByTimeAsync(600_000)
|
|
602
|
+
|
|
603
|
+
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
describe('heartbeat', () => {
|
|
608
|
+
it('should ask for a heartbeat', async () => {
|
|
609
|
+
const connection = new Connection('amqp://developer@localhost')
|
|
610
|
+
|
|
611
|
+
await connection.open()
|
|
612
|
+
|
|
613
|
+
expect(amqplib.connect).toHaveBeenCalledWith('amqp://developer@localhost?heartbeat=15',
|
|
614
|
+
expect.anything())
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('should append the heartbeat to an existing query', async () => {
|
|
618
|
+
const connection = new Connection('amqp://localhost?frameMax=8192')
|
|
619
|
+
|
|
620
|
+
await connection.open()
|
|
621
|
+
|
|
622
|
+
expect(amqplib.connect).toHaveBeenCalledWith('amqp://localhost?frameMax=8192&heartbeat=15',
|
|
623
|
+
expect.anything())
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it.each(['amqp://localhost?heartbeat=5', 'amqp://localhost?heartbeat=0'])(
|
|
627
|
+
'should keep the heartbeat requested by the caller (%s)',
|
|
628
|
+
async (url) => {
|
|
629
|
+
const connection = new Connection(url)
|
|
630
|
+
|
|
631
|
+
await connection.open()
|
|
632
|
+
|
|
633
|
+
expect(amqplib.connect).toHaveBeenCalledWith(url, expect.anything())
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('should keep the socket alive', async () => {
|
|
637
|
+
await connection.open()
|
|
638
|
+
|
|
639
|
+
expect(amqplib.connect)
|
|
640
|
+
.toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ keepAlive: true }))
|
|
641
|
+
})
|
|
422
642
|
})
|
|
423
643
|
|
|
424
644
|
describe('close', () => {
|
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')
|
|
@@ -92,8 +93,29 @@ describe('close', () => {
|
|
|
92
93
|
await expect(io.close()).resolves.not.toThrow()
|
|
93
94
|
})
|
|
94
95
|
|
|
96
|
+
// a connection is shared and outlives its IOs, and it has a limited number of channels
|
|
97
|
+
it('should give the channels back', async () => {
|
|
98
|
+
await reply()
|
|
99
|
+
await io.close()
|
|
100
|
+
|
|
101
|
+
expect(requests.close).toHaveBeenCalled()
|
|
102
|
+
expect(events.close).toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should give them back before letting go of the connection', async () => {
|
|
106
|
+
await reply()
|
|
107
|
+
|
|
108
|
+
connection.close.mockImplementation(async () => {
|
|
109
|
+
expect(requests.close).toHaveBeenCalled()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
await io.close()
|
|
113
|
+
|
|
114
|
+
expect.assertions(1)
|
|
115
|
+
})
|
|
116
|
+
|
|
95
117
|
it('should wait for event processing completion', async () => {
|
|
96
|
-
const promise = /** @type {Promise<void>} */
|
|
118
|
+
const promise = /** @type {Promise<void>} */ new Promex()
|
|
97
119
|
const queue = generate()
|
|
98
120
|
const group = generate()
|
|
99
121
|
const consumer = jest.fn(async () => promise)
|
|
@@ -128,7 +150,7 @@ describe('close', () => {
|
|
|
128
150
|
})
|
|
129
151
|
|
|
130
152
|
it('should wait for request processing completion', async () => {
|
|
131
|
-
const promise = /** @type {Promise<void>} */
|
|
153
|
+
const promise = /** @type {Promise<void>} */ new Promex()
|
|
132
154
|
const producer = jest.fn(async () => promise)
|
|
133
155
|
|
|
134
156
|
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', 'error', 'reconnect'])('should re-emit %s from connection',
|
|
64
|
+
it.each(['open', 'close', 'error', 'reconnect', 'exhausted'])('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
|