undici 7.0.0-alpha.1 → 7.0.0-alpha.10
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/README.md +24 -38
- package/docs/docs/api/Agent.md +14 -14
- package/docs/docs/api/BalancedPool.md +16 -16
- package/docs/docs/api/CacheStore.md +131 -0
- package/docs/docs/api/Client.md +12 -12
- package/docs/docs/api/Debug.md +1 -1
- package/docs/docs/api/Dispatcher.md +98 -193
- package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
- package/docs/docs/api/MockAgent.md +5 -3
- package/docs/docs/api/MockClient.md +5 -5
- package/docs/docs/api/MockPool.md +4 -3
- package/docs/docs/api/Pool.md +15 -15
- package/docs/docs/api/PoolStats.md +1 -1
- package/docs/docs/api/ProxyAgent.md +3 -3
- package/docs/docs/api/RedirectHandler.md +1 -1
- package/docs/docs/api/RetryAgent.md +1 -1
- package/docs/docs/api/RetryHandler.md +4 -4
- package/docs/docs/api/WebSocket.md +46 -4
- package/docs/docs/api/api-lifecycle.md +11 -11
- package/docs/docs/best-practices/mocking-request.md +2 -2
- package/docs/docs/best-practices/proxy.md +1 -1
- package/index.d.ts +1 -1
- package/index.js +23 -3
- package/lib/api/abort-signal.js +2 -0
- package/lib/api/api-pipeline.js +4 -2
- package/lib/api/api-request.js +6 -4
- package/lib/api/api-stream.js +3 -1
- package/lib/api/api-upgrade.js +2 -2
- package/lib/api/readable.js +200 -47
- package/lib/api/util.js +2 -0
- package/lib/cache/memory-cache-store.js +177 -0
- package/lib/cache/sqlite-cache-store.js +446 -0
- package/lib/core/connect.js +54 -22
- package/lib/core/constants.js +35 -10
- package/lib/core/diagnostics.js +122 -128
- package/lib/core/errors.js +2 -2
- package/lib/core/request.js +6 -6
- package/lib/core/symbols.js +2 -0
- package/lib/core/tree.js +4 -2
- package/lib/core/util.js +238 -40
- package/lib/dispatcher/client-h1.js +405 -142
- package/lib/dispatcher/client-h2.js +212 -109
- package/lib/dispatcher/client.js +24 -7
- package/lib/dispatcher/dispatcher-base.js +4 -1
- package/lib/dispatcher/dispatcher.js +4 -0
- package/lib/dispatcher/fixed-queue.js +91 -49
- package/lib/dispatcher/pool-base.js +3 -3
- package/lib/dispatcher/pool-stats.js +2 -0
- package/lib/dispatcher/proxy-agent.js +3 -1
- package/lib/handler/cache-handler.js +393 -0
- package/lib/handler/cache-revalidation-handler.js +124 -0
- package/lib/handler/decorator-handler.js +3 -0
- package/lib/handler/redirect-handler.js +45 -59
- package/lib/handler/retry-handler.js +68 -109
- package/lib/handler/unwrap-handler.js +96 -0
- package/lib/handler/wrap-handler.js +98 -0
- package/lib/interceptor/cache.js +350 -0
- package/lib/interceptor/dns.js +375 -0
- package/lib/interceptor/response-error.js +15 -7
- package/lib/mock/mock-agent.js +5 -8
- package/lib/mock/mock-client.js +7 -2
- package/lib/mock/mock-errors.js +3 -1
- package/lib/mock/mock-interceptor.js +8 -6
- package/lib/mock/mock-pool.js +7 -2
- package/lib/mock/mock-symbols.js +2 -1
- package/lib/mock/mock-utils.js +33 -5
- package/lib/util/cache.js +360 -0
- package/lib/util/timers.js +50 -6
- package/lib/web/cache/cache.js +25 -21
- package/lib/web/cache/cachestorage.js +3 -1
- package/lib/web/cookies/index.js +18 -5
- package/lib/web/cookies/parse.js +6 -1
- package/lib/web/eventsource/eventsource.js +2 -0
- package/lib/web/fetch/body.js +43 -39
- package/lib/web/fetch/constants.js +45 -29
- package/lib/web/fetch/data-url.js +2 -2
- package/lib/web/fetch/formdata-parser.js +84 -46
- package/lib/web/fetch/formdata.js +42 -20
- package/lib/web/fetch/headers.js +119 -85
- package/lib/web/fetch/index.js +69 -65
- package/lib/web/fetch/request.js +132 -55
- package/lib/web/fetch/response.js +81 -36
- package/lib/web/fetch/util.js +274 -103
- package/lib/web/fetch/webidl.js +54 -18
- package/lib/web/websocket/connection.js +92 -15
- package/lib/web/websocket/constants.js +69 -9
- package/lib/web/websocket/events.js +8 -2
- package/lib/web/websocket/receiver.js +20 -26
- package/lib/web/websocket/stream/websocketerror.js +83 -0
- package/lib/web/websocket/stream/websocketstream.js +485 -0
- package/lib/web/websocket/util.js +115 -10
- package/lib/web/websocket/websocket.js +47 -170
- package/package.json +15 -11
- package/types/agent.d.ts +1 -1
- package/types/cache-interceptor.d.ts +172 -0
- package/types/cookies.d.ts +2 -0
- package/types/dispatcher.d.ts +29 -4
- package/types/env-http-proxy-agent.d.ts +1 -1
- package/types/fetch.d.ts +9 -8
- package/types/handlers.d.ts +4 -4
- package/types/index.d.ts +3 -1
- package/types/interceptors.d.ts +18 -1
- package/types/mock-agent.d.ts +4 -1
- package/types/mock-client.d.ts +1 -1
- package/types/mock-pool.d.ts +1 -1
- package/types/proxy-agent.d.ts +1 -1
- package/types/readable.d.ts +10 -7
- package/types/retry-handler.d.ts +3 -3
- package/types/webidl.d.ts +30 -4
- package/types/websocket.d.ts +33 -0
- package/lib/mock/pluralizer.js +0 -29
- package/lib/web/cache/symbols.js +0 -5
- package/lib/web/fetch/symbols.js +0 -8
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { parseHeaders } = require('../core/util')
|
|
4
|
+
const { InvalidArgumentError } = require('../core/errors')
|
|
5
|
+
|
|
6
|
+
const kResume = Symbol('resume')
|
|
7
|
+
|
|
8
|
+
class UnwrapController {
|
|
9
|
+
#paused = false
|
|
10
|
+
#reason = null
|
|
11
|
+
#aborted = false
|
|
12
|
+
#abort
|
|
13
|
+
|
|
14
|
+
[kResume] = null
|
|
15
|
+
|
|
16
|
+
constructor (abort) {
|
|
17
|
+
this.#abort = abort
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pause () {
|
|
21
|
+
this.#paused = true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resume () {
|
|
25
|
+
if (this.#paused) {
|
|
26
|
+
this.#paused = false
|
|
27
|
+
this[kResume]?.()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
abort (reason) {
|
|
32
|
+
if (!this.#aborted) {
|
|
33
|
+
this.#aborted = true
|
|
34
|
+
this.#reason = reason
|
|
35
|
+
this.#abort(reason)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get aborted () {
|
|
40
|
+
return this.#aborted
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get reason () {
|
|
44
|
+
return this.#reason
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get paused () {
|
|
48
|
+
return this.#paused
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = class UnwrapHandler {
|
|
53
|
+
#handler
|
|
54
|
+
#controller
|
|
55
|
+
|
|
56
|
+
constructor (handler) {
|
|
57
|
+
this.#handler = handler
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static unwrap (handler) {
|
|
61
|
+
// TODO (fix): More checks...
|
|
62
|
+
return !handler.onRequestStart ? handler : new UnwrapHandler(handler)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onConnect (abort, context) {
|
|
66
|
+
this.#controller = new UnwrapController(abort)
|
|
67
|
+
this.#handler.onRequestStart?.(this.#controller, context)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onUpgrade (statusCode, rawHeaders, socket) {
|
|
71
|
+
this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
|
75
|
+
this.#controller[kResume] = resume
|
|
76
|
+
this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage)
|
|
77
|
+
return !this.#controller.paused
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
onData (data) {
|
|
81
|
+
this.#handler.onResponseData?.(this.#controller, data)
|
|
82
|
+
return !this.#controller.paused
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onComplete (rawTrailers) {
|
|
86
|
+
this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onError (err) {
|
|
90
|
+
if (!this.#handler.onResponseError) {
|
|
91
|
+
throw new InvalidArgumentError('invalid onError method')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.#handler.onResponseError?.(this.#controller, err)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { InvalidArgumentError } = require('../core/errors')
|
|
4
|
+
|
|
5
|
+
module.exports = class WrapHandler {
|
|
6
|
+
#handler
|
|
7
|
+
|
|
8
|
+
constructor (handler) {
|
|
9
|
+
this.#handler = handler
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static wrap (handler) {
|
|
13
|
+
// TODO (fix): More checks...
|
|
14
|
+
return handler.onRequestStart ? handler : new WrapHandler(handler)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Unwrap Interface
|
|
18
|
+
|
|
19
|
+
onConnect (abort, context) {
|
|
20
|
+
return this.#handler.onConnect?.(abort, context)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
|
24
|
+
return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onUpgrade (statusCode, rawHeaders, socket) {
|
|
28
|
+
return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onData (data) {
|
|
32
|
+
return this.#handler.onData?.(data)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onComplete (trailers) {
|
|
36
|
+
return this.#handler.onComplete?.(trailers)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onError (err) {
|
|
40
|
+
if (!this.#handler.onError) {
|
|
41
|
+
throw err
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this.#handler.onError?.(err)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Wrap Interface
|
|
48
|
+
|
|
49
|
+
onRequestStart (controller, context) {
|
|
50
|
+
this.#handler.onConnect?.((reason) => controller.abort(reason), context)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onRequestUpgrade (controller, statusCode, headers, socket) {
|
|
54
|
+
const rawHeaders = []
|
|
55
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
56
|
+
// TODO (fix): What if val is Array
|
|
57
|
+
rawHeaders.push(Buffer.from(key), Buffer.from(val))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
64
|
+
const rawHeaders = []
|
|
65
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
66
|
+
// TODO (fix): What if val is Array
|
|
67
|
+
rawHeaders.push(Buffer.from(key), Buffer.from(val))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) {
|
|
71
|
+
controller.pause()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onResponseData (controller, data) {
|
|
76
|
+
if (this.#handler.onData?.(data) === false) {
|
|
77
|
+
controller.pause()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onResponseEnd (controller, trailers) {
|
|
82
|
+
const rawTrailers = []
|
|
83
|
+
for (const [key, val] of Object.entries(trailers)) {
|
|
84
|
+
// TODO (fix): What if val is Array
|
|
85
|
+
rawTrailers.push(Buffer.from(key), Buffer.from(val))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.#handler.onComplete?.(rawTrailers)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onResponseError (controller, err) {
|
|
92
|
+
if (!this.#handler.onError) {
|
|
93
|
+
throw new InvalidArgumentError('invalid onError method')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.#handler.onError?.(err)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { Readable } = require('node:stream')
|
|
5
|
+
const util = require('../core/util')
|
|
6
|
+
const CacheHandler = require('../handler/cache-handler')
|
|
7
|
+
const MemoryCacheStore = require('../cache/memory-cache-store')
|
|
8
|
+
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
|
|
9
|
+
const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js')
|
|
10
|
+
const { AbortError } = require('../core/errors.js')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
18
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function needsRevalidation (result, cacheControlDirectives) {
|
|
22
|
+
if (cacheControlDirectives?.['no-cache']) {
|
|
23
|
+
// Always revalidate requests with the no-cache directive
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const now = Date.now()
|
|
28
|
+
if (now > result.staleAt) {
|
|
29
|
+
// Response is stale
|
|
30
|
+
if (cacheControlDirectives?.['max-stale']) {
|
|
31
|
+
// There's a threshold where we can serve stale responses, let's see if
|
|
32
|
+
// we're in it
|
|
33
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
|
|
34
|
+
const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000)
|
|
35
|
+
return now > gracePeriod
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (cacheControlDirectives?.['min-fresh']) {
|
|
42
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
|
|
43
|
+
|
|
44
|
+
// At this point, staleAt is always > now
|
|
45
|
+
const timeLeftTillStale = result.staleAt - now
|
|
46
|
+
const threshold = cacheControlDirectives['min-fresh'] * 1000
|
|
47
|
+
|
|
48
|
+
return timeLeftTillStale <= threshold
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {DispatchFn} dispatch
|
|
56
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
|
|
57
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
58
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
|
|
59
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
|
|
60
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
|
|
61
|
+
*/
|
|
62
|
+
function handleUncachedResponse (
|
|
63
|
+
dispatch,
|
|
64
|
+
globalOpts,
|
|
65
|
+
cacheKey,
|
|
66
|
+
handler,
|
|
67
|
+
opts,
|
|
68
|
+
reqCacheControl
|
|
69
|
+
) {
|
|
70
|
+
if (reqCacheControl?.['only-if-cached']) {
|
|
71
|
+
let aborted = false
|
|
72
|
+
try {
|
|
73
|
+
if (typeof handler.onConnect === 'function') {
|
|
74
|
+
handler.onConnect(() => {
|
|
75
|
+
aborted = true
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (aborted) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof handler.onHeaders === 'function') {
|
|
84
|
+
handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
|
|
85
|
+
if (aborted) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof handler.onComplete === 'function') {
|
|
91
|
+
handler.onComplete([])
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (typeof handler.onError === 'function') {
|
|
95
|
+
handler.onError(err)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
107
|
+
* @param {number} age
|
|
108
|
+
*/
|
|
109
|
+
function sendCachedValue (handler, opts, result, age, context) {
|
|
110
|
+
// TODO (perf): Readable.from path can be optimized...
|
|
111
|
+
const stream = util.isStream(result.body)
|
|
112
|
+
? result.body
|
|
113
|
+
: Readable.from(result.body ?? [])
|
|
114
|
+
|
|
115
|
+
assert(!stream.destroyed, 'stream should not be destroyed')
|
|
116
|
+
assert(!stream.readableDidRead, 'stream should not be readableDidRead')
|
|
117
|
+
|
|
118
|
+
const controller = {
|
|
119
|
+
resume () {
|
|
120
|
+
stream.resume()
|
|
121
|
+
},
|
|
122
|
+
pause () {
|
|
123
|
+
stream.pause()
|
|
124
|
+
},
|
|
125
|
+
get paused () {
|
|
126
|
+
return stream.isPaused()
|
|
127
|
+
},
|
|
128
|
+
get aborted () {
|
|
129
|
+
return stream.destroyed
|
|
130
|
+
},
|
|
131
|
+
get reason () {
|
|
132
|
+
return stream.errored
|
|
133
|
+
},
|
|
134
|
+
abort (reason) {
|
|
135
|
+
stream.destroy(reason ?? new AbortError())
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
stream
|
|
140
|
+
.on('error', function (err) {
|
|
141
|
+
if (!this.readableEnded) {
|
|
142
|
+
if (typeof handler.onResponseError === 'function') {
|
|
143
|
+
handler.onResponseError(controller, err)
|
|
144
|
+
} else {
|
|
145
|
+
throw err
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
.on('close', function () {
|
|
150
|
+
if (!this.errored) {
|
|
151
|
+
handler.onResponseEnd?.(controller, {})
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
handler.onRequestStart?.(controller, context)
|
|
156
|
+
|
|
157
|
+
if (stream.destroyed) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add the age header
|
|
162
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
|
|
163
|
+
// TODO (fix): What if headers.age already exists?
|
|
164
|
+
const headers = age != null ? { ...result.headers, age: String(age) } : result.headers
|
|
165
|
+
|
|
166
|
+
handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
|
|
167
|
+
|
|
168
|
+
if (opts.method === 'HEAD') {
|
|
169
|
+
stream.destroy()
|
|
170
|
+
} else {
|
|
171
|
+
stream.on('data', function (chunk) {
|
|
172
|
+
handler.onResponseData?.(controller, chunk)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {DispatchFn} dispatch
|
|
179
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
|
|
180
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
|
181
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
|
|
182
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
|
|
183
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
|
|
184
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result
|
|
185
|
+
*/
|
|
186
|
+
function handleResult (
|
|
187
|
+
dispatch,
|
|
188
|
+
globalOpts,
|
|
189
|
+
cacheKey,
|
|
190
|
+
handler,
|
|
191
|
+
opts,
|
|
192
|
+
reqCacheControl,
|
|
193
|
+
result
|
|
194
|
+
) {
|
|
195
|
+
if (!result) {
|
|
196
|
+
return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const now = Date.now()
|
|
200
|
+
if (now > result.deleteAt) {
|
|
201
|
+
// Response is expired, cache store shouldn't have given this to us
|
|
202
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const age = Math.round((now - result.cachedAt) / 1000)
|
|
206
|
+
if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
|
|
207
|
+
// Response is considered expired for this specific request
|
|
208
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
|
|
209
|
+
return dispatch(opts, handler)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check if the response is stale
|
|
213
|
+
if (needsRevalidation(result, reqCacheControl)) {
|
|
214
|
+
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
|
|
215
|
+
// If body is is stream we can't revalidate...
|
|
216
|
+
// TODO (fix): This could be less strict...
|
|
217
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let withinStaleIfErrorThreshold = false
|
|
221
|
+
const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
|
|
222
|
+
if (staleIfErrorExpiry) {
|
|
223
|
+
withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let headers = {
|
|
227
|
+
...opts.headers,
|
|
228
|
+
'if-modified-since': new Date(result.cachedAt).toUTCString(),
|
|
229
|
+
'if-none-match': result.etag
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (result.vary) {
|
|
233
|
+
headers = {
|
|
234
|
+
...headers,
|
|
235
|
+
...result.vary
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// We need to revalidate the response
|
|
240
|
+
return dispatch(
|
|
241
|
+
{
|
|
242
|
+
...opts,
|
|
243
|
+
headers
|
|
244
|
+
},
|
|
245
|
+
new CacheRevalidationHandler(
|
|
246
|
+
(success, context) => {
|
|
247
|
+
if (success) {
|
|
248
|
+
sendCachedValue(handler, opts, result, age, context)
|
|
249
|
+
} else if (util.isStream(result.body)) {
|
|
250
|
+
result.body.on('error', () => {}).destroy()
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
new CacheHandler(globalOpts, cacheKey, handler),
|
|
254
|
+
withinStaleIfErrorThreshold
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Dump request body.
|
|
260
|
+
if (util.isStream(opts.body)) {
|
|
261
|
+
opts.body.on('error', () => {}).destroy()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
sendCachedValue(handler, opts, result, age, null)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
|
|
269
|
+
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
|
|
270
|
+
*/
|
|
271
|
+
module.exports = (opts = {}) => {
|
|
272
|
+
const {
|
|
273
|
+
store = new MemoryCacheStore(),
|
|
274
|
+
methods = ['GET'],
|
|
275
|
+
cacheByDefault = undefined,
|
|
276
|
+
type = 'shared'
|
|
277
|
+
} = opts
|
|
278
|
+
|
|
279
|
+
if (typeof opts !== 'object' || opts === null) {
|
|
280
|
+
throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
assertCacheStore(store, 'opts.store')
|
|
284
|
+
assertCacheMethods(methods, 'opts.methods')
|
|
285
|
+
|
|
286
|
+
if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
|
|
287
|
+
throw new TypeError(`exepcted opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
|
|
291
|
+
throw new TypeError(`exepcted opts.type to be shared, private, or undefined, got ${typeof type}`)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const globalOpts = {
|
|
295
|
+
store,
|
|
296
|
+
methods,
|
|
297
|
+
cacheByDefault,
|
|
298
|
+
type
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
|
|
302
|
+
|
|
303
|
+
return dispatch => {
|
|
304
|
+
return (opts, handler) => {
|
|
305
|
+
if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
|
|
306
|
+
// Not a method we want to cache or we don't have the origin, skip
|
|
307
|
+
return dispatch(opts, handler)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const reqCacheControl = opts.headers?.['cache-control']
|
|
311
|
+
? parseCacheControlHeader(opts.headers['cache-control'])
|
|
312
|
+
: undefined
|
|
313
|
+
|
|
314
|
+
if (reqCacheControl?.['no-store']) {
|
|
315
|
+
return dispatch(opts, handler)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
320
|
+
*/
|
|
321
|
+
const cacheKey = makeCacheKey(opts)
|
|
322
|
+
const result = store.get(cacheKey)
|
|
323
|
+
|
|
324
|
+
if (result && typeof result.then === 'function') {
|
|
325
|
+
result.then(result => {
|
|
326
|
+
handleResult(dispatch,
|
|
327
|
+
globalOpts,
|
|
328
|
+
cacheKey,
|
|
329
|
+
handler,
|
|
330
|
+
opts,
|
|
331
|
+
reqCacheControl,
|
|
332
|
+
result
|
|
333
|
+
)
|
|
334
|
+
})
|
|
335
|
+
} else {
|
|
336
|
+
handleResult(
|
|
337
|
+
dispatch,
|
|
338
|
+
globalOpts,
|
|
339
|
+
cacheKey,
|
|
340
|
+
handler,
|
|
341
|
+
opts,
|
|
342
|
+
reqCacheControl,
|
|
343
|
+
result
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|