undici 7.0.0-alpha.3 → 7.0.0-alpha.5

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 (47) hide show
  1. package/README.md +2 -1
  2. package/docs/docs/api/Agent.md +14 -14
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +17 -14
  5. package/docs/docs/api/Client.md +11 -11
  6. package/docs/docs/api/Dispatcher.md +30 -10
  7. package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
  8. package/docs/docs/api/MockAgent.md +3 -3
  9. package/docs/docs/api/MockClient.md +5 -5
  10. package/docs/docs/api/MockPool.md +2 -2
  11. package/docs/docs/api/Pool.md +15 -15
  12. package/docs/docs/api/PoolStats.md +1 -1
  13. package/docs/docs/api/ProxyAgent.md +3 -3
  14. package/docs/docs/api/RetryHandler.md +2 -2
  15. package/docs/docs/api/WebSocket.md +1 -1
  16. package/docs/docs/api/api-lifecycle.md +11 -11
  17. package/docs/docs/best-practices/mocking-request.md +2 -2
  18. package/docs/docs/best-practices/proxy.md +1 -1
  19. package/index.d.ts +1 -1
  20. package/index.js +2 -1
  21. package/lib/api/api-request.js +1 -1
  22. package/lib/cache/memory-cache-store.js +106 -342
  23. package/lib/core/connect.js +5 -0
  24. package/lib/core/request.js +2 -2
  25. package/lib/core/util.js +13 -40
  26. package/lib/dispatcher/client-h2.js +53 -33
  27. package/lib/handler/cache-handler.js +126 -85
  28. package/lib/handler/cache-revalidation-handler.js +45 -13
  29. package/lib/handler/redirect-handler.js +5 -3
  30. package/lib/handler/retry-handler.js +3 -3
  31. package/lib/interceptor/cache.js +213 -92
  32. package/lib/interceptor/dns.js +71 -48
  33. package/lib/util/cache.js +73 -13
  34. package/lib/util/timers.js +19 -1
  35. package/lib/web/cookies/index.js +12 -1
  36. package/lib/web/cookies/parse.js +6 -1
  37. package/lib/web/fetch/body.js +1 -5
  38. package/lib/web/fetch/formdata-parser.js +70 -43
  39. package/lib/web/fetch/headers.js +1 -1
  40. package/lib/web/fetch/index.js +4 -6
  41. package/lib/web/fetch/webidl.js +12 -4
  42. package/package.json +2 -3
  43. package/types/cache-interceptor.d.ts +51 -54
  44. package/types/cookies.d.ts +2 -0
  45. package/types/dispatcher.d.ts +1 -1
  46. package/types/index.d.ts +0 -1
  47. package/types/interceptors.d.ts +0 -1
@@ -75,7 +75,8 @@ class RedirectHandler {
75
75
  this.opts.body &&
76
76
  typeof this.opts.body !== 'string' &&
77
77
  !ArrayBuffer.isView(this.opts.body) &&
78
- util.isIterable(this.opts.body)
78
+ util.isIterable(this.opts.body) &&
79
+ !util.isFormDataLike(this.opts.body)
79
80
  ) {
80
81
  // TODO: Should we allow re-using iterable if !this.opts.idempotent
81
82
  // or through some other flag?
@@ -227,9 +228,10 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
227
228
  }
228
229
  }
229
230
  } else if (headers && typeof headers === 'object') {
230
- for (const key of Object.keys(headers)) {
231
+ const entries = typeof headers[Symbol.iterator] === 'function' ? headers : Object.entries(headers)
232
+ for (const [key, value] of entries) {
231
233
  if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
232
- ret.push(key, headers[key])
234
+ ret.push(key, value)
233
235
  }
234
236
  }
235
237
  } else {
@@ -229,7 +229,7 @@ class RetryHandler {
229
229
  return false
230
230
  }
231
231
 
232
- const { start, size, end = size } = contentRange
232
+ const { start, size, end = size - 1 } = contentRange
233
233
 
234
234
  assert(this.start === start, 'content-range mismatch')
235
235
  assert(this.end == null || this.end === end, 'content-range mismatch')
@@ -252,7 +252,7 @@ class RetryHandler {
252
252
  )
253
253
  }
