@nxtedition/nxt-undici 6.0.1 → 6.0.2
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/lib/cache/sqlite-cache-store.js +32 -26
- package/lib/index.js +0 -1
- package/lib/interceptor/cache.js +20 -9
- package/lib/interceptor/dns.js +90 -18
- package/package.json +3 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
import { assertCacheKey, assertCacheValue } from './util.js'
|
|
2
4
|
|
|
3
5
|
let DatabaseSync
|
|
@@ -13,16 +15,24 @@ const VERSION = 3
|
|
|
13
15
|
const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* @typedef {import('
|
|
18
|
+
* @typedef {import('undici-types/cache-interceptor.d.ts').default.CacheStore} CacheStore
|
|
17
19
|
* @implements {CacheStore}
|
|
18
20
|
*
|
|
19
21
|
* @typedef {{
|
|
20
22
|
* id: Readonly<number>
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* body?: Buffer
|
|
24
|
+
* statusCode: number
|
|
25
|
+
* statusMessage: string
|
|
26
|
+
* headers?: string
|
|
27
|
+
* vary?: string
|
|
28
|
+
* etag?: string
|
|
29
|
+
* cacheControlDirectives?: string
|
|
30
|
+
* cachedAt: number
|
|
31
|
+
* staleAt: number
|
|
32
|
+
* deleteAt: number
|
|
33
|
+
* }} SqliteStoreValue
|
|
25
34
|
*/
|
|
35
|
+
|
|
26
36
|
export class SqliteCacheStore {
|
|
27
37
|
#maxEntrySize = MAX_ENTRY_SIZE
|
|
28
38
|
#maxCount = Infinity
|
|
@@ -68,13 +78,9 @@ export class SqliteCacheStore {
|
|
|
68
78
|
#deleteOldValuesQuery
|
|
69
79
|
|
|
70
80
|
/**
|
|
71
|
-
* @param {import('
|
|
81
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
|
|
72
82
|
*/
|
|
73
83
|
constructor(opts) {
|
|
74
|
-
if (!DatabaseSync) {
|
|
75
|
-
throw new Error('SqliteCacheStore requires node:sqlite')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
84
|
if (opts) {
|
|
79
85
|
if (typeof opts !== 'object') {
|
|
80
86
|
throw new TypeError('SqliteCacheStore options must be an object')
|
|
@@ -224,8 +230,8 @@ export class SqliteCacheStore {
|
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
/**
|
|
227
|
-
* @param {import('
|
|
228
|
-
* @returns {import('
|
|
233
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
234
|
+
* @returns {import('undici-types/cache-interceptor.d.ts').default.GetResult | undefined}
|
|
229
235
|
*/
|
|
230
236
|
get(key) {
|
|
231
237
|
assertCacheKey(key)
|
|
@@ -237,15 +243,15 @@ export class SqliteCacheStore {
|
|
|
237
243
|
}
|
|
238
244
|
|
|
239
245
|
/**
|
|
240
|
-
* @type {import('
|
|
246
|
+
* @type {import('undici-types/cache-interceptor.d.ts').default.GetResult}
|
|
241
247
|
*/
|
|
242
248
|
const result = {
|
|
243
|
-
body: Buffer.from(value.body),
|
|
249
|
+
body: value.body ? Buffer.from(value.body) : null,
|
|
244
250
|
statusCode: value.statusCode,
|
|
245
251
|
statusMessage: value.statusMessage,
|
|
246
252
|
headers: value.headers ? JSON.parse(value.headers) : undefined,
|
|
247
253
|
etag: value.etag ? value.etag : undefined,
|
|
248
|
-
vary: value.vary
|
|
254
|
+
vary: value.vary ? JSON.parse(value.vary) : undefined,
|
|
249
255
|
cacheControlDirectives: value.cacheControlDirectives
|
|
250
256
|
? JSON.parse(value.cacheControlDirectives)
|
|
251
257
|
: undefined,
|
|
@@ -258,14 +264,14 @@ export class SqliteCacheStore {
|
|
|
258
264
|
}
|
|
259
265
|
|
|
260
266
|
/**
|
|
261
|
-
* @param {import('
|
|
262
|
-
* @param {import('
|
|
267
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
268
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheValue & { body: Buffer | Array<Buffer> | null }} value
|
|
263
269
|
*/
|
|
264
270
|
set(key, value) {
|
|
265
|
-
assertCacheKey(
|
|
271
|
+
assertCacheKey(key)
|
|
266
272
|
assertCacheValue(value)
|
|
267
273
|
|
|
268
|
-
const body = value.body
|
|
274
|
+
const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body
|
|
269
275
|
const size = body ? body.byteLength : 0
|
|
270
276
|
|
|
271
277
|
if (size > this.#maxEntrySize) {
|
|
@@ -314,7 +320,7 @@ export class SqliteCacheStore {
|
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
/**
|
|
317
|
-
* @param {import('
|
|
323
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
318
324
|
*/
|
|
319
325
|
delete(key) {
|
|
320
326
|
if (typeof key !== 'object') {
|
|
@@ -358,7 +364,7 @@ export class SqliteCacheStore {
|
|
|
358
364
|
}
|
|
359
365
|
|
|
360
366
|
/**
|
|
361
|
-
* @param {import('
|
|
367
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
362
368
|
* @returns {string}
|
|
363
369
|
*/
|
|
364
370
|
#makeValueUrl(key) {
|
|
@@ -366,9 +372,9 @@ export class SqliteCacheStore {
|
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
/**
|
|
369
|
-
* @param {import('
|
|
375
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
370
376
|
* @param {boolean} [canBeExpired=false]
|
|
371
|
-
* @returns {
|
|
377
|
+
* @returns {SqliteStoreValue | undefined}
|
|
372
378
|
*/
|
|
373
379
|
#findValue(key, canBeExpired = false) {
|
|
374
380
|
const url = this.#makeValueUrl(key)
|
|
@@ -396,10 +402,10 @@ export class SqliteCacheStore {
|
|
|
396
402
|
return undefined
|
|
397
403
|
}
|
|
398
404
|
|
|
399
|
-
|
|
405
|
+
const vary = JSON.parse(value.vary)
|
|
400
406
|
|
|
401
|
-
for (const header in
|
|
402
|
-
if (!headerValueEquals(headers[header],
|
|
407
|
+
for (const header in vary) {
|
|
408
|
+
if (!headerValueEquals(headers[header], vary[header])) {
|
|
403
409
|
matches = false
|
|
404
410
|
break
|
|
405
411
|
}
|
package/lib/index.js
CHANGED
package/lib/interceptor/cache.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SqliteCacheStore } from '../cache/sqlite-cache-store.js'
|
|
|
2
2
|
import { DecoratorHandler, parseCacheControl } from '../utils.js'
|
|
3
3
|
|
|
4
4
|
const DEFAULT_STORE = new SqliteCacheStore({ location: ':memory:' })
|
|
5
|
+
const MAX_ENTRY_SIZE = 128 * 1024
|
|
5
6
|
|
|
6
7
|
class CacheHandler extends DecoratorHandler {
|
|
7
8
|
#value
|
|
@@ -27,19 +28,18 @@ class CacheHandler extends DecoratorHandler {
|
|
|
27
28
|
return super.onHeaders(statusCode, headers, resume)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
if (headers.vary === '*') {
|
|
31
|
+
if (headers.vary === '*' || headers.trailers) {
|
|
31
32
|
// Not cacheble...
|
|
32
33
|
return super.onHeaders(statusCode, headers, resume)
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const cacheControl = parseCacheControl(headers['cache-control'])
|
|
36
36
|
const contentLength = headers['content-length'] ? Number(headers['content-length']) : Infinity
|
|
37
|
-
|
|
38
|
-
if (contentLength) {
|
|
37
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_ENTRY_SIZE) {
|
|
39
38
|
// We don't support caching responses with body...
|
|
40
39
|
return super.onHeaders(statusCode, headers, resume)
|
|
41
40
|
}
|
|
42
41
|
|
|
42
|
+
const cacheControl = parseCacheControl(headers['cache-control'])
|
|
43
43
|
if (
|
|
44
44
|
!cacheControl ||
|
|
45
45
|
!cacheControl.public ||
|
|
@@ -83,7 +83,8 @@ class CacheHandler extends DecoratorHandler {
|
|
|
83
83
|
const cachedAt = Date.now()
|
|
84
84
|
|
|
85
85
|
this.#value = {
|
|
86
|
-
body:
|
|
86
|
+
body: [],
|
|
87
|
+
size: 0,
|
|
87
88
|
deleteAt: cachedAt + ttl * 1e3,
|
|
88
89
|
statusCode,
|
|
89
90
|
statusMessage: '',
|
|
@@ -99,15 +100,25 @@ class CacheHandler extends DecoratorHandler {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
onData(chunk) {
|
|
102
|
-
this.#value
|
|
103
|
+
if (this.#value) {
|
|
104
|
+
this.#value.size += chunk.length
|
|
105
|
+
this.#value.body.push(chunk)
|
|
106
|
+
|
|
107
|
+
if (this.#value.size > MAX_ENTRY_SIZE) {
|
|
108
|
+
this.#value = null
|
|
109
|
+
this.#value.size = 0
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
return super.onData(chunk)
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
onComplete() {
|
|
107
|
-
if (this.#value) {
|
|
116
|
+
onComplete(trailers) {
|
|
117
|
+
if (this.#value && (!trailers || Object.keys(trailers).length === 0)) {
|
|
108
118
|
this.#store.set(this.#opts, this.#value)
|
|
109
119
|
}
|
|
110
|
-
|
|
120
|
+
|
|
121
|
+
super.onComplete(trailers)
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
|
package/lib/interceptor/dns.js
CHANGED
|
@@ -1,9 +1,56 @@
|
|
|
1
1
|
import net from 'node:net'
|
|
2
2
|
import { resolve4 } from 'node:dns/promises'
|
|
3
|
-
import { getFastNow } from '../utils.js'
|
|
3
|
+
import { DecoratorHandler, getFastNow } from '../utils.js'
|
|
4
|
+
|
|
5
|
+
function noop() {}
|
|
6
|
+
|
|
7
|
+
const MAX_TTL = 10e3
|
|
8
|
+
|
|
9
|
+
class Handler extends DecoratorHandler {
|
|
10
|
+
#callback
|
|
11
|
+
|
|
12
|
+
constructor(handler, callback) {
|
|
13
|
+
super(handler)
|
|
14
|
+
this.#callback = callback
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onComplete(trailers) {
|
|
18
|
+
this.#callback(null)
|
|
19
|
+
super.onComplete(trailers)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onError(err) {
|
|
23
|
+
this.#callback(err)
|
|
24
|
+
super.onError(err)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
4
27
|
|
|
5
28
|
export default () => (dispatch) => {
|
|
6
29
|
const cache = new Map()
|
|
30
|
+
const resolvers = new Map()
|
|
31
|
+
|
|
32
|
+
function resolve(hostname) {
|
|
33
|
+
let promise = resolvers.get(hostname)
|
|
34
|
+
if (!promise) {
|
|
35
|
+
promise = resolve4(hostname, { ttl: true })
|
|
36
|
+
.then((records) => {
|
|
37
|
+
const now = getFastNow()
|
|
38
|
+
const prev = cache.get(hostname)
|
|
39
|
+
const next = records.map(({ address, ttl }) => ({
|
|
40
|
+
address,
|
|
41
|
+
expires: now + Math.min(MAX_TTL, 1e3 * ttl),
|
|
42
|
+
stats: prev?.find((x) => x.address === address)?.stats || { pending: 0, errored: 0 },
|
|
43
|
+
}))
|
|
44
|
+
cache.set(hostname, next)
|
|
45
|
+
return next
|
|
46
|
+
})
|
|
47
|
+
.finally(() => {
|
|
48
|
+
resolvers.delete(hostname)
|
|
49
|
+
})
|
|
50
|
+
resolvers.set(hostname, promise)
|
|
51
|
+
}
|
|
52
|
+
return promise
|
|
53
|
+
}
|
|
7
54
|
|
|
8
55
|
return async (opts, handler) => {
|
|
9
56
|
if (!opts || !opts.dns || !opts.origin) {
|
|
@@ -16,33 +63,58 @@ export default () => (dispatch) => {
|
|
|
16
63
|
return dispatch(opts, handler)
|
|
17
64
|
}
|
|
18
65
|
|
|
19
|
-
const now = getFastNow()
|
|
20
66
|
const { host, hostname } = origin
|
|
21
67
|
|
|
22
|
-
const
|
|
68
|
+
const now = getFastNow()
|
|
23
69
|
|
|
24
|
-
let records =
|
|
70
|
+
let records = cache.get(hostname)
|
|
25
71
|
|
|
26
|
-
records
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
31
|
-
cache.set(hostname, promise)
|
|
32
|
-
records = await promise
|
|
33
|
-
cache.set(hostname, records)
|
|
72
|
+
if (records == null || records.every((x) => x.stats.errored || x.expires < now)) {
|
|
73
|
+
records = await resolve(hostname)
|
|
74
|
+
} else if (records.some((x) => x.errored || x.expires + 1e3 > now)) {
|
|
75
|
+
resolve(hostname).catch(noop)
|
|
34
76
|
}
|
|
35
77
|
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
records.sort((a, b) => {
|
|
79
|
+
if (a.stats.errored !== b.stats.errored) {
|
|
80
|
+
return a.stats.errored - b.stats.errored
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (a.stats.pending !== b.stats.pending) {
|
|
84
|
+
return a.stats.pending - b.stats.pending
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return 0
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const record = records.find((x) => x.expires >= now)
|
|
91
|
+
|
|
92
|
+
if (!record) {
|
|
93
|
+
throw Object.assign(new Error(`No available DNS records found for ${hostname}`), {
|
|
38
94
|
code: 'ENOTFOUND',
|
|
39
|
-
hostname: origin.hostname,
|
|
40
95
|
})
|
|
41
96
|
}
|
|
42
97
|
|
|
43
|
-
|
|
44
|
-
origin.hostname = addresses[Math.floor(Math.random() * addresses.length)]
|
|
98
|
+
origin.hostname = record.address
|
|
45
99
|
|
|
46
|
-
|
|
100
|
+
record.stats.pending++
|
|
101
|
+
try {
|
|
102
|
+
return dispatch(
|
|
103
|
+
{ ...opts, origin, headers: { ...opts.headers, host } },
|
|
104
|
+
new Handler(handler, (err) => {
|
|
105
|
+
record.pending--
|
|
106
|
+
if (err.name === 'AbortError') {
|
|
107
|
+
// Do nothing...
|
|
108
|
+
} else if (err.statusCode == null || err.statusCode >= 500) {
|
|
109
|
+
record.stats.errored++
|
|
110
|
+
} else {
|
|
111
|
+
record.stats.errored = 0
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
record.stats.pending--
|
|
117
|
+
throw err
|
|
118
|
+
}
|
|
47
119
|
}
|
|
48
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/nxt-undici",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Robert Nagy <robert.nagy@boffins.se>",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"pinst": "^3.0.0",
|
|
23
23
|
"prettier": "^3.4.1",
|
|
24
24
|
"send": "^1.1.0",
|
|
25
|
-
"tap": "^21.0.1"
|
|
25
|
+
"tap": "^21.0.1",
|
|
26
|
+
"undici-types": "^7.2.3"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
29
|
"prepare": "husky",
|