@nxtedition/lib 15.0.28 → 15.0.30
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 = {}) {
|
|
@@ -120,6 +121,18 @@ async function request(urlOrOpts, opts = {}) {
|
|
|
120
121
|
logger: opts.logger,
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
const expectsPayload = opts.method === 'PUT' || opts.method === 'POST' || opts.method === 'PATCH'
|
|
125
|
+
|
|
126
|
+
if (opts.headers['content-length'] === '0' && !expectsPayload) {
|
|
127
|
+
// https://tools.ietf.org/html/rfc7230#section-3.3.2
|
|
128
|
+
// A user agent SHOULD NOT send a Content-Length header field when
|
|
129
|
+
// the request message does not contain a payload body and the method
|
|
130
|
+
// semantics do not anticipate such a body.
|
|
131
|
+
|
|
132
|
+
// undici will error if provided an unexpected content-length: 0 header.
|
|
133
|
+
delete opts.headers['content-length']
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
const dispatcher = opts.dispatcher ?? undici.getGlobalDispatcher()
|
|
124
137
|
|
|
125
138
|
return new Promise((resolve) => {
|
|
@@ -136,6 +149,7 @@ async function request(urlOrOpts, opts = {}) {
|
|
|
136
149
|
dispatch = dispatchers.redirect(dispatch)
|
|
137
150
|
dispatch = dispatchers.signal(dispatch)
|
|
138
151
|
dispatch = dispatchers.proxy(dispatch)
|
|
152
|
+
dispatch = dispatchers.cache(dispatch)
|
|
139
153
|
|
|
140
154
|
dispatch(opts, {
|
|
141
155
|
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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { parseHeaders } = require('../../../http.js')
|
|
2
2
|
const xuid = require('xuid')
|
|
3
|
+
const { performance } = require('perf_hooks')
|
|
3
4
|
|
|
4
5
|
class Handler {
|
|
5
6
|
constructor(opts, { handler }) {
|
|
@@ -9,10 +10,12 @@ class Handler {
|
|
|
9
10
|
this.aborted = false
|
|
10
11
|
this.logger = opts.logger.child({ ureq: { id: opts.id } })
|
|
11
12
|
this.pos = 0
|
|
13
|
+
this.startTime = 0
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
onConnect(abort) {
|
|
15
17
|
this.abort = abort
|
|
18
|
+
this.startTime = performance.now()
|
|
16
19
|
this.logger.debug({ ureq: this.opts }, 'upstream request started')
|
|
17
20
|
this.handler.onConnect?.((reason) => {
|
|
18
21
|
this.aborted = true
|
|
@@ -26,7 +29,10 @@ class Handler {
|
|
|
26
29
|
|
|
27
30
|
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
|
|
28
31
|
this.logger.debug(
|
|
29
|
-
{
|
|
32
|
+
{
|
|
33
|
+
ures: { statusCode, headers: parseHeaders(rawHeaders) },
|
|
34
|
+
elapsedTime: this.startTime - performance.now(),
|
|
35
|
+
},
|
|
30
36
|
'upstream request response',
|
|
31
37
|
)
|
|
32
38
|
return this.handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
|
|
@@ -38,15 +44,24 @@ class Handler {
|
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
onComplete(rawTrailers) {
|
|
41
|
-
this.logger.debug(
|
|
47
|
+
this.logger.debug(
|
|
48
|
+
{ bytesRead: this.pos, elapsedTime: this.startTime - performance.now() },
|
|
49
|
+
'upstream request completed',
|
|
50
|
+
)
|
|
42
51
|
return this.handler.onComplete?.(rawTrailers)
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
onError(err) {
|
|
46
55
|
if (this.aborted) {
|
|
47
|
-
this.logger.debug(
|
|
56
|
+
this.logger.debug(
|
|
57
|
+
{ bytesRead: this.pos, elapsedTime: this.startTime - performance.now(), err },
|
|
58
|
+
'upstream request aborted',
|
|
59
|
+
)
|
|
48
60
|
} else {
|
|
49
|
-
this.logger.error(
|
|
61
|
+
this.logger.error(
|
|
62
|
+
{ bytesRead: this.pos, elapsedTime: this.startTime - performance.now(), err },
|
|
63
|
+
'upstream request failed',
|
|
64
|
+
)
|
|
50
65
|
}
|
|
51
66
|
return this.handler.onError?.(err)
|
|
52
67
|
}
|
|
@@ -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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/lib",
|
|
3
|
-
"version": "15.0.
|
|
3
|
+
"version": "15.0.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Robert Nagy <robert.nagy@boffins.se>",
|
|
6
6
|
"files": [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"rxjs/*",
|
|
10
10
|
"util/*",
|
|
11
11
|
"lib/*",
|
|
12
|
-
"undici
|
|
12
|
+
"undici.js",
|
|
13
13
|
"http-client.js",
|
|
14
14
|
"subtract-ranges.js",
|
|
15
15
|
"serializers.js",
|
package/undici.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./lib/undici/index.js')
|