comq 0.24.1 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/commit-msg +5 -0
- package/benchmarks/run.js +66 -4
- package/benchmarks/server.js +14 -0
- package/commitlint.config.js +6 -0
- package/features/rpc.streams.feature +28 -0
- package/features/steps/retransmission.js +1 -1
- package/features/steps/rpc.js +31 -1
- package/package.json +18 -10
- package/readme.md +6 -0
- package/source/.io/ReplyPipe.js +130 -11
- package/source/.io/ReplyStream.js +41 -2
- package/source/.io/const.js +6 -0
- package/source/attributes/track.test.js +1 -1
- package/source/channel.js +16 -3
- package/source/connection.js +2 -2
- package/source/io.js +1 -1
- package/source/promex.js +47 -0
- package/source/promex.test.js +79 -0
- package/source/retry.js +42 -0
- package/source/retry.test.js +120 -0
- package/source/shards/channel.js +1 -1
- package/test/.types/jest.d.ts +22 -0
- package/test/ReplyPipe.test.js +71 -2
- package/test/ReplyStream.test.js +33 -0
- package/test/channel.test.js +25 -0
- package/test/connection.test.js +1 -1
- package/test/globals.js +143 -0
- package/test/io.close.test.js +1 -1
- package/test/io.request.test.js +1 -1
- package/test/shards/channel.test.js +1 -1
- package/test/shards/connection.test.js +2 -3
package/source/promex.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A promise settled by whoever holds it, rather than only from within its executor.
|
|
5
|
+
* `callback` settles it the way node-style callbacks report: an error, or a value.
|
|
6
|
+
*
|
|
7
|
+
* @template T
|
|
8
|
+
* @extends {Promise<T>}
|
|
9
|
+
*/
|
|
10
|
+
class Promex extends Promise {
|
|
11
|
+
/** @type {(value?: T | PromiseLike<T>) => void} */
|
|
12
|
+
resolve
|
|
13
|
+
|
|
14
|
+
/** @type {(reason?: any) => void} */
|
|
15
|
+
reject
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {(resolve: (value?: any) => void, reject: (reason?: any) => void) => void} [executor]
|
|
19
|
+
*/
|
|
20
|
+
constructor (executor = noop) {
|
|
21
|
+
let ok
|
|
22
|
+
let oh
|
|
23
|
+
|
|
24
|
+
super((resolve, reject) => {
|
|
25
|
+
ok = resolve
|
|
26
|
+
oh = reject
|
|
27
|
+
|
|
28
|
+
return executor(resolve, reject)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
this.resolve = ok
|
|
32
|
+
this.reject = oh
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {any} error
|
|
37
|
+
* @param {T} [value]
|
|
38
|
+
*/
|
|
39
|
+
callback = (error, value) => {
|
|
40
|
+
if (error !== null && error !== undefined) this.reject(error)
|
|
41
|
+
else this.resolve(value)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function noop () {}
|
|
46
|
+
|
|
47
|
+
exports.Promex = Promex
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Promex } = require('./promex')
|
|
4
|
+
|
|
5
|
+
it('should resolve', async () => {
|
|
6
|
+
const promise = new Promex()
|
|
7
|
+
|
|
8
|
+
setImmediate(() => promise.resolve('ok'))
|
|
9
|
+
|
|
10
|
+
await expect(promise).resolves.toStrictEqual('ok')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should reject', async () => {
|
|
14
|
+
const promise = new Promex()
|
|
15
|
+
|
|
16
|
+
setImmediate(() => promise.reject(new Error('oh')))
|
|
17
|
+
|
|
18
|
+
await expect(promise).rejects.toThrow('oh')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should resolve using the callback', async () => {
|
|
22
|
+
const promise = new Promex()
|
|
23
|
+
|
|
24
|
+
setImmediate(() => promise.callback(null, 'ok'))
|
|
25
|
+
|
|
26
|
+
await expect(promise).resolves.toStrictEqual('ok')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should reject using the callback', async () => {
|
|
30
|
+
const promise = new Promex()
|
|
31
|
+
|
|
32
|
+
setImmediate(() => promise.callback('oh'))
|
|
33
|
+
|
|
34
|
+
await expect(promise).rejects.toStrictEqual('oh')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should resolve using the callback given no error', async () => {
|
|
38
|
+
const promise = new Promex()
|
|
39
|
+
|
|
40
|
+
setImmediate(() => promise.callback(undefined, 'ok'))
|
|
41
|
+
|
|
42
|
+
await expect(promise).resolves.toStrictEqual('ok')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should run the executor it is given', async () => {
|
|
46
|
+
const promise = new Promex((resolve) => resolve('ok'))
|
|
47
|
+
|
|
48
|
+
await expect(promise).resolves.toStrictEqual('ok')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should stay settled by whoever holds it', async () => {
|
|
52
|
+
const promise = new Promex()
|
|
53
|
+
|
|
54
|
+
promise.resolve('first')
|
|
55
|
+
promise.resolve('second')
|
|
56
|
+
|
|
57
|
+
await expect(promise).resolves.toStrictEqual('first')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should be a promise', async () => {
|
|
61
|
+
const promise = new Promex()
|
|
62
|
+
|
|
63
|
+
expect(promise).toBeInstanceOf(Promise)
|
|
64
|
+
|
|
65
|
+
promise.resolve('ok')
|
|
66
|
+
|
|
67
|
+
await expect(Promise.all([promise])).resolves.toStrictEqual(['ok'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should derive promises of its own kind', async () => {
|
|
71
|
+
const promise = new Promex()
|
|
72
|
+
const derived = promise.then((value) => value + '!')
|
|
73
|
+
|
|
74
|
+
expect(derived).toBeInstanceOf(Promex)
|
|
75
|
+
|
|
76
|
+
promise.resolve('ok')
|
|
77
|
+
|
|
78
|
+
await expect(derived).resolves.toStrictEqual('ok!')
|
|
79
|
+
})
|
package/source/retry.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs an operation until it stops asking for another attempt, waiting longer before
|
|
5
|
+
* each one. The operation is handed the token it returns to ask for a retry; anything
|
|
6
|
+
* else it returns is the result, and whatever it throws is thrown on.
|
|
7
|
+
*
|
|
8
|
+
* @template T
|
|
9
|
+
* @param {(again: symbol) => Promise<T | symbol>} operation
|
|
10
|
+
* @param {Partial<typeof DEFAULTS>} [options]
|
|
11
|
+
* @returns {Promise<T>}
|
|
12
|
+
*/
|
|
13
|
+
async function retry (operation, options) {
|
|
14
|
+
const { attempts, base, max, factor, dispersion } = { ...DEFAULTS, ...options }
|
|
15
|
+
|
|
16
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
17
|
+
const result = await operation(again)
|
|
18
|
+
|
|
19
|
+
if (result !== again) return /** @type {T} */ (result)
|
|
20
|
+
|
|
21
|
+
const interval = Math.min(base * factor ** attempt, max)
|
|
22
|
+
|
|
23
|
+
// attempts that started together are spread out rather than kept in step
|
|
24
|
+
const spread = interval * dispersion * (Math.random() - 0.5)
|
|
25
|
+
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, interval + spread))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error('Maximum attempts exceeded')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const again = Symbol('again')
|
|
33
|
+
|
|
34
|
+
const DEFAULTS = {
|
|
35
|
+
attempts: Infinity,
|
|
36
|
+
base: 1000,
|
|
37
|
+
max: 30000,
|
|
38
|
+
factor: 1.5,
|
|
39
|
+
dispersion: 0.1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.retry = retry
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { retry } = require('./retry')
|
|
4
|
+
|
|
5
|
+
const immediate = { base: 0, max: 0, dispersion: 0 }
|
|
6
|
+
|
|
7
|
+
it('should return the result', async () => {
|
|
8
|
+
const result = await retry(async () => 'ok', immediate)
|
|
9
|
+
|
|
10
|
+
expect(result).toStrictEqual('ok')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should retry while the operation asks for it', async () => {
|
|
14
|
+
let attempts = 0
|
|
15
|
+
|
|
16
|
+
const result = await retry(async (again) => {
|
|
17
|
+
attempts++
|
|
18
|
+
|
|
19
|
+
return attempts < 3 ? again : 'ok'
|
|
20
|
+
}, immediate)
|
|
21
|
+
|
|
22
|
+
expect(attempts).toStrictEqual(3)
|
|
23
|
+
expect(result).toStrictEqual('ok')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should pass the same token on every attempt', async () => {
|
|
27
|
+
const tokens = []
|
|
28
|
+
|
|
29
|
+
await retry(async (again) => {
|
|
30
|
+
tokens.push(again)
|
|
31
|
+
|
|
32
|
+
return tokens.length < 3 ? again : 'ok'
|
|
33
|
+
}, immediate)
|
|
34
|
+
|
|
35
|
+
expect(tokens).toHaveLength(3)
|
|
36
|
+
expect(new Set(tokens).size).toStrictEqual(1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should throw what the operation throws', async () => {
|
|
40
|
+
const exception = new Error('nope')
|
|
41
|
+
|
|
42
|
+
await expect(retry(async () => { throw exception }, immediate)).rejects.toThrow(exception)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should give up after the last attempt', async () => {
|
|
46
|
+
let attempts = 0
|
|
47
|
+
|
|
48
|
+
const attempt = retry(async (again) => {
|
|
49
|
+
attempts++
|
|
50
|
+
|
|
51
|
+
return again
|
|
52
|
+
}, { ...immediate, attempts: 3 })
|
|
53
|
+
|
|
54
|
+
await expect(attempt).rejects.toThrow('Maximum attempts exceeded')
|
|
55
|
+
expect(attempts).toStrictEqual(3)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should not run the operation without an attempt to spare', async () => {
|
|
59
|
+
const operation = jest.fn(async (again) => again)
|
|
60
|
+
|
|
61
|
+
await expect(retry(operation, { ...immediate, attempts: 0 }))
|
|
62
|
+
.rejects.toThrow('Maximum attempts exceeded')
|
|
63
|
+
|
|
64
|
+
expect(operation).not.toHaveBeenCalled()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should back off, up to the maximum, before each attempt', async () => {
|
|
68
|
+
jest.useFakeTimers()
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const waits = []
|
|
72
|
+
|
|
73
|
+
let last = Date.now()
|
|
74
|
+
let attempts = 0
|
|
75
|
+
|
|
76
|
+
const attempt = retry(async (again) => {
|
|
77
|
+
waits.push(Date.now() - last)
|
|
78
|
+
last = Date.now()
|
|
79
|
+
attempts++
|
|
80
|
+
|
|
81
|
+
return attempts < 5 ? again : 'ok'
|
|
82
|
+
}, { base: 100, factor: 2, max: 300, dispersion: 0 })
|
|
83
|
+
|
|
84
|
+
await jest.advanceTimersByTimeAsync(10_000)
|
|
85
|
+
|
|
86
|
+
expect(await attempt).toStrictEqual('ok')
|
|
87
|
+
|
|
88
|
+
// the first attempt waits for nothing; then 100, 200, and 400 capped at 300
|
|
89
|
+
expect(waits).toStrictEqual([0, 100, 200, 300, 300])
|
|
90
|
+
} finally {
|
|
91
|
+
jest.useRealTimers()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should spread the wait around the interval', async () => {
|
|
96
|
+
const waits = []
|
|
97
|
+
|
|
98
|
+
const timer = global.setTimeout
|
|
99
|
+
|
|
100
|
+
global.setTimeout = (callback, ms) => {
|
|
101
|
+
waits.push(ms)
|
|
102
|
+
|
|
103
|
+
return timer(callback, 0)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
let attempts = 0
|
|
108
|
+
|
|
109
|
+
await retry(async (again) => {
|
|
110
|
+
attempts++
|
|
111
|
+
|
|
112
|
+
return attempts < 20 ? again : 'ok'
|
|
113
|
+
}, { base: 1000, factor: 1, max: 1000, dispersion: 0.1 })
|
|
114
|
+
} finally {
|
|
115
|
+
global.setTimeout = timer
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
expect(waits.every((ms) => ms >= 950 && ms <= 1050)).toStrictEqual(true)
|
|
119
|
+
expect(new Set(waits).size).toBeGreaterThan(1)
|
|
120
|
+
})
|
package/source/shards/channel.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as mock from 'jest-mock'
|
|
2
|
+
|
|
3
|
+
// The test files are written against Jest's mock types, and keep being: the mocks are still
|
|
4
|
+
// Jest's, taken as the standalone `jest-mock` package. This is what @types/jest declared of it.
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
|
|
8
|
+
namespace jest {
|
|
9
|
+
|
|
10
|
+
type Mock<T extends mock.FunctionLike = mock.UnknownFunction> = mock.Mock<T>
|
|
11
|
+
|
|
12
|
+
type Mocked<T extends object> = mock.Mocked<T>
|
|
13
|
+
|
|
14
|
+
type MockedClass<T extends mock.ClassLike> = mock.MockedClass<T>
|
|
15
|
+
|
|
16
|
+
type MockedFunction<T extends mock.FunctionLike = mock.UnknownFunction> = mock.MockedFunction<T>
|
|
17
|
+
|
|
18
|
+
type MockedFn<T extends mock.FunctionLike = mock.UnknownFunction> = mock.MockedFunction<T>
|
|
19
|
+
|
|
20
|
+
type MockedObject<T extends object> = mock.MockedObject<T>
|
|
21
|
+
}
|
|
22
|
+
}
|
package/test/ReplyPipe.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Readable } = require('node:stream')
|
|
4
|
-
const { Promex } = require('promex')
|
|
4
|
+
const { Promex } = require('../source/promex')
|
|
5
5
|
const { ReplyPipe } = require('../source/.io/ReplyPipe')
|
|
6
6
|
const { createReplyEmitter } = require('../source/.io/createReplyEmitter')
|
|
7
7
|
const { control, FLOW_HEADER } = require('../source/.io/const')
|
|
@@ -96,10 +96,14 @@ it('should pull the source no faster than the channel takes', async () => {
|
|
|
96
96
|
})
|
|
97
97
|
|
|
98
98
|
it('should hold the source while the consumer has asked for a pause', async () => {
|
|
99
|
-
|
|
99
|
+
// the pause is asked for while the confirmation is still being published: the pipe does not
|
|
100
|
+
// read its source until that has been published, so nothing can be in flight yet
|
|
101
|
+
const opening = ReplyPipe.create(request, Readable.from([1, 2, 3]), channel, feedback, reply)
|
|
100
102
|
|
|
101
103
|
feedback.emit(request.properties.correlationId, control.pause, {})
|
|
102
104
|
|
|
105
|
+
const pipe = await opening
|
|
106
|
+
|
|
103
107
|
await timeout(10)
|
|
104
108
|
|
|
105
109
|
expect(messages()).toStrictEqual([control.ok])
|
|
@@ -208,3 +212,68 @@ it('should destroy the source if the confirmation cannot be sent', async () => {
|
|
|
208
212
|
|
|
209
213
|
expect(source.destroyed).toStrictEqual(true)
|
|
210
214
|
})
|
|
215
|
+
|
|
216
|
+
describe('batches', () => {
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
request.properties.contentType = 'application/json'
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should send the values its source holds as one message', async () => {
|
|
222
|
+
const pipe = await ReplyPipe.create(request, Readable.from([1, 2, 3]), channel, feedback, reply)
|
|
223
|
+
|
|
224
|
+
await closing(pipe)
|
|
225
|
+
|
|
226
|
+
expect(messages()).toStrictEqual([control.ok, [1, 2, 3], control.end])
|
|
227
|
+
expect(sent[1][1].type).toStrictEqual('batch')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should send each value that arrives on its own as it arrives', async () => {
|
|
231
|
+
async function * source () {
|
|
232
|
+
yield 1
|
|
233
|
+
await timeout(10)
|
|
234
|
+
yield 2
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const pipe = await ReplyPipe.create(request, Readable.from(source()), channel, feedback, reply)
|
|
238
|
+
|
|
239
|
+
await closing(pipe)
|
|
240
|
+
|
|
241
|
+
expect(messages()).toStrictEqual([control.ok, 1, 2, control.end])
|
|
242
|
+
expect(sent[1][1].type).toBeUndefined()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should send buffers as one message, each prefixed with its length', async () => {
|
|
246
|
+
const source = Readable.from([Buffer.from('ab'), Buffer.from('cde')])
|
|
247
|
+
const pipe = await ReplyPipe.create(request, source, channel, feedback, reply)
|
|
248
|
+
|
|
249
|
+
await closing(pipe)
|
|
250
|
+
|
|
251
|
+
const expected = Buffer.from([0, 0, 0, 2, 0x61, 0x62, 0, 0, 0, 3, 0x63, 0x64, 0x65])
|
|
252
|
+
|
|
253
|
+
expect(messages()[1]).toStrictEqual(expected)
|
|
254
|
+
expect(sent[1][1].type).toStrictEqual('buffers')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should send values one at a time in an encoding that holds no list', async () => {
|
|
258
|
+
request.properties.contentType = 'text/plain'
|
|
259
|
+
|
|
260
|
+
const pipe = await ReplyPipe.create(request, Readable.from(['a', 'b']), channel, feedback, reply)
|
|
261
|
+
|
|
262
|
+
await closing(pipe)
|
|
263
|
+
|
|
264
|
+
expect(messages()).toStrictEqual([control.ok, 'a', 'b', control.end])
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should keep a batch within its limit', async () => {
|
|
268
|
+
const values = Array.from({ length: 300 }, (_, i) => i)
|
|
269
|
+
const source = Readable.from(values, { highWaterMark: 1000 })
|
|
270
|
+
const pipe = await ReplyPipe.create(request, source, channel, feedback, reply)
|
|
271
|
+
|
|
272
|
+
await closing(pipe)
|
|
273
|
+
|
|
274
|
+
const batches = messages().slice(1, -1)
|
|
275
|
+
|
|
276
|
+
expect(batches.flat()).toStrictEqual(values)
|
|
277
|
+
expect(Math.max(...batches.map((batch) => [batch].flat().length))).toBeLessThanOrEqual(128)
|
|
278
|
+
})
|
|
279
|
+
})
|
package/test/ReplyStream.test.js
CHANGED
|
@@ -204,3 +204,36 @@ describe('flow', () => {
|
|
|
204
204
|
}
|
|
205
205
|
})
|
|
206
206
|
})
|
|
207
|
+
|
|
208
|
+
describe('batches', () => {
|
|
209
|
+
const ok = { headers: { index: 0, [FLOW_HEADER]: true }, type: 'control' }
|
|
210
|
+
const end = (index) => [control.end, { headers: { index }, type: 'control' }]
|
|
211
|
+
|
|
212
|
+
it('should yield each value of a batch, in order', async () => {
|
|
213
|
+
stream.arrange(control.ok, ok)
|
|
214
|
+
stream.arrange([1, 2, 3], { headers: { index: 1 }, type: 'batch' })
|
|
215
|
+
stream.arrange(4, { headers: { index: 2 } })
|
|
216
|
+
stream.arrange(...end(3))
|
|
217
|
+
|
|
218
|
+
expect(await stream.toArray()).toStrictEqual([1, 2, 3, 4])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should yield each buffer of a batch of buffers', async () => {
|
|
222
|
+
const buffers = Buffer.from([0, 0, 0, 2, 0x61, 0x62, 0, 0, 0, 0, 0, 0, 0, 1, 0x63])
|
|
223
|
+
|
|
224
|
+
stream.arrange(control.ok, ok)
|
|
225
|
+
stream.arrange(buffers, { headers: { index: 1 }, type: 'buffers' })
|
|
226
|
+
stream.arrange(...end(2))
|
|
227
|
+
|
|
228
|
+
expect(await stream.toArray()).toStrictEqual([Buffer.from('ab'), Buffer.alloc(0), Buffer.from('c')])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should hold a batch that arrives out of order until the gap is filled', async () => {
|
|
232
|
+
stream.arrange(control.ok, ok)
|
|
233
|
+
stream.arrange([3, 4], { headers: { index: 2 }, type: 'batch' })
|
|
234
|
+
stream.arrange([1, 2], { headers: { index: 1 }, type: 'batch' })
|
|
235
|
+
stream.arrange(...end(3))
|
|
236
|
+
|
|
237
|
+
expect(await stream.toArray()).toStrictEqual([1, 2, 3, 4])
|
|
238
|
+
})
|
|
239
|
+
})
|
package/test/channel.test.js
CHANGED
|
@@ -1251,6 +1251,31 @@ describe('failed messages', () => {
|
|
|
1251
1251
|
expect(chan.ack).toHaveBeenCalled()
|
|
1252
1252
|
})
|
|
1253
1253
|
|
|
1254
|
+
it('should release the original before the channel is given back', async () => {
|
|
1255
|
+
await channel.consume(queue, consumer)
|
|
1256
|
+
|
|
1257
|
+
let confirm
|
|
1258
|
+
|
|
1259
|
+
chan.publish.mockImplementationOnce((_0, _1, _2, _3, callback) => (confirm = callback))
|
|
1260
|
+
|
|
1261
|
+
const delivering = deliver(delivery())
|
|
1262
|
+
|
|
1263
|
+
// closed while the copy is on its way: an original still unacknowledged then is
|
|
1264
|
+
// requeued by the broker, and the message is held twice
|
|
1265
|
+
const closing = channel.close()
|
|
1266
|
+
|
|
1267
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
1268
|
+
|
|
1269
|
+
confirm(null)
|
|
1270
|
+
|
|
1271
|
+
await closing
|
|
1272
|
+
await delivering
|
|
1273
|
+
|
|
1274
|
+
expect(chan.ack).toHaveBeenCalledTimes(1)
|
|
1275
|
+
expect(chan.ack.mock.invocationCallOrder[0])
|
|
1276
|
+
.toBeLessThan(chan.close.mock.invocationCallOrder[0])
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1254
1279
|
it('should increment the attempt', async () => {
|
|
1255
1280
|
await channel.consume(queue, consumer)
|
|
1256
1281
|
|
package/test/connection.test.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { generate } = require('randomstring')
|
|
6
6
|
|
|
7
|
-
const { Promex } = require('promex')
|
|
7
|
+
const { Promex } = require('../source/promex')
|
|
8
8
|
const { timeout, random } = require('./helpers')
|
|
9
9
|
const { amqplib, connect } = require('./amqplib.mock')
|
|
10
10
|
const { channel: create } = require('./connection.mock')
|
package/test/globals.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Jest's globals, on top of node:test.
|
|
4
|
+
//
|
|
5
|
+
// The runner is Node's own; the assertions, the mocks and the fake timers are still Jest's,
|
|
6
|
+
// taken as the standalone packages they are published as. Loaded with `--import`, so that
|
|
7
|
+
// the test files themselves keep seeing `it`, `expect` and `jest` as globals.
|
|
8
|
+
|
|
9
|
+
const { createRequire } = require('node:module')
|
|
10
|
+
const { format } = require('node:util')
|
|
11
|
+
const nodeTest = require('node:test')
|
|
12
|
+
const { expect } = require('expect')
|
|
13
|
+
const { ModuleMocker } = require('jest-mock')
|
|
14
|
+
const { ModernFakeTimers } = require('@jest/fake-timers')
|
|
15
|
+
|
|
16
|
+
// node:test drives its own queue with process.nextTick: faking it, as Jest does inside its
|
|
17
|
+
// sandbox, stops the runner mid-file and the tests left behind are never reported as missing
|
|
18
|
+
const DO_NOT_FAKE = ['nextTick']
|
|
19
|
+
|
|
20
|
+
const mocker = new ModuleMocker(globalThis)
|
|
21
|
+
const timers = new ModernFakeTimers({ global: globalThis, config: { rootDir: process.cwd() } })
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Where `jest.mock` was called from, so that its specifier resolves
|
|
25
|
+
* relative to the test file rather than to this one.
|
|
26
|
+
*
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function origin () {
|
|
30
|
+
const prepare = Error.prepareStackTrace
|
|
31
|
+
|
|
32
|
+
Error.prepareStackTrace = (_, frames) => frames
|
|
33
|
+
|
|
34
|
+
const { stack } = new Error('origin')
|
|
35
|
+
|
|
36
|
+
Error.prepareStackTrace = prepare
|
|
37
|
+
|
|
38
|
+
for (const frame of stack) {
|
|
39
|
+
const file = frame.getFileName()
|
|
40
|
+
|
|
41
|
+
if (file !== null && file !== undefined && file !== __filename && !file.startsWith('node:')) {
|
|
42
|
+
return file
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error('cannot tell which file has called jest.mock')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Replaces a module, as `jest.mock` does. Without a factory the module is automocked:
|
|
51
|
+
* every function it exports becomes a mock function.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} specifier
|
|
54
|
+
* @param {() => object} [factory]
|
|
55
|
+
*/
|
|
56
|
+
function mockModule (specifier, factory) {
|
|
57
|
+
if (nodeTest.mock.module === undefined) {
|
|
58
|
+
throw new Error('jest.mock requires node --experimental-test-module-mocks')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const path = createRequire(origin()).resolve(specifier)
|
|
62
|
+
|
|
63
|
+
const namedExports = factory === undefined
|
|
64
|
+
? mocker.generateFromMetadata(mocker.getMetadata(require(path)))
|
|
65
|
+
: factory()
|
|
66
|
+
|
|
67
|
+
nodeTest.mock.module(path, { namedExports, cache: false })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const jest = {
|
|
71
|
+
fn: (implementation) => mocker.fn(implementation),
|
|
72
|
+
spyOn: (object, property, accessType) => mocker.spyOn(object, property, accessType),
|
|
73
|
+
clearAllMocks: () => mocker.clearAllMocks(),
|
|
74
|
+
resetAllMocks: () => mocker.resetAllMocks(),
|
|
75
|
+
restoreAllMocks: () => mocker.restoreAllMocks(),
|
|
76
|
+
mock: mockModule,
|
|
77
|
+
useFakeTimers: (config) => timers.useFakeTimers({ doNotFake: DO_NOT_FAKE, ...config }),
|
|
78
|
+
useRealTimers: () => timers.useRealTimers(),
|
|
79
|
+
advanceTimersByTime: (ms) => timers.advanceTimersByTime(ms),
|
|
80
|
+
advanceTimersByTimeAsync: (ms) => timers.advanceTimersByTimeAsync(ms),
|
|
81
|
+
runAllTimers: () => timers.runAllTimers(),
|
|
82
|
+
runAllTimersAsync: () => timers.runAllTimersAsync(),
|
|
83
|
+
runOnlyPendingTimers: () => timers.runOnlyPendingTimers(),
|
|
84
|
+
getTimerCount: () => timers.getTimerCount(),
|
|
85
|
+
setSystemTime: (now) => timers.setSystemTime(now)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `it.each` / `describe.each`: one test per row, the row spread into the callback,
|
|
90
|
+
* the title formatted with the row's values.
|
|
91
|
+
*
|
|
92
|
+
* @param {Function} declare
|
|
93
|
+
* @returns {(table: any[]) => (title: string, fn: Function, options?: object) => void}
|
|
94
|
+
*/
|
|
95
|
+
const each = (declare) => (table) => (title, fn, options) => {
|
|
96
|
+
const placeholders = (title.replace(/%%/g, '').match(/%[sdifjoOc]/g) ?? []).length
|
|
97
|
+
|
|
98
|
+
for (const row of table) {
|
|
99
|
+
const values = Array.isArray(row) ? row : [row]
|
|
100
|
+
|
|
101
|
+
declare(format(title, ...values.slice(0, placeholders)), options, () => fn(...values))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Jest's `it` and `describe` carry `.each`; node:test's do not.
|
|
107
|
+
*
|
|
108
|
+
* @param {Function} declare
|
|
109
|
+
* @returns {Function}
|
|
110
|
+
*/
|
|
111
|
+
function extend (declare) {
|
|
112
|
+
// Jest takes a timeout where node:test takes options
|
|
113
|
+
const call = (...args) => declare(...(typeof args[2] === 'number'
|
|
114
|
+
? [args[0], { timeout: args[2] }, args[1]]
|
|
115
|
+
: args))
|
|
116
|
+
|
|
117
|
+
const extended = Object.assign(call, declare, { each: each(call) })
|
|
118
|
+
|
|
119
|
+
extended.only = Object.assign((...args) => declare.only(...args), { each: each(declare.only) })
|
|
120
|
+
extended.skip = Object.assign((...args) => declare.skip(...args), { each: each(declare.skip) })
|
|
121
|
+
extended.todo = (...args) => declare.todo(...args)
|
|
122
|
+
|
|
123
|
+
return extended
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Object.assign(globalThis, {
|
|
127
|
+
jest,
|
|
128
|
+
expect,
|
|
129
|
+
it: extend(nodeTest.it),
|
|
130
|
+
test: extend(nodeTest.test),
|
|
131
|
+
describe: extend(nodeTest.describe),
|
|
132
|
+
beforeEach: nodeTest.beforeEach,
|
|
133
|
+
afterEach: nodeTest.afterEach,
|
|
134
|
+
beforeAll: nodeTest.before,
|
|
135
|
+
afterAll: nodeTest.after
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// `expect.assertions` is only enforced by whoever runs the test
|
|
139
|
+
nodeTest.afterEach(() => {
|
|
140
|
+
const errors = expect.extractExpectedAssertionsErrors()
|
|
141
|
+
|
|
142
|
+
if (errors.length > 0) throw errors[0].error
|
|
143
|
+
})
|
package/test/io.close.test.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomBytes } = require('node:crypto')
|
|
4
4
|
const { generate } = require('randomstring')
|
|
5
|
-
const { Promex } = require('promex')
|
|
5
|
+
const { Promex } = require('../source/promex')
|
|
6
6
|
const { immediate } = require('./helpers')
|
|
7
7
|
|
|
8
8
|
const mock = require('./connection.mock')
|
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 { Promex } = require('promex')
|
|
8
|
+
const { Promex } = require('../source/promex')
|
|
9
9
|
const { immediate } = require('./helpers')
|
|
10
10
|
const { encode } = require('../source/encode')
|
|
11
11
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomBytes } = require('node:crypto')
|
|
4
4
|
const { generate } = require('randomstring')
|
|
5
|
-
const { Promex } = require('promex')
|
|
5
|
+
const { Promex } = require('../../source/promex')
|
|
6
6
|
const { sample, immediate, random } = require('../helpers')
|
|
7
7
|
|
|
8
8
|
const mock = require('../connection.mock')
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { generate } = require('randomstring')
|
|
4
|
-
const { Promex } = require('promex')
|
|
4
|
+
const { Promex } = require('../../source/promex')
|
|
5
5
|
const { random, immediate } = require('../helpers')
|
|
6
6
|
|
|
7
|
-
const { Connection } = require('../../source/shards')
|
|
8
|
-
|
|
9
7
|
const mock = require('../connection.mock')
|
|
10
8
|
|
|
11
9
|
jest.mock('../../source/shards/channel')
|
|
12
10
|
|
|
11
|
+
const { Connection } = require('../../source/shards')
|
|
13
12
|
const { create } = require('../../source/shards/channel')
|
|
14
13
|
|
|
15
14
|
it('should be', async () => {
|