254
254
 
255
- const { start, size, end = size } = range
255
+ const { start, size, end = size - 1 } = range
256
256
  assert(
257
257
  start != null && Number.isFinite(start),
258
258
  'content-range mismatch'
@@ -266,7 +266,7 @@ class RetryHandler {
266
266
  // We make our best to checkpoint the body for further range headers
267
267
  if (this.end == null) {
268
268
  const contentLength = headers['content-length']
269
- this.end = contentLength != null ? Number(contentLength) : null
269
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
270
270
  }
271
271
 
272
272
  assert(Number.isFinite(this.start))
@@ -1,13 +1,88 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert')
4
+ const { Readable } = require('node:stream')
3
5
  const util = require('../core/util')
4
6
  const CacheHandler = require('../handler/cache-handler')
5
7
  const MemoryCacheStore = require('../cache/memory-cache-store')
6
8
  const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
7
- const { assertCacheStore, assertCacheMethods } = require('../util/cache.js')
9
+ const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js')
10
+ const { nowAbsolute } = require('../util/timers.js')
8
11
 
9
12
  const AGE_HEADER = Buffer.from('age')
10
13
 
14
+ /**
15
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
16
+ */
17
+ function sendGatewayTimeout (handler) {
18
+ let aborted = false
19
+ try {
20
+ if (typeof handler.onConnect === 'function') {
21
+ handler.onConnect(() => {
22
+ aborted = true
23
+ })
24
+
25
+ if (aborted) {
26
+ return
27
+ }
28
+ }
29
+
30
+ if (typeof handler.onHeaders === 'function') {
31
+ handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
32
+ if (aborted) {
33
+ return
34
+ }
35
+ }
36
+
37
+ if (typeof handler.onComplete === 'function') {
38
+ handler.onComplete([])
39
+ }
40
+ } catch (err) {
41
+ if (typeof handler.onError === 'function') {
42
+ handler.onError(err)
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
49
+ * @param {number} age
50
+ * @param {import('../util/cache.js').CacheControlDirectives | undefined} cacheControlDirectives
51
+ * @returns {boolean}
52
+ */
53
+ function needsRevalidation (result, age, cacheControlDirectives) {
54
+ if (cacheControlDirectives?.['no-cache']) {
55
+ // Always revalidate requests with the no-cache directive
56
+ return true
57
+ }
58
+
59
+ const now = nowAbsolute()
60
+ if (now > result.staleAt) {
61
+ // Response is stale
62
+ if (cacheControlDirectives?.['max-stale']) {
63
+ // There's a threshold where we can serve stale responses, let's see if
64
+ // we're in it
65
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
66
+ const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000)
67
+ return now > gracePeriod
68
+ }
69
+
70
+ return true
71
+ }
72
+
73
+ if (cacheControlDirectives?.['min-fresh']) {
74
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
75
+
76
+ // At this point, staleAt is always > now
77
+ const timeLeftTillStale = result.staleAt - now
78
+ const threshold = cacheControlDirectives['min-fresh'] * 1000
79
+
80
+ return timeLeftTillStale <= threshold
81
+ }
82
+
83
+ return false
84
+ }
85
+
11
86
  /**
12
87
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
13
88
  * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
@@ -34,136 +109,182 @@ module.exports = (opts = {}) => {
34
109
 
35
110
  return dispatch => {
36
111
  return (opts, handler) => {
112
+ // TODO (fix): What if e.g. opts.headers has if-modified-since header? Or other headers
113
+ // that make things ambigious?
114
+
37
115
  if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
38
116
  // Not a method we want to cache or we don't have the origin, skip
39
117
  return dispatch(opts, handler)
40
118
  }
41
119
 
42
- const stream = store.createReadStream(opts)
43
- if (!stream) {
44
- // Request isn't cached
45
- return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
46
- }
120
+ const requestCacheControl = opts.headers?.['cache-control']
121
+ ? parseCacheControlHeader(opts.headers['cache-control'])
122
+ : undefined
47
123
 
48
- let onErrorCalled = false
124
+ if (requestCacheControl?.['no-store']) {
125
+ return dispatch(opts, handler)
126
+ }
49
127
 
50
128
  /**
51
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable} stream
52
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} value
129
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
53
130
  */
54
- const respondWithCachedValue = (stream, value) => {
55
- const ac = new AbortController()
56
- const signal = ac.signal
131
+ const cacheKey = makeCacheKey(opts)
57
132
 
58
- signal.onabort = (_, err) => {
59
- stream.destroy()
60
- if (!onErrorCalled) {
61
- handler.onError(err)
62
- onErrorCalled = true
63
- }
133
+ // TODO (perf): For small entries support returning a Buffer instead of a stream.
134
+ // Maybe store should return { staleAt, headers, body, etc... } instead of a stream + stream.value?
135
+ // Where body can be a Buffer, string, stream or blob?
136
+ const result = store.get(cacheKey)
137
+ if (!result) {
138
+ if (requestCacheControl?.['only-if-cached']) {
139
+ // We only want cached responses
140
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
141
+ sendGatewayTimeout(handler)
142
+ return true
64
143
  }
65
144
 
66
- stream.on('error', (err) => {
67
- if (!onErrorCalled) {
68
- handler.onError(err)
69
- onErrorCalled = true
70
- }
71
- })
72
-
73
- try {
74
- if (typeof handler.onConnect === 'function') {
75
- handler.onConnect(ac.abort)
76
- signal.throwIfAborted()
77
- }
78
-
79
- if (typeof handler.onHeaders === 'function') {
80
- // Add the age header
81
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
82
- const age = Math.round((Date.now() - value.cachedAt) / 1000)
145
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
146
+ }
83
147
 
84
- value.rawHeaders.push(AGE_HEADER, Buffer.from(`${age}`))
148
+ /**
149
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
150
+ * @param {number} age
151
+ */
152
+ const respondWithCachedValue = ({ rawHeaders, statusCode, statusMessage, body }, age) => {
153
+ const stream = util.isStream(body)
154
+ ? body
155
+ : Readable.from(body ?? [])
85
156
 
86
- handler.onHeaders(value.statusCode, value.rawHeaders, stream.resume, value.statusMessage)
87
- signal.throwIfAborted()
88
- }
157
+ assert(!stream.destroyed, 'stream should not be destroyed')
158
+ assert(!stream.readableDidRead, 'stream should not be readableDidRead')
89
159
 
90
- if (opts.method === 'HEAD') {
91
- if (typeof handler.onComplete === 'function') {
92
- handler.onComplete(null)
93
- stream.destroy()
160
+ stream
161
+ .on('error', function (err) {
162
+ if (!this.readableEnded) {
163
+ if (typeof handler.onError === 'function') {
164
+ handler.onError(err)
165
+ } else {
166
+ throw err
167
+ }
94
168
  }
95
- } else {
96
- if (typeof handler.onData === 'function') {
97
- stream.on('data', chunk => {
98
- if (!handler.onData(chunk)) {
99
- stream.pause()
100
- }
101
- })
169
+ })
170
+ .on('close', function () {
171
+ if (!this.errored && typeof handler.onComplete === 'function') {
172
+ handler.onComplete([])
102
173
  }
174
+ })
103
175
 
104
- if (typeof handler.onComplete === 'function') {
105
- stream.on('end', () => {
106
- handler.onComplete(value.rawTrailers ?? [])
107
- })
108
- }
176
+ if (typeof handler.onConnect === 'function') {
177
+ handler.onConnect((err) => {
178
+ stream.destroy(err)
179
+ })
180
+
181
+ if (stream.destroyed) {
182
+ return
109
183
  }
110
- } catch (err) {
111
- stream.destroy(err)
112
- if (!onErrorCalled && typeof handler.onError === 'function') {
113
- handler.onError(err)
114
- onErrorCalled = true
184
+ }
185
+
186
+ if (typeof handler.onHeaders === 'function') {
187
+ // Add the age header
188
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
189
+
190
+ // TODO (fix): What if rawHeaders already contains age header?
191
+ rawHeaders = [...rawHeaders, AGE_HEADER, Buffer.from(`${age}`)]
192
+
193
+ if (handler.onHeaders(statusCode, rawHeaders, () => stream?.resume(), statusMessage) === false) {
194
+ stream.pause()
115
195
  }
116
196
  }
197
+
198
+ if (opts.method === 'HEAD') {
199
+ stream.destroy()
200
+ } else {
201
+ stream.on('data', function (chunk) {
202
+ if (typeof handler.onData === 'function' && !handler.onData(chunk)) {
203
+ stream.pause()
204
+ }
205
+ })
206
+ }
117
207
  }
118
208
 
119
209
  /**
120
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable | undefined} stream
210
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
121
211
  */
122
- const handleStream = (stream) => {
123
- if (!stream) {
124
- // Request isn't cached
125
- return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
126
- }
212
+ const handleResult = (result) => {
213
+ // TODO (perf): Readable.from path can be optimized...
127
214
 
128
- const { value } = stream
215
+ if (!result.body && opts.method !== 'HEAD') {
216
+ throw new Error('stream is undefined but method isn\'t HEAD')
217
+ }
129
218
 
130
- // Dump body on error
131
- if (util.isStream(opts.body)) {
132
- opts.body?.on('error', () => {}).resume()
219
+ const age = Math.round((nowAbsolute() - result.cachedAt) / 1000)
220
+ if (requestCacheControl?.['max-age'] && age >= requestCacheControl['max-age']) {
221
+ // Response is considered expired for this specific request
222
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
223
+ return dispatch(opts, handler)
133
224
  }
134
225
 
135
226
  // Check if the response is stale
136
- const now = Date.now()
137
- if (now >= value.staleAt) {
138
- if (now >= value.deleteAt) {
139
- // Safety check in case the store gave us a response that should've been
140
- // deleted already
141
- dispatch(opts, new CacheHandler(globalOpts, opts, handler))
142
- return
143
- }
144
-
145
- if (!opts.headers) {
146
- opts.headers = {}
227
+ if (needsRevalidation(result, age, requestCacheControl)) {
228
+ if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
229
+ // If body is is stream we can't revalidate...
230
+ // TODO (fix): This could be less strict...
231
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
147
232
  }
148
233
 
149
- opts.headers['if-modified-since'] = new Date(value.cachedAt).toUTCString()
150
-
151
- // Need to revalidate the response
152
- dispatch(
153
- opts,
234
+ // We need to revalidate the response
235
+ return dispatch(
236
+ {
237
+ ...opts,
238
+ headers: {
239
+ ...opts.headers,
240
+ 'if-modified-since': new Date(result.cachedAt).toUTCString(),
241
+ etag: result.etag
242
+ }
243
+ },
154
244
  new CacheRevalidationHandler(
155
- () => respondWithCachedValue(stream, value),
156
- new CacheHandler(globalOpts, opts, handler)
245
+ (success) => {
246
+ if (success) {
247
+ respondWithCachedValue(result, age)
248
+ } else if (util.isStream(result.body)) {
249
+ result.body.on('error', () => {}).destroy()
250
+ }
251
+ },
252
+ new CacheHandler(globalOpts, cacheKey, handler)
157
253
  )
158
254
  )
159
-
160
- return
161
255
  }
162
256
 
163
- respondWithCachedValue(stream, value)
257
+ // Dump request body.
258
+ if (util.isStream(opts.body)) {
259
+ opts.body.on('error', () => {}).destroy()
260
+ }
261
+ respondWithCachedValue(result, age)
164
262
  }
165
263
 
166
- Promise.resolve(stream).then(handleStream).catch(handler.onError)
264
+ if (typeof result.then === 'function') {
265
+ result.then((result) => {
266
+ if (!result) {
267
+ if (requestCacheControl?.['only-if-cached']) {
268
+ // We only want cached responses
269
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
270
+ sendGatewayTimeout(handler)
271
+ return true
272
+ }
273
+
274
+ dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
275
+ } else {
276
+ handleResult(result)
277
+ }
278
+ }, err => {
279
+ if (typeof handler.onError === 'function') {
280
+ handler.onError(err)
281
+ } else {
282
+ throw err
283
+ }
284
+ })
285
+ } else {
286
+ handleResult(result)
287
+ }
167
288
 
168
289
  return true
169
290
  }
@@ -13,7 +13,6 @@ class DNSInstance {
13
13
  affinity = null
14
14
  lookup = null
15
15
  pick = null
16
- lastIpFamily = null
17
16
 
18
17
  constructor (opts) {
19
18
  this.#maxTTL = opts.maxTTL
@@ -61,16 +60,23 @@ class DNSInstance {
61
60
  const ip = this.pick(
62
61
  origin,
63
62
  records,
64
- // Only set affinity if dual stack is disabled
65
- // otherwise let it go through normal flow
66
- !newOpts.dualStack && newOpts.affinity
63
+ newOpts.affinity
67
64
  )
68
65
 
66
+ let port
67
+ if (typeof ip.port === 'number') {
68
+ port = `:${ip.port}`
69
+ } else if (origin.port !== '') {
70
+ port = `:${origin.port}`
71
+ } else {
72
+ port = ''
73
+ }
74
+
69
75
  cb(
70
76
  null,
71
77
  `${origin.protocol}//${
72
78
  ip.family === 6 ? `[${ip.address}]` : ip.address
73
- }${origin.port === '' ? '' : `:${origin.port}`}`
79
+ }${port}`
74
80
  )
75
81
  })
76
82
  } else {
@@ -78,9 +84,7 @@ class DNSInstance {
78
84
  const ip = this.pick(
79
85
  origin,
80
86
  ips,
81
- // Only set affinity if dual stack is disabled
82
- // otherwise let it go through normal flow
83
- !newOpts.dualStack && newOpts.affinity
87
+ newOpts.affinity
84
88
  )
85
89
 
86
90
  // If no IPs we lookup - deleting old records
@@ -90,11 +94,20 @@ class DNSInstance {
90
94
  return
91
95
  }
92
96
 
97
+ let port
98
+ if (typeof ip.port === 'number') {
99
+ port = `:${ip.port}`
100
+ } else if (origin.port !== '') {
101
+ port = `:${origin.port}`
102
+ } else {
103
+ port = ''
104
+ }
105
+
93
106
  cb(
94
107
  null,
95
108
  `${origin.protocol}//${
96
109
  ip.family === 6 ? `[${ip.address}]` : ip.address
97
- }${origin.port === '' ? '' : `:${origin.port}`}`
110
+ }${port}`
98
111
  )
99
112
  }
100
113
  }
@@ -102,7 +115,11 @@ class DNSInstance {
102
115
  #defaultLookup (origin, opts, cb) {
103
116
  lookup(
104
117
  origin.hostname,
105
- { all: true, family: this.dualStack === false ? this.affinity : 0 },
118
+ {
119
+ all: true,
120
+ family: this.dualStack === false ? this.affinity : 0,
121
+ order: 'ipv4first'
122
+ },
106
123
  (err, addresses) => {
107
124
  if (err) {
108
125
  return cb(err)
@@ -111,15 +128,9 @@ class DNSInstance {
111
128
  const results = new Map()
112
129
 
113
130
  for (const addr of addresses) {
114
- const record = {
115
- address: addr.address,
116
- ttl: opts.maxTTL,
117
- family: addr.family
118
- }
119
-
120
131
  // On linux we found duplicates, we attempt to remove them with
121
132
  // the latest record
122
- results.set(`${record.address}:${record.family}`, record)
133
+ results.set(`${addr.address}:${addr.family}`, addr)
123
134
  }
124
135
 
125
136
  cb(null, results.values())
@@ -129,36 +140,36 @@ class DNSInstance {
129
140
 
130
141
  #defaultPick (origin, hostnameRecords, affinity) {
131
142
  let ip = null
132
- const { records, offset = 0 } = hostnameRecords
133
- let newOffset = 0
143
+ const { records, offset } = hostnameRecords
144
+
145
+ let family
146
+ if (this.dualStack) {
147
+ if (affinity == null) {
148
+ // Balance between ip families
149
+ if (offset == null || offset === maxInt) {
150
+ hostnameRecords.offset = 0
151
+ affinity = 4
152
+ } else {
153
+ hostnameRecords.offset++
154
+ affinity = (hostnameRecords.offset & 1) === 1 ? 6 : 4
155
+ }
156
+ }
134
157
 
135
- if (offset === maxInt) {
136
- newOffset = 0
158
+ if (records[affinity] != null && records[affinity].ips.length > 0) {
159
+ family = records[affinity]
160
+ } else {
161
+ family = records[affinity === 4 ? 6 : 4]
162
+ }
137
163
  } else {
138
- newOffset = offset + 1
164
+ family = records[affinity]
139
165
  }
140
166
 
141
- // We balance between the two IP families
142
- // If dual-stack disabled, we automatically pick the affinity
143
- const newIpFamily = (newOffset & 1) === 1 ? 4 : 6
144
- const family =
145
- this.dualStack === false
146
- ? records[this.affinity] // If dual-stack is disabled, we pick the default affiniy
147
- : records[affinity] ?? records[newIpFamily]
148
-
149
- // If no IPs and we have tried both families or dual stack is disabled, we return null
150
- if (
151
- (family == null || family.ips.length === 0) &&
152
- // eslint-disable-next-line eqeqeq
153
- (this.dualStack === false || this.lastIpFamily != newIpFamily)
154
- ) {
167
+ // If no IPs we return null
168
+ if (family == null || family.ips.length === 0) {
155
169
  return ip
156
170
  }
157
171
 
158
- family.offset = family.offset ?? 0
159
- hostnameRecords.offset = newOffset
160
-
161
- if (family.offset === maxInt) {
172
+ if (family.offset == null || family.offset === maxInt) {
162
173
  family.offset = 0
163
174
  } else {
164
175
  family.offset++
@@ -171,24 +182,28 @@ class DNSInstance {
171
182
  return ip
172
183
  }
173
184
 
174
- const timestamp = Date.now()
175
- // Record TTL is already in ms
176
- if (ip.timestamp != null && timestamp - ip.timestamp > ip.ttl) {
185
+ if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
177
186
  // We delete expired records
178
187
  // It is possible that they have different TTL, so we manage them individually
179
188
  family.ips.splice(position, 1)
180
189
  return this.pick(origin, hostnameRecords, affinity)
181
190
  }
182
191
 
183
- ip.timestamp = timestamp
184
-
185
- this.lastIpFamily = newIpFamily
186
192
  return ip
187
193
  }
188
194
 
189
195
  setRecords (origin, addresses) {
196
+ const timestamp = Date.now()
190
197
  const records = { records: { 4: null, 6: null } }
191
198
  for (const record of addresses) {
199
+ record.timestamp = timestamp
200
+ if (typeof record.ttl === 'number') {
201
+ // The record TTL is expected to be in ms
202
+ record.ttl = Math.min(record.ttl, this.#maxTTL)
203
+ } else {
204
+ record.ttl = this.#maxTTL
205
+ }
206
+
192
207
  const familyRecords = records.records[record.family] ?? { ips: [] }
193
208
 
194
209
  familyRecords.ips.push(record)
@@ -302,12 +317,20 @@ module.exports = interceptorOpts => {
302
317
  throw new InvalidArgumentError('Invalid pick. Must be a function')
303
318
  }
304
319
 
320
+ const dualStack = interceptorOpts?.dualStack ?? true
321
+ let affinity
322
+ if (dualStack) {
323
+ affinity = interceptorOpts?.affinity ?? null
324
+ } else {
325
+ affinity = interceptorOpts?.affinity ?? 4
326
+ }
327
+
305
328
  const opts = {
306
329
  maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms
307
330
  lookup: interceptorOpts?.lookup ?? null,
308
331
  pick: interceptorOpts?.pick ?? null,
309
- dualStack: interceptorOpts?.dualStack ?? true,
310
- affinity: interceptorOpts?.affinity ?? 4,
332
+ dualStack,
333
+ affinity,
311
334
  maxItems: interceptorOpts?.maxItems ?? Infinity
312
335
  }
313
336