@nxtedition/lib 15.0.28 → 15.0.29
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/undici/index.js
CHANGED
|
@@ -74,6 +74,7 @@ const dispatchers = {
|
|
|
74
74
|
responseRetry: require('./interceptor/response-retry.js'),
|
|
75
75
|
signal: require('./interceptor/signal.js'),
|
|
76
76
|
proxy: require('./interceptor/proxy.js'),
|
|
77
|
+
cache: require('./interceptor/cache.js'),
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
async function request(urlOrOpts, opts = {}) {
|
|
@@ -136,6 +137,7 @@ async function request(urlOrOpts, opts = {}) {
|
|
|
136
137
|
dispatch = dispatchers.redirect(dispatch)
|
|
137
138
|
dispatch = dispatchers.signal(dispatch)
|
|
138
139
|
dispatch = dispatchers.proxy(dispatch)
|
|
140
|
+
dispatch = dispatchers.cache(dispatch)
|
|
139
141
|
|
|
140
142
|
dispatch(opts, {
|
|
141
143
|
resolve,
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { LRUCache } from 'lru-cache'
|
|
3
|
+
import cacheControlParser from 'cache-control-parser'
|
|
4
|
+
|
|
5
|
+
class CacheHandler {
|
|
6
|
+
constructor({ key, handler, store }) {
|
|
7
|
+
this.key = key
|
|
8
|
+
this.handler = handler
|
|
9
|
+
this.store = store
|
|
10
|
+
this.value = null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onConnect(abort) {
|
|
14
|
+
return this.handler.onConnect(abort)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
18
|
+
// NOTE: Only cache 307 respones for now...
|
|
19
|
+
if (statusCode !== 307) {
|
|
20
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let cacheControl
|
|
24
|
+
for (let n = 0; n < rawHeaders.length; n += 2) {
|
|
25
|
+
if (
|
|
26
|
+
rawHeaders[n].length === 'cache-control'.length &&
|
|
27
|
+
rawHeaders[n].toString().toLowerCase() === 'cache-control'
|
|
28
|
+
) {
|
|
29
|
+
cacheControl = cacheControlParser.parse(rawHeaders[n + 1].toString())
|
|
30
|
+
break
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
cacheControl &&
|
|
36
|
+
cacheControl.public &&
|
|
37
|
+
!cacheControl.private &&
|
|
38
|
+
!cacheControl['no-store'] &&
|
|
39
|
+
// TODO (fix): Support all cache control directives...
|
|
40
|
+
// !opts.headers['no-transform'] &&
|
|
41
|
+
!cacheControl['no-cache'] &&
|
|
42
|
+
!cacheControl['must-understand'] &&
|
|
43
|
+
!cacheControl['must-revalidate'] &&
|
|
44
|
+
!cacheControl['proxy-revalidate']
|
|
45
|
+
) {
|
|
46
|
+
const maxAge = cacheControl['s-max-age'] ?? cacheControl['max-age']
|
|
47
|
+
const ttl = cacheControl.immutable
|
|
48
|
+
? 31556952 // 1 year
|
|
49
|
+
: Number(maxAge)
|
|
50
|
+
|
|
51
|
+
if (ttl > 0) {
|
|
52
|
+
this.value = {
|
|
53
|
+
statusCode,
|
|
54
|
+
statusMessage,
|
|
55
|
+
rawHeaders,
|
|
56
|
+
rawTrailers: null,
|
|
57
|
+
body: [],
|
|
58
|
+
size: 0,
|
|
59
|
+
ttl: ttl * 1e3,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onData(chunk) {
|
|
68
|
+
if (this.value) {
|
|
69
|
+
this.value.size += chunk.bodyLength
|
|
70
|
+
if (this.value.size > this.store.maxEntrySize) {
|
|
71
|
+
this.value = null
|
|
72
|
+
} else {
|
|
73
|
+
this.value.body.push(chunk)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return this.handler.onData(chunk)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onComplete(rawTrailers) {
|
|
80
|
+
if (this.value) {
|
|
81
|
+
this.value.rawTrailers = rawTrailers
|
|
82
|
+
this.store.set(this.key, this.value, this.value.ttl)
|
|
83
|
+
}
|
|
84
|
+
return this.handler.onComplete(rawTrailers)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onError(err) {
|
|
88
|
+
return this.handler.onError(err)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// TODO (fix): Async filesystem cache.
|
|
93
|
+
class CacheStore {
|
|
94
|
+
constructor({ maxSize, maxEntrySize }) {
|
|
95
|
+
this.maxSize = maxSize
|
|
96
|
+
this.maxEntrySize = maxEntrySize
|
|
97
|
+
this.cache = new LRUCache({
|
|
98
|
+
maxSize,
|
|
99
|
+
sizeCalculation: (value) => value.body.byteLength,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
set(key, value, ttl) {
|
|
104
|
+
this.cache.set(key, value, ttl)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get(key) {
|
|
108
|
+
return this.cache.get(key)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function makeKey(opts) {
|
|
113
|
+
// NOTE: Ignores headers...
|
|
114
|
+
return `${opts.method}:${opts.path}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const DEFAULT_CACHE_STORE = new CacheStore({ maxSize: 128 * 1024, maxEntrySize: 1024 })
|
|
118
|
+
|
|
119
|
+
module.exports = (dispatch) => (opts, handler) => {
|
|
120
|
+
if (!opts.cache) {
|
|
121
|
+
return dispatch(opts, handler)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (opts.method !== 'GET' && opts.method !== 'HEAD') {
|
|
125
|
+
dispatch(opts, handler)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (opts.headers?.['cache-control'] || opts.headers?.authorization) {
|
|
130
|
+
// TODO (fix): Support all cache control directives...
|
|
131
|
+
// const cacheControl = cacheControlParser.parse(opts.headers['cache-control'])
|
|
132
|
+
// cacheControl['no-cache']
|
|
133
|
+
// cacheControl['no-store']
|
|
134
|
+
// cacheControl['max-age']
|
|
135
|
+
// cacheControl['max-stale']
|
|
136
|
+
// cacheControl['min-fresh']
|
|
137
|
+
// cacheControl['no-transform']
|
|
138
|
+
// cacheControl['only-if-cached']
|
|
139
|
+
dispatch(opts, handler)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// TODO (fix): Support body...
|
|
144
|
+
assert(opts.method === 'GET' || opts.method === 'HEAD')
|
|
145
|
+
// Dump body...
|
|
146
|
+
opts.body.on('error', () => {}).resume()
|
|
147
|
+
|
|
148
|
+
const store = opts.cache === true ? DEFAULT_CACHE_STORE : opts.cache
|
|
149
|
+
|
|
150
|
+
if (!store) {
|
|
151
|
+
throw new Error(`Cache store not provided.`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let key = makeKey(opts)
|
|
155
|
+
let value = store.get(key)
|
|
156
|
+
|
|
157
|
+
if (value == null && opts.method === 'HEAD') {
|
|
158
|
+
key = makeKey({ ...opts, method: 'GET' })
|
|
159
|
+
value = store.get(key)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (value) {
|
|
163
|
+
const { statusCode, statusMessage, rawHeaders, rawTrailers, body } = value
|
|
164
|
+
const ac = new AbortController()
|
|
165
|
+
const signal = ac.signal
|
|
166
|
+
|
|
167
|
+
const resume = () => {}
|
|
168
|
+
const abort = () => {
|
|
169
|
+
ac.abort()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
handler.onConnect(abort)
|
|
174
|
+
signal.throwIfAborted()
|
|
175
|
+
handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
|
|
176
|
+
signal.throwIfAborted()
|
|
177
|
+
if (opts.method !== 'HEAD') {
|
|
178
|
+
for (const chunk of body) {
|
|
179
|
+
const ret = handler.onData(chunk)
|
|
180
|
+
signal.throwIfAborted()
|
|
181
|
+
if (ret === false) {
|
|
182
|
+
// TODO (fix): back pressure...
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handler.onComplete(rawTrailers)
|
|
186
|
+
signal.throwIfAborted()
|
|
187
|
+
} else {
|
|
188
|
+
handler.onComplete([])
|
|
189
|
+
signal.throwIfAborted()
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
handler.onError(err)
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
dispatch(opts, new CacheHandler({ handler, store, key: makeKey(opts) }))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -4,32 +4,11 @@ const net = require('net')
|
|
|
4
4
|
class Handler {
|
|
5
5
|
constructor(opts, { handler }) {
|
|
6
6
|
this.handler = handler
|
|
7
|
-
this.opts =
|
|
8
|
-
...opts,
|
|
9
|
-
headers: reduceHeaders(
|
|
10
|
-
{
|
|
11
|
-
headers: opts.headers ?? {},
|
|
12
|
-
httpVersion: opts.proxy.httpVersion ?? opts.proxy.req?.httpVersion,
|
|
13
|
-
socket: opts.proxy.socket ?? opts.proxy.req?.socket,
|
|
14
|
-
proxyName: opts.proxy.name,
|
|
15
|
-
},
|
|
16
|
-
(obj, key, val) => {
|
|
17
|
-
obj[key] = val
|
|
18
|
-
return obj
|
|
19
|
-
},
|
|
20
|
-
{},
|
|
21
|
-
),
|
|
22
|
-
}
|
|
23
|
-
this.abort = null
|
|
24
|
-
this.aborted = false
|
|
7
|
+
this.opts = opts
|
|
25
8
|
}
|
|
26
9
|
|
|
27
10
|
onConnect(abort) {
|
|
28
|
-
this.abort
|
|
29
|
-
this.handler.onConnect?.((reason) => {
|
|
30
|
-
this.aborted = true
|
|
31
|
-
this.abort(reason)
|
|
32
|
-
})
|
|
11
|
+
return this.handler.onConnect?.(abort)
|
|
33
12
|
}
|
|
34
13
|
|
|
35
14
|
onBodySent(chunk) {
|
|
@@ -70,8 +49,30 @@ class Handler {
|
|
|
70
49
|
}
|
|
71
50
|
}
|
|
72
51
|
|
|
73
|
-
module.exports = (dispatch) => (opts, handler) =>
|
|
74
|
-
opts.proxy
|
|
52
|
+
module.exports = (dispatch) => (opts, handler) => {
|
|
53
|
+
if (!opts.proxy) {
|
|
54
|
+
return dispatch(opts, handler)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
opts = {
|
|
58
|
+
...opts,
|
|
59
|
+
headers: reduceHeaders(
|
|
60
|
+
{
|
|
61
|
+
headers: opts.headers ?? {},
|
|
62
|
+
httpVersion: opts.proxy.httpVersion ?? opts.proxy.req?.httpVersion,
|
|
63
|
+
socket: opts.proxy.socket ?? opts.proxy.req?.socket,
|
|
64
|
+
proxyName: opts.proxy.name,
|
|
65
|
+
},
|
|
66
|
+
(obj, key, val) => {
|
|
67
|
+
obj[key] = val
|
|
68
|
+
return obj
|
|
69
|
+
},
|
|
70
|
+
{},
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return dispatch(opts, new Handler(opts, { handler }))
|
|
75
|
+
}
|
|
75
76
|
|
|
76
77
|
// This expression matches hop-by-hop headers.
|
|
77
78
|
// These headers are meaningful only for a single transport-level connection,
|