comq 0.25.1 → 0.26.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/docs/headers.md +16 -1
- package/features/properties.feature +39 -0
- package/features/shards.virtual.feature +89 -0
- package/features/steps/context.js +29 -2
- package/features/steps/hooks.js +1 -0
- package/features/steps/names.js +245 -0
- package/features/steps/networks.js +23 -5
- package/features/steps/rpc.js +14 -2
- package/package.json +1 -1
- package/readme.md +102 -4
- package/source/channel.js +147 -2
- package/source/connect.js +18 -15
- package/source/connection.js +8 -5
- package/source/events.js +7 -0
- package/source/io.js +31 -9
- package/source/shards/channel.js +289 -19
- package/source/shards/connection.js +404 -18
- package/source/shards/expand.js +57 -0
- package/source/shards/index.js +2 -0
- package/source/shards/resolve.js +134 -0
- package/source/shards/watch.js +257 -0
- package/source/singleton.js +3 -2
- package/test/amqplib.mock.js +1 -0
- package/test/assert.test.js +7 -2
- package/test/channel.quiet.test.js +301 -0
- package/test/connect.test.js +49 -8
- package/test/connection.mock.js +3 -0
- package/test/connection.test.js +15 -0
- package/test/io.call.test.js +1 -1
- package/test/io.diagnostics.test.js +1 -1
- package/test/io.reply.test.js +12 -1
- package/test/io.request.test.js +18 -0
- package/test/io.shards.test.js +85 -0
- package/test/shards/channel.test.js +411 -0
- package/test/shards/connection.test.js +372 -17
- package/test/shards/expand.test.js +58 -0
- package/test/shards/resolve.test.js +73 -0
- package/test/shards/watch.test.js +264 -0
- package/test/singleton.test.js +1 -1
- package/types/channel.d.ts +23 -0
- package/types/connection.d.ts +13 -2
- package/types/diagnostic.d.ts +2 -1
- package/types/io.d.ts +17 -1
- package/types/shards.d.ts +58 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { retry } = require('../retry')
|
|
4
|
+
const emitter = require('../emitter')
|
|
5
|
+
const { resolve, choose } = require('./resolve')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Watches what a set of names resolve to, and tells once it has changed for good.
|
|
9
|
+
*
|
|
10
|
+
* A change is seldom seen whole. Records are updated one after another, and until their TTL has
|
|
11
|
+
* run out the caches on the way answer with the old address and the new one in turn. So what is
|
|
12
|
+
* observed is a candidate, and it replaces what is in effect only once it has been observed
|
|
13
|
+
* unchanged for the settle time; anything else observed in between starts that time anew.
|
|
14
|
+
*/
|
|
15
|
+
class Watch {
|
|
16
|
+
/** @type {string[]} */
|
|
17
|
+
#hosts
|
|
18
|
+
|
|
19
|
+
/** @type {comq.shards.Timing} */
|
|
20
|
+
#options
|
|
21
|
+
|
|
22
|
+
/** @type {(host: string) => boolean} */
|
|
23
|
+
#urgent
|
|
24
|
+
|
|
25
|
+
/** @type {Map<string, string>} what is in effect */
|
|
26
|
+
#current = new Map()
|
|
27
|
+
|
|
28
|
+
/** @type {Map<string, string>} what was observed last */
|
|
29
|
+
#candidate = new Map()
|
|
30
|
+
|
|
31
|
+
/** @type {number} when the candidate was first observed as it is */
|
|
32
|
+
#since = 0
|
|
33
|
+
|
|
34
|
+
/** @type {number} the longest TTL any name has been answered with, in seconds */
|
|
35
|
+
#ttl = 0
|
|
36
|
+
|
|
37
|
+
/** @type {boolean} whether a name was answered without a TTL */
|
|
38
|
+
#untimed = false
|
|
39
|
+
|
|
40
|
+
/** @type {ReturnType<setTimeout> | undefined} */
|
|
41
|
+
#timer
|
|
42
|
+
|
|
43
|
+
#polling = false
|
|
44
|
+
#poked = false
|
|
45
|
+
#stopped = false
|
|
46
|
+
|
|
47
|
+
#events = emitter.create()
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string[]} hosts
|
|
51
|
+
* @param {comq.shards.Timing} [options]
|
|
52
|
+
* @param {(host: string) => boolean} [urgent] whether a name is of no use as it is resolved
|
|
53
|
+
* now, so that what it resolves to next is taken at once
|
|
54
|
+
*/
|
|
55
|
+
constructor (hosts, options = {}, urgent = () => false) {
|
|
56
|
+
this.#hosts = Array.from(new Set(hosts))
|
|
57
|
+
this.#options = options
|
|
58
|
+
this.#urgent = urgent
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get interval () {
|
|
62
|
+
return this.#options.interval ?? INTERVAL_MS
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* How long an answer is to hold before it is acted upon. A TTL bounds how long a cache may
|
|
67
|
+
* answer with what is no longer true, and says nothing about records that are being updated
|
|
68
|
+
* one by one, hence the floor.
|
|
69
|
+
*/
|
|
70
|
+
get settle () {
|
|
71
|
+
return this.#options.settle ?? this.#period()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** How long a broker no name points at is to stay idle before it is left. */
|
|
75
|
+
get linger () {
|
|
76
|
+
return this.#options.linger ?? this.#period()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolves every name, as many times as it takes: a name that does not resolve is no more a
|
|
81
|
+
* reason to give up than a broker that is not up yet.
|
|
82
|
+
*
|
|
83
|
+
* @return {Promise<Map<string, string>>} the address of each name
|
|
84
|
+
*/
|
|
85
|
+
async start () {
|
|
86
|
+
this.#stopped = false
|
|
87
|
+
|
|
88
|
+
await retry(async (again) => {
|
|
89
|
+
if (this.#stopped) return
|
|
90
|
+
|
|
91
|
+
const [observed, failed] = await this.#observe()
|
|
92
|
+
|
|
93
|
+
if (failed) return again
|
|
94
|
+
|
|
95
|
+
this.#current = observed
|
|
96
|
+
this.#candidate = observed
|
|
97
|
+
this.#since = Date.now()
|
|
98
|
+
}, global.COMQ_TESTING_RESOLVE_BACKOFF)
|
|
99
|
+
|
|
100
|
+
this.#schedule()
|
|
101
|
+
|
|
102
|
+
return new Map(this.#current)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stop () {
|
|
106
|
+
this.#stopped = true
|
|
107
|
+
|
|
108
|
+
clearTimeout(this.#timer)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Looks now rather than when the interval is up.
|
|
113
|
+
*/
|
|
114
|
+
poke () {
|
|
115
|
+
if (this.#stopped) return
|
|
116
|
+
|
|
117
|
+
if (this.#polling) {
|
|
118
|
+
this.#poked = true
|
|
119
|
+
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
clearTimeout(this.#timer)
|
|
124
|
+
|
|
125
|
+
this.#poll().catch(noop)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {'change' | 'error'} event
|
|
130
|
+
* @param {Function} listener
|
|
131
|
+
*/
|
|
132
|
+
on (event, listener) {
|
|
133
|
+
this.#events.on(event, listener)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#schedule () {
|
|
137
|
+
if (this.#stopped || this.#hosts.length === 0) return
|
|
138
|
+
|
|
139
|
+
this.#timer = setTimeout(() => this.#poll().catch(noop), this.interval)
|
|
140
|
+
this.#timer.unref()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async #poll () {
|
|
144
|
+
this.#polling = true
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const [observed] = await this.#observe()
|
|
148
|
+
|
|
149
|
+
if (!this.#stopped) this.#consider(observed)
|
|
150
|
+
} finally {
|
|
151
|
+
this.#polling = false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this.#poked) {
|
|
155
|
+
this.#poked = false
|
|
156
|
+
|
|
157
|
+
return await this.#poll()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.#schedule()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {Map<string, string>} observed
|
|
165
|
+
*/
|
|
166
|
+
#consider (observed) {
|
|
167
|
+
const now = Date.now()
|
|
168
|
+
|
|
169
|
+
if (!same(observed, this.#candidate)) {
|
|
170
|
+
this.#candidate = observed
|
|
171
|
+
this.#since = now
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const settled = now - this.#since >= this.settle
|
|
175
|
+
|
|
176
|
+
let changed = false
|
|
177
|
+
|
|
178
|
+
for (const [host, address] of this.#candidate) {
|
|
179
|
+
if (address === this.#current.get(host)) continue
|
|
180
|
+
if (!settled && !this.#urgent(host)) continue
|
|
181
|
+
|
|
182
|
+
this.#current.set(host, address)
|
|
183
|
+
|
|
184
|
+
changed = true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (changed) this.#events.emit('change', new Map(this.#current))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A name that fails to resolve keeps the address it was observed with: a resolver that is
|
|
192
|
+
* briefly out of reach has not said that the name is gone.
|
|
193
|
+
*
|
|
194
|
+
* @return {Promise<[Map<string, string>, boolean]>} the address of each name, and whether any
|
|
195
|
+
* of them is not known
|
|
196
|
+
*/
|
|
197
|
+
async #observe () {
|
|
198
|
+
const observed = new Map()
|
|
199
|
+
|
|
200
|
+
let failed = false
|
|
201
|
+
|
|
202
|
+
await Promise.all(this.#hosts.map(async (host) => {
|
|
203
|
+
try {
|
|
204
|
+
const { addresses, ttl } = await resolve(host)
|
|
205
|
+
|
|
206
|
+
if (addresses.length === 0) throw new Error(`${host} resolves to no address`)
|
|
207
|
+
|
|
208
|
+
if (ttl === undefined) this.#untimed = true
|
|
209
|
+
else this.#ttl = Math.max(this.#ttl, ttl)
|
|
210
|
+
|
|
211
|
+
observed.set(host, choose(addresses, this.#current.get(host)))
|
|
212
|
+
} catch (exception) {
|
|
213
|
+
this.#events.emit('error', exception)
|
|
214
|
+
|
|
215
|
+
if (this.#candidate.has(host)) observed.set(host, this.#candidate.get(host))
|
|
216
|
+
else failed = true
|
|
217
|
+
}
|
|
218
|
+
}))
|
|
219
|
+
|
|
220
|
+
return [observed, failed]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* A cache answers with the time its record has left, so the longest ever seen is the closest
|
|
225
|
+
* to what the record was published with.
|
|
226
|
+
*
|
|
227
|
+
* @return {number}
|
|
228
|
+
*/
|
|
229
|
+
#period () {
|
|
230
|
+
return Math.max(FLOOR_MS, this.#ttl * 1000, this.#untimed ? UNTIMED_MS : 0)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {Map<string, string>} one
|
|
236
|
+
* @param {Map<string, string>} another
|
|
237
|
+
* @return {boolean}
|
|
238
|
+
*/
|
|
239
|
+
function same (one, another) {
|
|
240
|
+
if (one.size !== another.size) return false
|
|
241
|
+
|
|
242
|
+
for (const [host, address] of one) if (another.get(host) !== address) return false
|
|
243
|
+
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const INTERVAL_MS = 10_000
|
|
248
|
+
|
|
249
|
+
/** No TTL is short enough to act on at once. */
|
|
250
|
+
const FLOOR_MS = 30_000
|
|
251
|
+
|
|
252
|
+
/** What a name answered without a TTL is given. */
|
|
253
|
+
const UNTIMED_MS = 60_000
|
|
254
|
+
|
|
255
|
+
function noop () {}
|
|
256
|
+
|
|
257
|
+
exports.Watch = Watch
|
package/source/singleton.js
CHANGED
|
@@ -19,8 +19,9 @@ class SingletonConnection extends Connection {
|
|
|
19
19
|
*
|
|
20
20
|
* @param {string} url
|
|
21
21
|
* @param {comq.topology.Overrides} [overrides]
|
|
22
|
+
* @param {string} [servername]
|
|
22
23
|
*/
|
|
23
|
-
constructor (url, overrides = {}) {
|
|
24
|
+
constructor (url, overrides = {}, servername = undefined) {
|
|
24
25
|
const instance = instances.get(url)
|
|
25
26
|
|
|
26
27
|
if (instance !== undefined) {
|
|
@@ -31,7 +32,7 @@ class SingletonConnection extends Connection {
|
|
|
31
32
|
return instance
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
super(url, overrides)
|
|
35
|
+
super(url, overrides, servername)
|
|
35
36
|
|
|
36
37
|
this.#url = url
|
|
37
38
|
this.#overrides = overrides
|
package/test/amqplib.mock.js
CHANGED
|
@@ -13,6 +13,7 @@ class Channel extends EventEmitter {
|
|
|
13
13
|
nack = jest.fn(() => undefined)
|
|
14
14
|
assertQueue = jest.fn(async (name) => ({ queue: name ?? generate() }))
|
|
15
15
|
assertExchange = jest.fn(async () => undefined)
|
|
16
|
+
checkQueue = jest.fn(async (name) => ({ queue: name, messageCount: 0 }))
|
|
16
17
|
bindQueue = jest.fn(async () => undefined)
|
|
17
18
|
unbindQueue = jest.fn(async () => undefined)
|
|
18
19
|
publish = jest.fn((_0, _1, _2, _3, resolve) => resolve?.(null))
|
package/test/assert.test.js
CHANGED
|
@@ -50,9 +50,14 @@ it('should create sharded connection', async () => {
|
|
|
50
50
|
|
|
51
51
|
io = await assert(...urls)
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
expect(shards.Connection).toHaveBeenCalledWith(urls, expect.any(Function), undefined)
|
|
54
|
+
|
|
55
|
+
// a connection to a broker is shared, as one to a single URL is
|
|
56
|
+
const make = shards.Connection.mock.calls[0][1]
|
|
57
|
+
|
|
58
|
+
for (const url of urls) make(url)
|
|
59
|
+
for (const url of urls) expect(SingletonConnection).toHaveBeenCalledWith(url, {}, undefined)
|
|
54
60
|
|
|
55
|
-
expect(shards.Connection).toHaveBeenCalledWith(SingletonConnection.mock.instances)
|
|
56
61
|
expect(IO).toHaveBeenCalledWith(shards.Connection.mock.instances[0])
|
|
57
62
|
expect(io).toStrictEqual(IO.mock.instances[0])
|
|
58
63
|
})
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { randomBytes } = require('node:crypto')
|
|
4
|
+
const { generate } = require('randomstring')
|
|
5
|
+
const { Promex } = require('../source/promex')
|
|
6
|
+
const { immediate } = require('./helpers')
|
|
7
|
+
|
|
8
|
+
const { amqplib } = require('./amqplib.mock')
|
|
9
|
+
const { create } = require('../source/channel')
|
|
10
|
+
|
|
11
|
+
/** @type {jest.MockedObject<comq.amqp.Connection>} */
|
|
12
|
+
let connection
|
|
13
|
+
|
|
14
|
+
/** @type {comq.Channel} */
|
|
15
|
+
let channel
|
|
16
|
+
|
|
17
|
+
/** @type {jest.MockedObject<comq.amqp.Channel>} */
|
|
18
|
+
let chan
|
|
19
|
+
|
|
20
|
+
/** @type {jest.MockedObject<comq.amqp.Channel>[]} */
|
|
21
|
+
let probes
|
|
22
|
+
|
|
23
|
+
const queue = generate()
|
|
24
|
+
const message = () => ({ content: randomBytes(8), properties: {}, fields: {} })
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {Partial<comq.Topology>} [topology]
|
|
28
|
+
*/
|
|
29
|
+
async function open (topology = {}) {
|
|
30
|
+
connection = await amqplib.connect()
|
|
31
|
+
|
|
32
|
+
channel = await create(connection, {
|
|
33
|
+
prefetch: 1,
|
|
34
|
+
confirms: true,
|
|
35
|
+
durable: true,
|
|
36
|
+
acknowledgments: true,
|
|
37
|
+
persistent: true,
|
|
38
|
+
delay: [1000],
|
|
39
|
+
...topology
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
chan = await connection.createConfirmChannel.mock.results[0].value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const deliver = (message) => chan.consume.mock.calls.at(-1)[1](message)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A probe is a channel of its own, the one made last.
|
|
49
|
+
*
|
|
50
|
+
* @param {number} messageCount
|
|
51
|
+
*/
|
|
52
|
+
function depth (messageCount) {
|
|
53
|
+
connection.createChannel.mockImplementation(async () => {
|
|
54
|
+
const probe = await connection.createConfirmChannel.getMockImplementation()()
|
|
55
|
+
|
|
56
|
+
probe.checkQueue.mockImplementation(async (name) => ({ queue: name, messageCount }))
|
|
57
|
+
probes.push(probe)
|
|
58
|
+
|
|
59
|
+
return probe
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const REAL = ['nextTick', 'setImmediate', 'clearImmediate', 'setTimeout', 'clearTimeout',
|
|
64
|
+
'setInterval', 'clearInterval', 'queueMicrotask']
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
jest.clearAllMocks()
|
|
68
|
+
|
|
69
|
+
probes = []
|
|
70
|
+
|
|
71
|
+
// the clock alone: what is awaited here goes through timers of its own
|
|
72
|
+
jest.useFakeTimers({ now: 1_000_000, doNotFake: REAL })
|
|
73
|
+
|
|
74
|
+
await open()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
jest.useRealTimers()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('quiet', () => {
|
|
82
|
+
it('should have had nothing to do ever, when nothing happened', async () => {
|
|
83
|
+
await expect(channel.quiet()).resolves.toStrictEqual(0)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should have something to do while a delivery is with its consumer', async () => {
|
|
87
|
+
const consuming = new Promex()
|
|
88
|
+
|
|
89
|
+
await channel.consume(queue, () => consuming)
|
|
90
|
+
|
|
91
|
+
const delivering = deliver(message())
|
|
92
|
+
|
|
93
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
94
|
+
|
|
95
|
+
consuming.resolve()
|
|
96
|
+
|
|
97
|
+
await delivering
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should tell when the last delivery was done with', async () => {
|
|
101
|
+
await channel.consume(queue, async () => undefined)
|
|
102
|
+
await deliver(message())
|
|
103
|
+
|
|
104
|
+
const done = Date.now()
|
|
105
|
+
|
|
106
|
+
jest.advanceTimersByTime(5000)
|
|
107
|
+
|
|
108
|
+
await expect(channel.quiet()).resolves.toStrictEqual(done)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should count a delivery nothing is acknowledged for', async () => {
|
|
112
|
+
const consuming = new Promex()
|
|
113
|
+
|
|
114
|
+
await open({ acknowledgments: false })
|
|
115
|
+
await channel.consume(queue, () => consuming)
|
|
116
|
+
|
|
117
|
+
deliver(message())
|
|
118
|
+
|
|
119
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
120
|
+
|
|
121
|
+
consuming.resolve()
|
|
122
|
+
|
|
123
|
+
await immediate()
|
|
124
|
+
|
|
125
|
+
jest.advanceTimersByTime(5000)
|
|
126
|
+
|
|
127
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now() - 5000)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should hand back what a consumer nothing is acknowledged for returns', async () => {
|
|
131
|
+
await open({ acknowledgments: false })
|
|
132
|
+
|
|
133
|
+
const consumer = jest.fn(() => undefined)
|
|
134
|
+
|
|
135
|
+
await channel.consume(queue, consumer)
|
|
136
|
+
|
|
137
|
+
const one = message()
|
|
138
|
+
|
|
139
|
+
expect(deliver(one)).toBeUndefined()
|
|
140
|
+
expect(consumer).toHaveBeenCalledWith(one)
|
|
141
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should have something to do while a publication awaits its confirmation', async () => {
|
|
145
|
+
let confirm
|
|
146
|
+
|
|
147
|
+
chan.publish.mockImplementation((_0, _1, _2, _3, callback) => { confirm = callback })
|
|
148
|
+
|
|
149
|
+
const sending = channel.send(queue, randomBytes(8))
|
|
150
|
+
|
|
151
|
+
await immediate()
|
|
152
|
+
|
|
153
|
+
jest.advanceTimersByTime(5000)
|
|
154
|
+
|
|
155
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
156
|
+
|
|
157
|
+
confirm(null)
|
|
158
|
+
|
|
159
|
+
await sending
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should tell when a message was last published', async () => {
|
|
163
|
+
await channel.send(queue, randomBytes(8))
|
|
164
|
+
|
|
165
|
+
const published = Date.now()
|
|
166
|
+
|
|
167
|
+
jest.advanceTimersByTime(5000)
|
|
168
|
+
|
|
169
|
+
await expect(channel.quiet()).resolves.toStrictEqual(published)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should have something to do until a failed message is delivered again', async () => {
|
|
173
|
+
await channel.consume(queue, async () => { throw new Error('oops') })
|
|
174
|
+
await deliver(message())
|
|
175
|
+
|
|
176
|
+
const back = Date.now() + 1000
|
|
177
|
+
|
|
178
|
+
jest.advanceTimersByTime(999)
|
|
179
|
+
|
|
180
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
181
|
+
|
|
182
|
+
jest.advanceTimersByTime(2)
|
|
183
|
+
|
|
184
|
+
await expect(channel.quiet()).resolves.toStrictEqual(back)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should have something to do while a queue it consumes holds messages', async () => {
|
|
188
|
+
await channel.consume(queue, async () => undefined)
|
|
189
|
+
|
|
190
|
+
depth(3)
|
|
191
|
+
|
|
192
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
193
|
+
|
|
194
|
+
expect(probes[0].checkQueue).toHaveBeenCalledWith(queue)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should not ask of queues when it consumes none', async () => {
|
|
198
|
+
await channel.quiet()
|
|
199
|
+
|
|
200
|
+
expect(connection.createChannel).not.toHaveBeenCalled()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should take a queue that is gone for an empty one', async () => {
|
|
204
|
+
await channel.consume(queue, async () => undefined)
|
|
205
|
+
|
|
206
|
+
connection.createChannel.mockImplementation(async () => {
|
|
207
|
+
const probe = await connection.createConfirmChannel.getMockImplementation()()
|
|
208
|
+
|
|
209
|
+
probe.checkQueue.mockImplementation(async () => {
|
|
210
|
+
probe.emit('error', Object.assign(new Error('NOT_FOUND'), { code: 404 }))
|
|
211
|
+
|
|
212
|
+
throw new Error('Channel closed by server')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return probe
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await expect(channel.quiet()).resolves.toStrictEqual(0)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should have something to do when it cannot be told', async () => {
|
|
222
|
+
await channel.consume(queue, async () => undefined)
|
|
223
|
+
|
|
224
|
+
connection.createChannel.mockImplementation(async () => { throw new Error('Connection closed') })
|
|
225
|
+
|
|
226
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should ask of what is consumed again once the connection is restored', async () => {
|
|
230
|
+
await channel.consume(queue, async () => undefined)
|
|
231
|
+
|
|
232
|
+
const restored = await amqplib.connect()
|
|
233
|
+
|
|
234
|
+
await channel.recover(restored)
|
|
235
|
+
|
|
236
|
+
connection = restored
|
|
237
|
+
depth(3)
|
|
238
|
+
|
|
239
|
+
await expect(channel.quiet()).resolves.toStrictEqual(Date.now())
|
|
240
|
+
expect(probes[0].checkQueue).toHaveBeenCalledWith(queue)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('settled', () => {
|
|
245
|
+
it('should resolve when there is nothing to wait for', async () => {
|
|
246
|
+
await expect(channel.settled()).resolves.toBeUndefined()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should wait for the deliveries that are with their consumers', async () => {
|
|
250
|
+
const consuming = [new Promex(), new Promex()]
|
|
251
|
+
|
|
252
|
+
let n = 0
|
|
253
|
+
let settled = false
|
|
254
|
+
|
|
255
|
+
await channel.consume(queue, () => consuming[n++])
|
|
256
|
+
|
|
257
|
+
const delivering = [deliver(message()), deliver(message())]
|
|
258
|
+
|
|
259
|
+
channel.settled().then(() => { settled = true })
|
|
260
|
+
|
|
261
|
+
await immediate()
|
|
262
|
+
|
|
263
|
+
consuming[0].resolve()
|
|
264
|
+
|
|
265
|
+
await delivering[0]
|
|
266
|
+
await immediate()
|
|
267
|
+
|
|
268
|
+
expect(settled).toStrictEqual(false)
|
|
269
|
+
|
|
270
|
+
consuming[1].resolve()
|
|
271
|
+
|
|
272
|
+
await delivering[1]
|
|
273
|
+
await immediate()
|
|
274
|
+
|
|
275
|
+
expect(settled).toStrictEqual(true)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should wait for the publications that await their confirmation', async () => {
|
|
279
|
+
let confirm
|
|
280
|
+
let settled = false
|
|
281
|
+
|
|
282
|
+
chan.publish.mockImplementation((_0, _1, _2, _3, callback) => { confirm = callback })
|
|
283
|
+
|
|
284
|
+
const sending = channel.send(queue, randomBytes(8))
|
|
285
|
+
|
|
286
|
+
await immediate()
|
|
287
|
+
|
|
288
|
+
channel.settled().then(() => { settled = true })
|
|
289
|
+
|
|
290
|
+
await immediate()
|
|
291
|
+
|
|
292
|
+
expect(settled).toStrictEqual(false)
|
|
293
|
+
|
|
294
|
+
confirm(null)
|
|
295
|
+
|
|
296
|
+
await sending
|
|
297
|
+
await immediate()
|
|
298
|
+
|
|
299
|
+
expect(settled).toStrictEqual(true)
|
|
300
|
+
})
|
|
301
|
+
})
|
package/test/connect.test.js
CHANGED
|
@@ -61,12 +61,52 @@ describe('sharded connection', () => {
|
|
|
61
61
|
expect(shards.Connection).toHaveBeenCalled()
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it('should pass
|
|
65
|
-
|
|
64
|
+
it('should pass the urls, and what makes a connection to a broker', async () => {
|
|
65
|
+
expect(shards.Connection).toHaveBeenCalledWith(urls, expect.any(Function), undefined)
|
|
66
66
|
|
|
67
|
-
const
|
|
67
|
+
const make = shards.Connection.mock.calls[0][1]
|
|
68
|
+
const servername = generate()
|
|
69
|
+
const instance = make(urls[0], servername)
|
|
68
70
|
|
|
69
|
-
expect(
|
|
71
|
+
expect(Connection).toHaveBeenCalledWith(urls[0], {}, servername)
|
|
72
|
+
expect(instance).toStrictEqual(Connection.mock.instances[0])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should not connect by itself', async () => {
|
|
76
|
+
expect(Connection).not.toHaveBeenCalled()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('ranges', () => {
|
|
81
|
+
it('should expand a range into the urls of a sharded connection', async () => {
|
|
82
|
+
await connect('amqp://rmq[0..3].example.com')
|
|
83
|
+
|
|
84
|
+
const expanded = ['amqp://rmq0.example.com', 'amqp://rmq1.example.com', 'amqp://rmq2.example.com']
|
|
85
|
+
|
|
86
|
+
expect(shards.Connection).toHaveBeenCalledWith(expanded, expect.any(Function), undefined)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should connect to a range of one as to a single url', async () => {
|
|
90
|
+
await connect('amqp://rmq[0..1].example.com')
|
|
91
|
+
|
|
92
|
+
expect(Connection).toHaveBeenCalledWith('amqp://rmq0.example.com', {})
|
|
93
|
+
expect(shards.Connection).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('resolution', () => {
|
|
98
|
+
it('should pass how the names are followed, apart from the overrides', async () => {
|
|
99
|
+
const urls = [generate(), generate()]
|
|
100
|
+
const resolution = { interval: 1, settle: 2, linger: 3 }
|
|
101
|
+
const overrides = { event: { delay: 100 } }
|
|
102
|
+
|
|
103
|
+
await connect(...urls, { ...overrides, resolution })
|
|
104
|
+
|
|
105
|
+
expect(shards.Connection).toHaveBeenCalledWith(urls, expect.any(Function), resolution)
|
|
106
|
+
|
|
107
|
+
shards.Connection.mock.calls[0][1](urls[0])
|
|
108
|
+
|
|
109
|
+
expect(Connection).toHaveBeenCalledWith(urls[0], overrides, undefined)
|
|
70
110
|
})
|
|
71
111
|
})
|
|
72
112
|
|
|
@@ -85,8 +125,10 @@ describe('topology overrides', () => {
|
|
|
85
125
|
|
|
86
126
|
await connect(...urls, overrides)
|
|
87
127
|
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
const make = shards.Connection.mock.calls[0][1]
|
|
129
|
+
|
|
130
|
+
urls.forEach((url) => make(url))
|
|
131
|
+
urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, overrides, undefined))
|
|
90
132
|
})
|
|
91
133
|
|
|
92
134
|
it('should not take a url as overrides', async () => {
|
|
@@ -94,7 +136,6 @@ describe('topology overrides', () => {
|
|
|
94
136
|
|
|
95
137
|
await connect(...urls)
|
|
96
138
|
|
|
97
|
-
expect(Connection).
|
|
98
|
-
urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, {}))
|
|
139
|
+
expect(shards.Connection).toHaveBeenCalledWith(urls, expect.any(Function), undefined)
|
|
99
140
|
})
|
|
100
141
|
})
|
package/test/connection.mock.js
CHANGED
|
@@ -17,6 +17,9 @@ const channel = /** @type {jest.MockedFunction<(sharded?: boolean, index?: numbe
|
|
|
17
17
|
diagnose: jest.fn(async () => undefined),
|
|
18
18
|
forget: jest.fn(() => undefined),
|
|
19
19
|
seal: jest.fn(async () => undefined),
|
|
20
|
+
settled: jest.fn(async () => undefined),
|
|
21
|
+
quiet: jest.fn(async () => 0),
|
|
22
|
+
occupy: jest.fn(() => undefined),
|
|
20
23
|
close: jest.fn(async () => undefined),
|
|
21
24
|
closed: false,
|
|
22
25
|
recover: jest.fn(async () => undefined)
|