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.
Files changed (44) hide show
  1. package/docs/headers.md +16 -1
  2. package/features/properties.feature +39 -0
  3. package/features/shards.virtual.feature +89 -0
  4. package/features/steps/context.js +29 -2
  5. package/features/steps/hooks.js +1 -0
  6. package/features/steps/names.js +245 -0
  7. package/features/steps/networks.js +23 -5
  8. package/features/steps/rpc.js +14 -2
  9. package/package.json +1 -1
  10. package/readme.md +102 -4
  11. package/source/channel.js +147 -2
  12. package/source/connect.js +18 -15
  13. package/source/connection.js +8 -5
  14. package/source/events.js +7 -0
  15. package/source/io.js +31 -9
  16. package/source/shards/channel.js +289 -19
  17. package/source/shards/connection.js +404 -18
  18. package/source/shards/expand.js +57 -0
  19. package/source/shards/index.js +2 -0
  20. package/source/shards/resolve.js +134 -0
  21. package/source/shards/watch.js +257 -0
  22. package/source/singleton.js +3 -2
  23. package/test/amqplib.mock.js +1 -0
  24. package/test/assert.test.js +7 -2
  25. package/test/channel.quiet.test.js +301 -0
  26. package/test/connect.test.js +49 -8
  27. package/test/connection.mock.js +3 -0
  28. package/test/connection.test.js +15 -0
  29. package/test/io.call.test.js +1 -1
  30. package/test/io.diagnostics.test.js +1 -1
  31. package/test/io.reply.test.js +12 -1
  32. package/test/io.request.test.js +18 -0
  33. package/test/io.shards.test.js +85 -0
  34. package/test/shards/channel.test.js +411 -0
  35. package/test/shards/connection.test.js +372 -17
  36. package/test/shards/expand.test.js +58 -0
  37. package/test/shards/resolve.test.js +73 -0
  38. package/test/shards/watch.test.js +264 -0
  39. package/test/singleton.test.js +1 -1
  40. package/types/channel.d.ts +23 -0
  41. package/types/connection.d.ts +13 -2
  42. package/types/diagnostic.d.ts +2 -1
  43. package/types/io.d.ts +17 -1
  44. 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
@@ -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
@@ -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))
@@ -50,9 +50,14 @@ it('should create sharded connection', async () => {
50
50
 
51
51
  io = await assert(...urls)
52
52
 
53
- for (const url of urls) expect(SingletonConnection).toHaveBeenCalledWith(url, {})
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
+ })
@@ -61,12 +61,52 @@ describe('sharded connection', () => {
61
61
  expect(shards.Connection).toHaveBeenCalled()
62
62
  })
63
63
 
64
- it('should pass single connection instances', async () => {
65
- urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, {}))
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 instances = Connection.mock.instances
67
+ const make = shards.Connection.mock.calls[0][1]
68
+ const servername = generate()
69
+ const instance = make(urls[0], servername)
68
70
 
69
- expect(shards.Connection).toHaveBeenCalledWith(instances)
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
- urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, overrides))
89
- expect(shards.Connection).toHaveBeenCalled()
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).toHaveBeenCalledTimes(2)
98
- urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, {}))
139
+ expect(shards.Connection).toHaveBeenCalledWith(urls, expect.any(Function), undefined)
99
140
  })
100
141
  })
@@ -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)