undici 7.0.0-alpha.2 → 7.0.0-alpha.3
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 +1 -1
- package/docs/docs/api/CacheStore.md +116 -0
- package/docs/docs/api/Dispatcher.md +10 -0
- package/index.js +6 -1
- package/lib/api/api-request.js +1 -1
- package/lib/api/readable.js +6 -6
- package/lib/cache/memory-cache-store.js +417 -0
- package/lib/core/constants.js +24 -1
- package/lib/core/util.js +41 -2
- package/lib/dispatcher/client-h1.js +100 -87
- package/lib/dispatcher/client-h2.js +127 -75
- package/lib/dispatcher/pool-base.js +3 -3
- package/lib/handler/cache-handler.js +359 -0
- package/lib/handler/cache-revalidation-handler.js +119 -0
- package/lib/interceptor/cache.js +171 -0
- package/lib/util/cache.js +224 -0
- package/lib/web/cache/cache.js +1 -0
- package/lib/web/cache/cachestorage.js +2 -0
- package/lib/web/eventsource/eventsource.js +2 -0
- package/lib/web/fetch/constants.js +12 -5
- package/lib/web/fetch/data-url.js +2 -2
- package/lib/web/fetch/formdata.js +3 -1
- package/lib/web/fetch/headers.js +2 -0
- package/lib/web/fetch/request.js +3 -1
- package/lib/web/fetch/response.js +3 -1
- package/lib/web/fetch/util.js +171 -47
- package/lib/web/fetch/webidl.js +16 -12
- package/lib/web/websocket/constants.js +67 -6
- package/lib/web/websocket/events.js +4 -0
- package/lib/web/websocket/stream/websocketerror.js +1 -1
- package/lib/web/websocket/websocket.js +2 -0
- package/package.json +7 -3
- package/types/cache-interceptor.d.ts +97 -0
- package/types/fetch.d.ts +9 -8
- package/types/index.d.ts +3 -0
- package/types/interceptors.d.ts +4 -0
- package/types/webidl.d.ts +7 -1
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ Returns a promise with the result of the `Dispatcher.request` method.
|
|
|
132
132
|
|
|
133
133
|
Calls `options.dispatcher.request(options)`.
|
|
134
134
|
|
|
135
|
-
See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
|
|
135
|
+
See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./docs/examples/README.md) for examples.
|
|
136
136
|
|
|
137
137
|
### `undici.stream([url, options, ]factory): Promise`
|
|
138
138
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Cache Store
|
|
2
|
+
|
|
3
|
+
A Cache Store is responsible for storing and retrieving cached responses.
|
|
4
|
+
It is also responsible for deciding which specific response to use based off of
|
|
5
|
+
a response's `Vary` header (if present).
|
|
6
|
+
|
|
7
|
+
## Pre-built Cache Stores
|
|
8
|
+
|
|
9
|
+
### `MemoryCacheStore`
|
|
10
|
+
|
|
11
|
+
The `MemoryCacheStore` stores the responses in-memory.
|
|
12
|
+
|
|
13
|
+
**Options**
|
|
14
|
+
|
|
15
|
+
- `maxEntries` - The maximum amount of responses to store. Default `Infinity`.
|
|
16
|
+
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached.
|
|
17
|
+
|
|
18
|
+
## Defining a Custom Cache Store
|
|
19
|
+
|
|
20
|
+
The store must implement the following functions:
|
|
21
|
+
|
|
22
|
+
### Getter: `isFull`
|
|
23
|
+
|
|
24
|
+
This tells the cache interceptor if the store is full or not. If this is true,
|
|
25
|
+
the cache interceptor will not attempt to cache the response.
|
|
26
|
+
|
|
27
|
+
### Function: `createReadStream`
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
|
|
31
|
+
* **req** `Dispatcher.RequestOptions` - Incoming request
|
|
32
|
+
|
|
33
|
+
Returns: `CacheStoreReadable | Promise<CacheStoreReadable | undefined> | undefined` - If the request is cached, a readable for the body is returned. Otherwise, `undefined` is returned.
|
|
34
|
+
|
|
35
|
+
### Function: `createWriteStream`
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
|
|
39
|
+
* **req** `Dispatcher.RequestOptions` - Incoming request
|
|
40
|
+
* **value** `CacheStoreValue` - Response to store
|
|
41
|
+
|
|
42
|
+
Returns: `CacheStoreWriteable | undefined` - If the store is full, return `undefined`. Otherwise, return a writable so that the cache interceptor can stream the body and trailers to the store.
|
|
43
|
+
|
|
44
|
+
## `CacheStoreValue`
|
|
45
|
+
|
|
46
|
+
This is an interface containing the majority of a response's data (minus the body).
|
|
47
|
+
|
|
48
|
+
### Property `statusCode`
|
|
49
|
+
|
|
50
|
+
`number` - The response's HTTP status code.
|
|
51
|
+
|
|
52
|
+
### Property `statusMessage`
|
|
53
|
+
|
|
54
|
+
`string` - The response's HTTP status message.
|
|
55
|
+
|
|
56
|
+
### Property `rawHeaders`
|
|
57
|
+
|
|
58
|
+
`(Buffer | Buffer[])[]` - The response's headers.
|
|
59
|
+
|
|
60
|
+
### Property `rawTrailers`
|
|
61
|
+
|
|
62
|
+
`string[] | undefined` - The response's trailers.
|
|
63
|
+
|
|
64
|
+
### Property `vary`
|
|
65
|
+
|
|
66
|
+
`Record<string, string> | undefined` - The headers defined by the response's `Vary` header
|
|
67
|
+
and their respective values for later comparison
|
|
68
|
+
|
|
69
|
+
For example, for a response like
|
|
70
|
+
```
|
|
71
|
+
Vary: content-encoding, accepts
|
|
72
|
+
content-encoding: utf8
|
|
73
|
+
accepts: application/json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This would be
|
|
77
|
+
```js
|
|
78
|
+
{
|
|
79
|
+
'content-encoding': 'utf8',
|
|
80
|
+
accepts: 'application/json'
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Property `cachedAt`
|
|
85
|
+
|
|
86
|
+
`number` - Time in millis that this value was cached.
|
|
87
|
+
|
|
88
|
+
### Property `staleAt`
|
|
89
|
+
|
|
90
|
+
`number` - Time in millis that this value is considered stale.
|
|
91
|
+
|
|
92
|
+
### Property `deleteAt`
|
|
93
|
+
|
|
94
|
+
`number` - Time in millis that this value is to be deleted from the cache. This
|
|
95
|
+
is either the same sa staleAt or the `max-stale` caching directive.
|
|
96
|
+
|
|
97
|
+
The store must not return a response after the time defined in this property.
|
|
98
|
+
|
|
99
|
+
## `CacheStoreReadable`
|
|
100
|
+
|
|
101
|
+
This extends Node's [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
|
|
102
|
+
and defines extra properties relevant to the cache interceptor.
|
|
103
|
+
|
|
104
|
+
### Getter: `value`
|
|
105
|
+
|
|
106
|
+
The response's [`CacheStoreValue`](#cachestorevalue)
|
|
107
|
+
|
|
108
|
+
## `CacheStoreWriteable`
|
|
109
|
+
|
|
110
|
+
This extends Node's [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable)
|
|
111
|
+
and defines extra properties relevant to the cache interceptor.
|
|
112
|
+
|
|
113
|
+
### Setter: `rawTrailers`
|
|
114
|
+
|
|
115
|
+
If the response has trailers, the cache interceptor will pass them to the cache
|
|
116
|
+
interceptor through this method.
|
|
@@ -1233,6 +1233,16 @@ test('should not error if request status code is not in the specified error code
|
|
|
1233
1233
|
|
|
1234
1234
|
The Response Error Interceptor provides a robust mechanism for handling HTTP response errors by capturing detailed error information and propagating it through a structured `ResponseError` class. This enhancement improves error handling and debugging capabilities in applications using the interceptor.
|
|
1235
1235
|
|
|
1236
|
+
##### `Cache Interceptor`
|
|
1237
|
+
|
|
1238
|
+
The `cache` interceptor implements client-side response caching as described in
|
|
1239
|
+
[RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html).
|
|
1240
|
+
|
|
1241
|
+
**Options**
|
|
1242
|
+
|
|
1243
|
+
- `store` - The [`CacheStore`](./CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](./CacheStore.md#memorycachestore).
|
|
1244
|
+
- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
|
|
1245
|
+
|
|
1236
1246
|
## Instance Events
|
|
1237
1247
|
|
|
1238
1248
|
### Event: `'connect'`
|
package/index.js
CHANGED
|
@@ -40,7 +40,12 @@ module.exports.interceptors = {
|
|
|
40
40
|
redirect: require('./lib/interceptor/redirect'),
|
|
41
41
|
retry: require('./lib/interceptor/retry'),
|
|
42
42
|
dump: require('./lib/interceptor/dump'),
|
|
43
|
-
dns: require('./lib/interceptor/dns')
|
|
43
|
+
dns: require('./lib/interceptor/dns'),
|
|
44
|
+
cache: require('./lib/interceptor/cache')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports.cacheStores = {
|
|
48
|
+
MemoryCacheStore: require('./lib/cache/memory-cache-store')
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
module.exports.buildConnector = buildConnector
|
package/lib/api/api-request.js
CHANGED
|
@@ -65,7 +65,7 @@ class RequestHandler extends AsyncResource {
|
|
|
65
65
|
this.removeAbortListener = util.addAbortListener(signal, () => {
|
|
66
66
|
this.reason = signal.reason ?? new RequestAbortedError()
|
|
67
67
|
if (this.res) {
|
|
68
|
-
util.destroy(this.res, this.reason)
|
|
68
|
+
util.destroy(this.res.on('error', noop), this.reason)
|
|
69
69
|
} else if (this.abort) {
|
|
70
70
|
this.abort(this.reason)
|
|
71
71
|
}
|
package/lib/api/readable.js
CHANGED
|
@@ -164,7 +164,7 @@ class BodyReadable extends Readable {
|
|
|
164
164
|
* @see https://fetch.spec.whatwg.org/#dom-body-text
|
|
165
165
|
* @returns {Promise<string>}
|
|
166
166
|
*/
|
|
167
|
-
|
|
167
|
+
text () {
|
|
168
168
|
return consume(this, 'text')
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -174,7 +174,7 @@ class BodyReadable extends Readable {
|
|
|
174
174
|
* @see https://fetch.spec.whatwg.org/#dom-body-json
|
|
175
175
|
* @returns {Promise<unknown>}
|
|
176
176
|
*/
|
|
177
|
-
|
|
177
|
+
json () {
|
|
178
178
|
return consume(this, 'json')
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -184,7 +184,7 @@ class BodyReadable extends Readable {
|
|
|
184
184
|
* @see https://fetch.spec.whatwg.org/#dom-body-blob
|
|
185
185
|
* @returns {Promise<Blob>}
|
|
186
186
|
*/
|
|
187
|
-
|
|
187
|
+
blob () {
|
|
188
188
|
return consume(this, 'blob')
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -194,7 +194,7 @@ class BodyReadable extends Readable {
|
|
|
194
194
|
* @see https://fetch.spec.whatwg.org/#dom-body-bytes
|
|
195
195
|
* @returns {Promise<Uint8Array>}
|
|
196
196
|
*/
|
|
197
|
-
|
|
197
|
+
bytes () {
|
|
198
198
|
return consume(this, 'bytes')
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -204,7 +204,7 @@ class BodyReadable extends Readable {
|
|
|
204
204
|
* @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
|
|
205
205
|
* @returns {Promise<ArrayBuffer>}
|
|
206
206
|
*/
|
|
207
|
-
|
|
207
|
+
arrayBuffer () {
|
|
208
208
|
return consume(this, 'arrayBuffer')
|
|
209
209
|
}
|
|
210
210
|
|
|
@@ -355,7 +355,7 @@ function isUnusable (bodyReadable) {
|
|
|
355
355
|
* @param {string} type
|
|
356
356
|
* @returns {Promise<any>}
|
|
357
357
|
*/
|
|
358
|
-
|
|
358
|
+
function consume (stream, type) {
|
|
359
359
|
assert(!stream[kConsume])
|
|
360
360
|
|
|
361
361
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Writable, Readable } = require('node:stream')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
|
|
7
|
+
* @implements {CacheStore}
|
|
8
|
+
*
|
|
9
|
+
* @typedef {{
|
|
10
|
+
* readers: number
|
|
11
|
+
* readLock: boolean
|
|
12
|
+
* writeLock: boolean
|
|
13
|
+
* opts: import('../../types/cache-interceptor.d.ts').default.CacheStoreValue
|
|
14
|
+
* body: Buffer[]
|
|
15
|
+
* }} MemoryStoreValue
|
|
16
|
+
*/
|
|
17
|
+
class MemoryCacheStore {
|
|
18
|
+
#maxEntries = Infinity
|
|
19
|
+
|
|
20
|
+
#maxEntrySize = Infinity
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @type {((err) => void) | undefined}
|
|
24
|
+
*/
|
|
25
|
+
#errorCallback = undefined
|
|
26
|
+
|
|
27
|
+
#entryCount = 0
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @type {Map<string, Map<string, MemoryStoreValue>>}
|
|
31
|
+
*/
|
|
32
|
+
#data = new Map()
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
|
|
36
|
+
*/
|
|
37
|
+
constructor (opts) {
|
|
38
|
+
if (opts) {
|
|
39
|
+
if (typeof opts !== 'object') {
|
|
40
|
+
throw new TypeError('MemoryCacheStore options must be an object')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (opts.maxEntries !== undefined) {
|
|
44
|
+
if (
|
|
45
|
+
typeof opts.maxEntries !== 'number' ||
|
|
46
|
+
!Number.isInteger(opts.maxEntries) ||
|
|
47
|
+
opts.maxEntries < 0
|
|
48
|
+
) {
|
|
49
|
+
throw new TypeError('MemoryCacheStore options.maxEntries must be a non-negative integer')
|
|
50
|
+
}
|
|
51
|
+
this.#maxEntries = opts.maxEntries
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (opts.maxEntrySize !== undefined) {
|
|
55
|
+
if (
|
|
56
|
+
typeof opts.maxEntrySize !== 'number' ||
|
|
57
|
+
!Number.isInteger(opts.maxEntrySize) ||
|
|
58
|
+
opts.maxEntrySize < 0
|
|
59
|
+
) {
|
|
60
|
+
throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
|
|
61
|
+
}
|
|
62
|
+
this.#maxEntrySize = opts.maxEntrySize
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (opts.errorCallback !== undefined) {
|
|
66
|
+
if (typeof opts.errorCallback !== 'function') {
|
|
67
|
+
throw new TypeError('MemoryCacheStore options.errorCallback must be a function')
|
|
68
|
+
}
|
|
69
|
+
this.#errorCallback = opts.errorCallback
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get isFull () {
|
|
75
|
+
return this.#entryCount >= this.#maxEntries
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
|
|
80
|
+
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreReadable | undefined}
|
|
81
|
+
*/
|
|
82
|
+
createReadStream (req) {
|
|
83
|
+
if (typeof req !== 'object') {
|
|
84
|
+
throw new TypeError(`expected req to be object, got ${typeof req}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const values = this.#getValuesForRequest(req, false)
|
|
88
|
+
if (!values) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const value = this.#findValue(req, values)
|
|
93
|
+
|
|
94
|
+
if (!value || value.readLock) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return new MemoryStoreReadableStream(value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
|
|
103
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} opts
|
|
104
|
+
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheStoreWriteable | undefined}
|
|
105
|
+
*/
|
|
106
|
+
createWriteStream (req, opts) {
|
|
107
|
+
if (typeof req !== 'object') {
|
|
108
|
+
throw new TypeError(`expected req to be object, got ${typeof req}`)
|
|
109
|
+
}
|
|
110
|
+
if (typeof opts !== 'object') {
|
|
111
|
+
throw new TypeError(`expected value to be object, got ${typeof opts}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.isFull) {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const values = this.#getValuesForRequest(req, true)
|
|
119
|
+
|
|
120
|
+
let value = this.#findValue(req, values)
|
|
121
|
+
if (!value) {
|
|
122
|
+
// The value doesn't already exist, meaning we haven't cached this
|
|
123
|
+
// response before. Let's assign it a value and insert it into our data
|
|
124
|
+
// property.
|
|
125
|
+
|
|
126
|
+
if (this.isFull) {
|
|
127
|
+
// Or not, we don't have space to add another response
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.#entryCount++
|
|
132
|
+
|
|
133
|
+
value = {
|
|
134
|
+
readers: 0,
|
|
135
|
+
readLock: false,
|
|
136
|
+
writeLock: false,
|
|
137
|
+
opts,
|
|
138
|
+
body: []
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// We want to sort our responses in decending order by their deleteAt
|
|
142
|
+
// timestamps so that deleting expired responses is faster
|
|
143
|
+
if (
|
|
144
|
+
values.length === 0 ||
|
|
145
|
+
opts.deleteAt < values[values.length - 1].deleteAt
|
|
146
|
+
) {
|
|
147
|
+
// Our value is either the only response for this path or our deleteAt
|
|
148
|
+
// time is sooner than all the other responses
|
|
149
|
+
values.push(value)
|
|
150
|
+
} else if (opts.deleteAt >= values[0].deleteAt) {
|
|
151
|
+
// Our deleteAt is later than everyone elses
|
|
152
|
+
values.unshift(value)
|
|
153
|
+
} else {
|
|
154
|
+
// We're neither in the front or the end, let's just binary search to
|
|
155
|
+
// find our stop we need to be in
|
|
156
|
+
let startIndex = 0
|
|
157
|
+
let endIndex = values.length
|
|
158
|
+
while (true) {
|
|
159
|
+
if (startIndex === endIndex) {
|
|
160
|
+
values.splice(startIndex, 0, value)
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const middleIndex = Math.floor((startIndex + endIndex) / 2)
|
|
165
|
+
const middleValue = values[middleIndex]
|
|
166
|
+
if (opts.deleteAt === middleIndex) {
|
|
167
|
+
values.splice(middleIndex, 0, value)
|
|
168
|
+
break
|
|
169
|
+
} else if (opts.deleteAt > middleValue.opts.deleteAt) {
|
|
170
|
+
endIndex = middleIndex
|
|
171
|
+
continue
|
|
172
|
+
} else {
|
|
173
|
+
startIndex = middleIndex
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// Check if there's already another request writing to the value or
|
|
180
|
+
// a request reading from it
|
|
181
|
+
if (value.writeLock || value.readLock) {
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Empty it so we can overwrite it
|
|
186
|
+
value.body = []
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const writable = new MemoryStoreWritableStream(
|
|
190
|
+
value,
|
|
191
|
+
this.#maxEntrySize
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Remove the value if there was some error
|
|
195
|
+
writable.on('error', (err) => {
|
|
196
|
+
values.filter(current => value !== current)
|
|
197
|
+
if (this.#errorCallback) {
|
|
198
|
+
this.#errorCallback(err)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
writable.on('bodyOversized', () => {
|
|
203
|
+
values.filter(current => value !== current)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
return writable
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {string} origin
|
|
211
|
+
*/
|
|
212
|
+
deleteByOrigin (origin) {
|
|
213
|
+
this.#data.delete(origin)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Gets all of the requests of the same origin, path, and method. Does not
|
|
218
|
+
* take the `vary` property into account.
|
|
219
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
|
|
220
|
+
* @param {boolean} [makeIfDoesntExist=false]
|
|
221
|
+
*/
|
|
222
|
+
#getValuesForRequest (req, makeIfDoesntExist) {
|
|
223
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
|
|
224
|
+
let cachedPaths = this.#data.get(req.origin)
|
|
225
|
+
if (!cachedPaths) {
|
|
226
|
+
if (!makeIfDoesntExist) {
|
|
227
|
+
return undefined
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
cachedPaths = new Map()
|
|
231
|
+
this.#data.set(req.origin, cachedPaths)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let values = cachedPaths.get(`${req.path}:${req.method}`)
|
|
235
|
+
if (!values && makeIfDoesntExist) {
|
|
236
|
+
values = []
|
|
237
|
+
cachedPaths.set(`${req.path}:${req.method}`, values)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return values
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Given a list of values of a certain request, this decides the best value
|
|
245
|
+
* to respond with.
|
|
246
|
+
* @param {import('../../types/dispatcher.d.ts').default.RequestOptions} req
|
|
247
|
+
* @param {MemoryStoreValue[]} values
|
|
248
|
+
* @returns {MemoryStoreValue | undefined}
|
|
249
|
+
*/
|
|
250
|
+
#findValue (req, values) {
|
|
251
|
+
/**
|
|
252
|
+
* @type {MemoryStoreValue}
|
|
253
|
+
*/
|
|
254
|
+
let value
|
|
255
|
+
const now = Date.now()
|
|
256
|
+
for (let i = values.length - 1; i >= 0; i--) {
|
|
257
|
+
const current = values[i]
|
|
258
|
+
const currentCacheValue = current.opts
|
|
259
|
+
if (now >= currentCacheValue.deleteAt) {
|
|
260
|
+
// We've reached expired values, let's delete them
|
|
261
|
+
this.#entryCount -= values.length - i
|
|
262
|
+
values.length = i
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let matches = true
|
|
267
|
+
|
|
268
|
+
if (currentCacheValue.vary) {
|
|
269
|
+
if (!req.headers) {
|
|
270
|
+
matches = false
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const key in currentCacheValue.vary) {
|
|
275
|
+
if (currentCacheValue.vary[key] !== req.headers[key]) {
|
|
276
|
+
matches = false
|
|
277
|
+
break
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (matches) {
|
|
283
|
+
value = current
|
|
284
|
+
break
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return value
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
class MemoryStoreReadableStream extends Readable {
|
|
293
|
+
/**
|
|
294
|
+
* @type {MemoryStoreValue}
|
|
295
|
+
*/
|
|
296
|
+
#value
|
|
297
|
+
/**
|
|
298
|
+
* @type {Buffer[]}
|
|
299
|
+
*/
|
|
300
|
+
#chunksToSend = []
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {MemoryStoreValue} value
|
|
304
|
+
*/
|
|
305
|
+
constructor (value) {
|
|
306
|
+
super()
|
|
307
|
+
|
|
308
|
+
if (value.readLock) {
|
|
309
|
+
throw new Error('can\'t read a locked value')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.#value = value
|
|
313
|
+
this.#chunksToSend = value?.body ? [...value.body, null] : [null]
|
|
314
|
+
|
|
315
|
+
this.#value.readers++
|
|
316
|
+
this.#value.writeLock = true
|
|
317
|
+
|
|
318
|
+
this.on('close', () => {
|
|
319
|
+
this.#value.readers--
|
|
320
|
+
|
|
321
|
+
if (this.#value.readers === 0) {
|
|
322
|
+
this.#value.writeLock = false
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get value () {
|
|
328
|
+
return this.#value.opts
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {number} size
|
|
333
|
+
*/
|
|
334
|
+
_read (size) {
|
|
335
|
+
if (this.#chunksToSend.length === 0) {
|
|
336
|
+
throw new Error('no chunks left to read, stream should have closed')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (size > this.#chunksToSend.length) {
|
|
340
|
+
size = this.#chunksToSend.length
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (let i = 0; i < size; i++) {
|
|
344
|
+
this.push(this.#chunksToSend.shift())
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
class MemoryStoreWritableStream extends Writable {
|
|
350
|
+
/**
|
|
351
|
+
* @type {MemoryStoreValue}
|
|
352
|
+
*/
|
|
353
|
+
#value
|
|
354
|
+
#currentSize = 0
|
|
355
|
+
#maxEntrySize = 0
|
|
356
|
+
/**
|
|
357
|
+
* @type {Buffer[]|null}
|
|
358
|
+
*/
|
|
359
|
+
#body = []
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @param {MemoryStoreValue} value
|
|
363
|
+
* @param {number} maxEntrySize
|
|
364
|
+
*/
|
|
365
|
+
constructor (value, maxEntrySize) {
|
|
366
|
+
super()
|
|
367
|
+
this.#value = value
|
|
368
|
+
this.#value.readLock = true
|
|
369
|
+
this.#maxEntrySize = maxEntrySize ?? Infinity
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
get rawTrailers () {
|
|
373
|
+
return this.#value.opts.rawTrailers
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param {string[] | undefined} trailers
|
|
378
|
+
*/
|
|
379
|
+
set rawTrailers (trailers) {
|
|
380
|
+
this.#value.opts.rawTrailers = trailers
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @param {Buffer} chunk
|
|
385
|
+
* @param {string} encoding
|
|
386
|
+
* @param {BufferEncoding} encoding
|
|
387
|
+
*/
|
|
388
|
+
_write (chunk, encoding, callback) {
|
|
389
|
+
if (typeof chunk === 'string') {
|
|
390
|
+
chunk = Buffer.from(chunk, encoding)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.#currentSize += chunk.byteLength
|
|
394
|
+
if (this.#currentSize < this.#maxEntrySize) {
|
|
395
|
+
this.#body.push(chunk)
|
|
396
|
+
} else {
|
|
397
|
+
this.#body = null // release memory as early as possible
|
|
398
|
+
this.emit('bodyOversized')
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
callback()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @param {() => void} callback
|
|
406
|
+
*/
|
|
407
|
+
_final (callback) {
|
|
408
|
+
if (this.#currentSize < this.#maxEntrySize) {
|
|
409
|
+
this.#value.readLock = false
|
|
410
|
+
this.#value.body = this.#body
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
callback()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = MemoryCacheStore
|
package/lib/core/constants.js
CHANGED
|
@@ -107,6 +107,28 @@ const headerNameLowerCasedRecord = {}
|
|
|
107
107
|
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
|
108
108
|
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @type {Record<Lowercase<typeof wellknownHeaderNames[number]>, Buffer>}
|
|
112
|
+
*/
|
|
113
|
+
const wellknownHeaderNameBuffers = {}
|
|
114
|
+
|
|
115
|
+
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
|
116
|
+
Object.setPrototypeOf(wellknownHeaderNameBuffers, null)
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} header Lowercased header
|
|
120
|
+
* @returns {Buffer}
|
|
121
|
+
*/
|
|
122
|
+
function getHeaderNameAsBuffer (header) {
|
|
123
|
+
let buffer = wellknownHeaderNameBuffers[header]
|
|
124
|
+
|
|
125
|
+
if (buffer === undefined) {
|
|
126
|
+
buffer = Buffer.from(header)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return buffer
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
|
111
133
|
const key = wellknownHeaderNames[i]
|
|
112
134
|
const lowerCasedKey = key.toLowerCase()
|
|
@@ -116,5 +138,6 @@ for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
|
|
116
138
|
|
|
117
139
|
module.exports = {
|
|
118
140
|
wellknownHeaderNames,
|
|
119
|
-
headerNameLowerCasedRecord
|
|
141
|
+
headerNameLowerCasedRecord,
|
|
142
|
+
getHeaderNameAsBuffer
|
|
120
143
|
}
|