comq 0.26.0 → 0.28.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/.github/workflows/features.yaml +25 -0
- package/docs/shards.md +72 -0
- package/features/shards.virtual.feature +0 -5
- package/features/steps/call.js +25 -1
- package/features/steps/names.js +0 -10
- package/package.json +1 -1
- package/readme.md +15 -24
- package/source/connect.js +1 -1
- package/source/shards/connection.js +110 -23
- package/source/shards/index.js +0 -2
- package/source/shards/name.js +299 -0
- package/source/shards/resolve.js +12 -13
- package/test/connect.test.js +0 -17
- package/test/shards/connection.test.js +68 -1
- package/test/shards/name.test.js +348 -0
- package/test/shards/resolve.test.js +47 -1
- package/types/connection.d.ts +0 -1
- package/types/shards.d.ts +16 -3
- package/source/shards/expand.js +0 -57
- package/source/shards/watch.js +0 -257
- package/test/shards/expand.test.js +0 -58
- package/test/shards/watch.test.js +0 -264
package/source/shards/watch.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
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
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { expand } = require('../../source/shards/expand')
|
|
4
|
-
|
|
5
|
-
it('should leave a url without a range as it is', async () => {
|
|
6
|
-
expect(expand('amqp://user:secret@rmq.example.com:5672/vhost?heartbeat=5'))
|
|
7
|
-
.toStrictEqual(['amqp://user:secret@rmq.example.com:5672/vhost?heartbeat=5'])
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('should expand a range, its end being exclusive', async () => {
|
|
11
|
-
expect(expand('amqp://rmq[0..3].example.com')).toStrictEqual([
|
|
12
|
-
'amqp://rmq0.example.com',
|
|
13
|
-
'amqp://rmq1.example.com',
|
|
14
|
-
'amqp://rmq2.example.com'
|
|
15
|
-
])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('should name as many hosts as the end of a range that starts at zero', async () => {
|
|
19
|
-
expect(expand('amqp://rmq[0..32].example.com')).toHaveLength(32)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('should keep what surrounds the host', async () => {
|
|
23
|
-
expect(expand('amqps://user:secret@rmq-[1..3].example.com:5671/vhost?heartbeat=5')).toStrictEqual([
|
|
24
|
-
'amqps://user:secret@rmq-1.example.com:5671/vhost?heartbeat=5',
|
|
25
|
-
'amqps://user:secret@rmq-2.example.com:5671/vhost?heartbeat=5'
|
|
26
|
-
])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('should pad to the width the start is written with', async () => {
|
|
30
|
-
expect(expand('amqp://rmq[08..11]')).toStrictEqual(['amqp://rmq08', 'amqp://rmq09', 'amqp://rmq10'])
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('should expand every range', async () => {
|
|
34
|
-
expect(expand('amqp://rmq[0..2].zone[0..2]')).toStrictEqual([
|
|
35
|
-
'amqp://rmq0.zone0',
|
|
36
|
-
'amqp://rmq0.zone1',
|
|
37
|
-
'amqp://rmq1.zone0',
|
|
38
|
-
'amqp://rmq1.zone1'
|
|
39
|
-
])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('should leave an IPv6 literal alone', async () => {
|
|
43
|
-
expect(expand('amqp://[::1]:5672')).toStrictEqual(['amqp://[::1]:5672'])
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('should look at the host only', async () => {
|
|
47
|
-
const url = 'amqp://user:[0..2]@rmq.example.com/[0..2]?name=[0..2]'
|
|
48
|
-
|
|
49
|
-
expect(expand(url)).toStrictEqual([url])
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('should leave what is not a url as it is', async () => {
|
|
53
|
-
expect(expand('whatever')).toStrictEqual(['whatever'])
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it.each(['[3..3]', '[3..1]'])('should refuse a range that names no hosts: %s', async (range) => {
|
|
57
|
-
expect(() => expand(`amqp://rmq${range}`)).toThrow(RangeError)
|
|
58
|
-
})
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Watch } = require('../../source/shards/watch')
|
|
4
|
-
|
|
5
|
-
/** @type {Record<string, comq.shards.Resolution | Error>} */
|
|
6
|
-
let records
|
|
7
|
-
|
|
8
|
-
/** @type {Watch} */
|
|
9
|
-
let watch
|
|
10
|
-
|
|
11
|
-
/** @type {jest.MockedFunction<Function>} */
|
|
12
|
-
let change
|
|
13
|
-
|
|
14
|
-
/** @type {jest.MockedFunction<Function>} */
|
|
15
|
-
let error
|
|
16
|
-
|
|
17
|
-
const INTERVAL = 1000
|
|
18
|
-
const SETTLE = 5000
|
|
19
|
-
const hosts = ['one', 'two']
|
|
20
|
-
|
|
21
|
-
const tick = (ms = INTERVAL) => jest.advanceTimersByTimeAsync(ms)
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {comq.shards.Timing} [timing]
|
|
25
|
-
* @param {(host: string) => boolean} [urgent]
|
|
26
|
-
*/
|
|
27
|
-
async function start (timing = { interval: INTERVAL, settle: SETTLE }, urgent) {
|
|
28
|
-
watch = new Watch(hosts, timing, urgent)
|
|
29
|
-
change = jest.fn()
|
|
30
|
-
error = jest.fn()
|
|
31
|
-
|
|
32
|
-
watch.on('change', change)
|
|
33
|
-
watch.on('error', error)
|
|
34
|
-
|
|
35
|
-
return await watch.start()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
jest.useFakeTimers()
|
|
40
|
-
|
|
41
|
-
records = { one: { addresses: ['10.0.0.1'], ttl: 5 }, two: { addresses: ['10.0.0.2'], ttl: 5 } }
|
|
42
|
-
|
|
43
|
-
global.COMQ_TESTING_LOOKUP = async (host) => {
|
|
44
|
-
if (records[host] instanceof Error) throw records[host]
|
|
45
|
-
|
|
46
|
-
return records[host]
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
watch?.stop()
|
|
52
|
-
jest.useRealTimers()
|
|
53
|
-
|
|
54
|
-
delete global.COMQ_TESTING_LOOKUP
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('should tell the address of each name', async () => {
|
|
58
|
-
const addresses = await start()
|
|
59
|
-
|
|
60
|
-
expect(Object.fromEntries(addresses)).toStrictEqual({ one: '10.0.0.1', two: '10.0.0.2' })
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('should not tell of what has not changed', async () => {
|
|
64
|
-
await start()
|
|
65
|
-
await tick(SETTLE * 2)
|
|
66
|
-
|
|
67
|
-
expect(change).not.toHaveBeenCalled()
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('should tell of a change once it has held for the settle time', async () => {
|
|
71
|
-
await start()
|
|
72
|
-
|
|
73
|
-
records.two = { addresses: ['10.0.0.3'], ttl: 5 }
|
|
74
|
-
|
|
75
|
-
await tick(SETTLE)
|
|
76
|
-
|
|
77
|
-
expect(change).not.toHaveBeenCalled()
|
|
78
|
-
|
|
79
|
-
await tick()
|
|
80
|
-
|
|
81
|
-
expect(change).toHaveBeenCalledTimes(1)
|
|
82
|
-
expect(Object.fromEntries(change.mock.calls[0][0])).toStrictEqual({ one: '10.0.0.1', two: '10.0.0.3' })
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should start the settle time anew whenever the answer changes', async () => {
|
|
86
|
-
await start()
|
|
87
|
-
|
|
88
|
-
records.two = { addresses: ['10.0.0.3'], ttl: 5 }
|
|
89
|
-
|
|
90
|
-
await tick(SETTLE - INTERVAL)
|
|
91
|
-
|
|
92
|
-
records.one = { addresses: ['10.0.0.3'], ttl: 5 }
|
|
93
|
-
|
|
94
|
-
await tick(SETTLE - INTERVAL)
|
|
95
|
-
|
|
96
|
-
expect(change).not.toHaveBeenCalled()
|
|
97
|
-
|
|
98
|
-
await tick(INTERVAL * 2)
|
|
99
|
-
|
|
100
|
-
expect(change).toHaveBeenCalledTimes(1)
|
|
101
|
-
expect(Object.fromEntries(change.mock.calls[0][0])).toStrictEqual({ one: '10.0.0.3', two: '10.0.0.3' })
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('should not act on an answer that goes back and forth', async () => {
|
|
105
|
-
await start()
|
|
106
|
-
|
|
107
|
-
for (let i = 0; i < 20; i++) {
|
|
108
|
-
records.two = { addresses: [i % 2 === 0 ? '10.0.0.3' : '10.0.0.2'], ttl: 5 }
|
|
109
|
-
|
|
110
|
-
await tick()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
expect(change).not.toHaveBeenCalled()
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('should forget a change that was taken back', async () => {
|
|
117
|
-
await start()
|
|
118
|
-
|
|
119
|
-
records.two = { addresses: ['10.0.0.3'], ttl: 5 }
|
|
120
|
-
|
|
121
|
-
await tick(INTERVAL * 2)
|
|
122
|
-
|
|
123
|
-
records.two = { addresses: ['10.0.0.2'], ttl: 5 }
|
|
124
|
-
|
|
125
|
-
await tick(SETTLE * 2)
|
|
126
|
-
|
|
127
|
-
expect(change).not.toHaveBeenCalled()
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('should keep the address of a name that fails to resolve', async () => {
|
|
131
|
-
await start()
|
|
132
|
-
|
|
133
|
-
const exception = new Error('ENOTFOUND')
|
|
134
|
-
|
|
135
|
-
records.two = exception
|
|
136
|
-
|
|
137
|
-
await tick(SETTLE * 2)
|
|
138
|
-
|
|
139
|
-
expect(change).not.toHaveBeenCalled()
|
|
140
|
-
expect(error).toHaveBeenCalledWith(exception)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('should keep the address of a name that resolves to nothing', async () => {
|
|
144
|
-
await start()
|
|
145
|
-
|
|
146
|
-
records.two = { addresses: [] }
|
|
147
|
-
|
|
148
|
-
await tick(SETTLE * 2)
|
|
149
|
-
|
|
150
|
-
expect(change).not.toHaveBeenCalled()
|
|
151
|
-
expect(error).toHaveBeenCalled()
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('should keep the address in use while it is listed', async () => {
|
|
155
|
-
await start()
|
|
156
|
-
|
|
157
|
-
records.two = { addresses: ['10.0.0.1', '10.0.0.2'], ttl: 5 }
|
|
158
|
-
|
|
159
|
-
await tick(SETTLE * 2)
|
|
160
|
-
|
|
161
|
-
expect(change).not.toHaveBeenCalled()
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('should take what an urgent name resolves to at once', async () => {
|
|
165
|
-
await start(undefined, (host) => host === 'two')
|
|
166
|
-
|
|
167
|
-
records.one = { addresses: ['10.0.0.4'], ttl: 5 }
|
|
168
|
-
records.two = { addresses: ['10.0.0.3'], ttl: 5 }
|
|
169
|
-
|
|
170
|
-
await tick()
|
|
171
|
-
|
|
172
|
-
expect(change).toHaveBeenCalledTimes(1)
|
|
173
|
-
expect(Object.fromEntries(change.mock.calls[0][0])).toStrictEqual({ one: '10.0.0.1', two: '10.0.0.3' })
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('should look at once when poked', async () => {
|
|
177
|
-
await start(undefined, () => true)
|
|
178
|
-
|
|
179
|
-
records.two = { addresses: ['10.0.0.3'], ttl: 5 }
|
|
180
|
-
|
|
181
|
-
watch.poke()
|
|
182
|
-
|
|
183
|
-
await tick(0)
|
|
184
|
-
|
|
185
|
-
expect(change).toHaveBeenCalledTimes(1)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('should resolve until every name is known', async () => {
|
|
189
|
-
global.COMQ_TESTING_RESOLVE_BACKOFF = { base: 10, dispersion: 0 }
|
|
190
|
-
|
|
191
|
-
records.two = new Error('ENOTFOUND')
|
|
192
|
-
|
|
193
|
-
let addresses
|
|
194
|
-
|
|
195
|
-
const starting = start().then((result) => { addresses = result })
|
|
196
|
-
|
|
197
|
-
await tick(100)
|
|
198
|
-
|
|
199
|
-
expect(addresses).toBeUndefined()
|
|
200
|
-
|
|
201
|
-
records.two = { addresses: ['10.0.0.2'], ttl: 5 }
|
|
202
|
-
|
|
203
|
-
await tick(100)
|
|
204
|
-
await starting
|
|
205
|
-
|
|
206
|
-
expect(addresses.get('two')).toStrictEqual('10.0.0.2')
|
|
207
|
-
|
|
208
|
-
delete global.COMQ_TESTING_RESOLVE_BACKOFF
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('should stop', async () => {
|
|
212
|
-
await start()
|
|
213
|
-
|
|
214
|
-
records.two = { addresses: ['10.0.0.3'], ttl: 5 }
|
|
215
|
-
watch.stop()
|
|
216
|
-
|
|
217
|
-
await tick(SETTLE * 2)
|
|
218
|
-
|
|
219
|
-
expect(change).not.toHaveBeenCalled()
|
|
220
|
-
expect(jest.getTimerCount()).toStrictEqual(0)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
describe('timing', () => {
|
|
224
|
-
it('should settle and linger for the longest TTL seen', async () => {
|
|
225
|
-
records.one = { addresses: ['10.0.0.1'], ttl: 300 }
|
|
226
|
-
records.two = { addresses: ['10.0.0.2'], ttl: 120 }
|
|
227
|
-
|
|
228
|
-
await start({})
|
|
229
|
-
|
|
230
|
-
records.one = { addresses: ['10.0.0.1'], ttl: 10 }
|
|
231
|
-
|
|
232
|
-
await tick(10_000)
|
|
233
|
-
|
|
234
|
-
expect(watch.settle).toStrictEqual(300_000)
|
|
235
|
-
expect(watch.linger).toStrictEqual(300_000)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('should settle for no less than 30 seconds', async () => {
|
|
239
|
-
await start({})
|
|
240
|
-
|
|
241
|
-
expect(watch.settle).toStrictEqual(30_000)
|
|
242
|
-
expect(watch.linger).toStrictEqual(30_000)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('should give a name answered without a TTL 60 seconds', async () => {
|
|
246
|
-
records.two = { addresses: ['10.0.0.2'] }
|
|
247
|
-
|
|
248
|
-
await start({})
|
|
249
|
-
|
|
250
|
-
expect(watch.settle).toStrictEqual(60_000)
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
it('should resolve every 10 seconds unless told otherwise', async () => {
|
|
254
|
-
await start({})
|
|
255
|
-
|
|
256
|
-
expect(watch.interval).toStrictEqual(10_000)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('should take what it is told', async () => {
|
|
260
|
-
await start({ interval: 1, settle: 2, linger: 3 })
|
|
261
|
-
|
|
262
|
-
expect([watch.interval, watch.settle, watch.linger]).toStrictEqual([1, 2, 3])
|
|
263
|
-
})
|
|
264
|
-
})
|