undici 7.0.0-alpha.6 → 7.0.0-alpha.8
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/docs/docs/api/CacheStore.md +13 -1
- package/docs/docs/api/Dispatcher.md +8 -8
- package/docs/docs/api/RedirectHandler.md +1 -1
- package/docs/docs/api/RetryHandler.md +2 -2
- package/index.js +9 -0
- package/lib/cache/memory-cache-store.js +7 -10
- package/lib/cache/sqlite-cache-store.js +446 -0
- package/lib/core/util.js +5 -0
- package/lib/dispatcher/client-h1.js +11 -0
- package/lib/dispatcher/client-h2.js +20 -6
- package/lib/dispatcher/dispatcher-base.js +2 -1
- package/lib/dispatcher/dispatcher.js +4 -0
- package/lib/handler/cache-handler.js +189 -195
- package/lib/handler/cache-revalidation-handler.js +44 -71
- package/lib/handler/decorator-handler.js +3 -0
- package/lib/handler/redirect-handler.js +39 -55
- package/lib/handler/retry-handler.js +9 -15
- package/lib/handler/unwrap-handler.js +96 -0
- package/lib/handler/wrap-handler.js +98 -0
- package/lib/interceptor/cache.js +254 -193
- package/lib/util/cache.js +117 -41
- package/package.json +4 -3
- package/types/agent.d.ts +1 -1
- package/types/cache-interceptor.d.ts +84 -6
- package/types/dispatcher.d.ts +28 -3
- package/types/env-http-proxy-agent.d.ts +1 -1
- package/types/handlers.d.ts +4 -4
- package/types/mock-agent.d.ts +1 -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/retry-handler.d.ts +3 -3
|
@@ -13,9 +13,21 @@ The `MemoryCacheStore` stores the responses in-memory.
|
|
|
13
13
|
|
|
14
14
|
**Options**
|
|
15
15
|
|
|
16
|
-
- `
|
|
16
|
+
- `maxCount` - The maximum amount of responses to store. Default `Infinity`.
|
|
17
17
|
- `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.
|
|
18
18
|
|
|
19
|
+
### `SqliteCacheStore`
|
|
20
|
+
|
|
21
|
+
The `SqliteCacheStore` stores the responses in a SQLite database.
|
|
22
|
+
Under the hood, it uses Node.js' [`node:sqlite`](https://nodejs.org/api/sqlite.html) api.
|
|
23
|
+
The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present.
|
|
24
|
+
|
|
25
|
+
**Options**
|
|
26
|
+
|
|
27
|
+
- `location` - The location of the SQLite database to use. Default `:memory:`.
|
|
28
|
+
- `maxCount` - The maximum number of entries to store in the database. Default `Infinity`.
|
|
29
|
+
- `maxEntrySize` - The maximum size in bytes that a resposne's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
|
|
30
|
+
|
|
19
31
|
## Defining a Custom Cache Store
|
|
20
32
|
|
|
21
33
|
The store must implement the following functions:
|
|
@@ -205,14 +205,12 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
|
|
|
205
205
|
|
|
206
206
|
#### Parameter: `DispatchHandler`
|
|
207
207
|
|
|
208
|
-
* **
|
|
209
|
-
* **
|
|
210
|
-
* **
|
|
211
|
-
* **
|
|
212
|
-
* **
|
|
213
|
-
* **
|
|
214
|
-
* **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
|
|
215
|
-
* **onBodySent** `(chunk: string | Buffer | Uint8Array) => void` - Invoked when a body chunk is sent to the server. Not required. For a stream or iterable body this will be invoked for every chunk. For other body types, it will be invoked once after the body is sent.
|
|
208
|
+
* **onRequestStart** `(controller: DispatchController, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
|
|
209
|
+
* **onRequestUpgrade** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string[]>, socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
|
|
210
|
+
* **onResponseStart** `(controller: DispatchController, statusCode: number, statusMessage?: string, headers: Record<string, string | string []>) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
|
|
211
|
+
* **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests.
|
|
212
|
+
* **onResponseEnd** `(controller: DispatchController, trailers: Record<string, string | string[]>) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
|
|
213
|
+
* **onResponseError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
|
|
216
214
|
|
|
217
215
|
#### Example 1 - Dispatch GET request
|
|
218
216
|
|
|
@@ -1262,6 +1260,8 @@ The `cache` interceptor implements client-side response caching as described in
|
|
|
1262
1260
|
|
|
1263
1261
|
- `store` - The [`CacheStore`](/docs/docs/api/CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](/docs/docs/api/CacheStore.md#memorycachestore).
|
|
1264
1262
|
- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
|
|
1263
|
+
- `cacheByDefault` - The default expiration time to cache responses by if they don't have an explicit expiration. If this isn't present, responses without explicit expiration will not be cached. Default `undefined`.
|
|
1264
|
+
- `type` - The type of cache for Undici to act as. Can be `shared` or `private`. Default `shared`.
|
|
1265
1265
|
|
|
1266
1266
|
## Instance Events
|
|
1267
1267
|
|
|
@@ -16,7 +16,7 @@ Returns: `RedirectHandler`
|
|
|
16
16
|
|
|
17
17
|
### Parameters
|
|
18
18
|
|
|
19
|
-
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.
|
|
19
|
+
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection.
|
|
20
20
|
- **maxRedirections** `number` (required) - Maximum number of redirections allowed.
|
|
21
21
|
- **opts** `object` (required) - Options for handling redirection.
|
|
22
22
|
- **handler** `object` (required) - Handlers for different stages of the request lifecycle.
|
|
@@ -43,8 +43,8 @@ It represents the retry state for a given request.
|
|
|
43
43
|
|
|
44
44
|
### Parameter `RetryHandlers`
|
|
45
45
|
|
|
46
|
-
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.
|
|
47
|
-
- **handler** Extends [`Dispatch.
|
|
46
|
+
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
|
|
47
|
+
- **handler** Extends [`Dispatch.DispatchHandler`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
|
|
48
48
|
|
|
49
49
|
>__Note__: The `RetryHandler` does not retry over stateful bodies (e.g. streams, AsyncIterable) as those, once consumed, are left in a state that cannot be reutilized. For these situations the `RetryHandler` will identify
|
|
50
50
|
>the body as stateful and will not retry the request rejecting with the error `UND_ERR_REQ_RETRY`.
|
package/index.js
CHANGED
|
@@ -48,6 +48,15 @@ module.exports.cacheStores = {
|
|
|
48
48
|
MemoryCacheStore: require('./lib/cache/memory-cache-store')
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
try {
|
|
52
|
+
const SqliteCacheStore = require('./lib/cache/sqlite-cache-store')
|
|
53
|
+
module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.code !== 'ERR_UNKNOWN_BUILTIN_MODULE') {
|
|
56
|
+
throw err
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
module.exports.buildConnector = buildConnector
|
|
52
61
|
module.exports.errors = errors
|
|
53
62
|
module.exports.util = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Writable } = require('node:stream')
|
|
4
|
+
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
|
|
@@ -70,9 +71,7 @@ class MemoryCacheStore {
|
|
|
70
71
|
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
|
|
71
72
|
*/
|
|
72
73
|
get (key) {
|
|
73
|
-
|
|
74
|
-
throw new TypeError(`expected key to be object, got ${typeof key}`)
|
|
75
|
-
}
|
|
74
|
+
assertCacheKey(key)
|
|
76
75
|
|
|
77
76
|
const topLevelKey = `${key.origin}:${key.path}`
|
|
78
77
|
|
|
@@ -88,9 +87,11 @@ class MemoryCacheStore {
|
|
|
88
87
|
: {
|
|
89
88
|
statusMessage: entry.statusMessage,
|
|
90
89
|
statusCode: entry.statusCode,
|
|
91
|
-
|
|
90
|
+
headers: entry.headers,
|
|
92
91
|
body: entry.body,
|
|
92
|
+
vary: entry.vary ? entry.vary : undefined,
|
|
93
93
|
etag: entry.etag,
|
|
94
|
+
cacheControlDirectives: entry.cacheControlDirectives,
|
|
94
95
|
cachedAt: entry.cachedAt,
|
|
95
96
|
staleAt: entry.staleAt,
|
|
96
97
|
deleteAt: entry.deleteAt
|
|
@@ -103,12 +104,8 @@ class MemoryCacheStore {
|
|
|
103
104
|
* @returns {Writable | undefined}
|
|
104
105
|
*/
|
|
105
106
|
createWriteStream (key, val) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (typeof val !== 'object') {
|
|
110
|
-
throw new TypeError(`expected value to be object, got ${typeof val}`)
|
|
111
|
-
}
|
|
107
|
+
assertCacheKey(key)
|
|
108
|
+
assertCacheValue(val)
|
|
112
109
|
|
|
113
110
|
const topLevelKey = `${key.origin}:${key.path}`
|
|
114
111
|
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { DatabaseSync } = require('node:sqlite')
|
|
4
|
+
const { Writable } = require('stream')
|
|
5
|
+
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
|
|
6
|
+
|
|
7
|
+
const VERSION = 3
|
|
8
|
+
|
|
9
|
+
// 2gb
|
|
10
|
+
const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
|
|
14
|
+
* @implements {CacheStore}
|
|
15
|
+
*
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* id: Readonly<number>
|
|
18
|
+
* headers?: Record<string, string | string[]>
|
|
19
|
+
* vary?: string | object
|
|
20
|
+
* body: string
|
|
21
|
+
* } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
|
|
22
|
+
*/
|
|
23
|
+
module.exports = class SqliteCacheStore {
|
|
24
|
+
#maxEntrySize = MAX_ENTRY_SIZE
|
|
25
|
+
#maxCount = Infinity
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @type {import('node:sqlite').DatabaseSync}
|
|
29
|
+
*/
|
|
30
|
+
#db
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @type {import('node:sqlite').StatementSync}
|
|
34
|
+
*/
|
|
35
|
+
#getValuesQuery
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @type {import('node:sqlite').StatementSync}
|
|
39
|
+
*/
|
|
40
|
+
#updateValueQuery
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @type {import('node:sqlite').StatementSync}
|
|
44
|
+
*/
|
|
45
|
+
#insertValueQuery
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @type {import('node:sqlite').StatementSync}
|
|
49
|
+
*/
|
|
50
|
+
#deleteExpiredValuesQuery
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @type {import('node:sqlite').StatementSync}
|
|
54
|
+
*/
|
|
55
|
+
#deleteByUrlQuery
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @type {import('node:sqlite').StatementSync}
|
|
59
|
+
*/
|
|
60
|
+
#countEntriesQuery
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @type {import('node:sqlite').StatementSync}
|
|
64
|
+
*/
|
|
65
|
+
#deleteOldValuesQuery
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
|
|
69
|
+
*/
|
|
70
|
+
constructor (opts) {
|
|
71
|
+
if (opts) {
|
|
72
|
+
if (typeof opts !== 'object') {
|
|
73
|
+
throw new TypeError('SqliteCacheStore options must be an object')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (opts.maxEntrySize !== undefined) {
|
|
77
|
+
if (
|
|
78
|
+
typeof opts.maxEntrySize !== 'number' ||
|
|
79
|
+
!Number.isInteger(opts.maxEntrySize) ||
|
|
80
|
+
opts.maxEntrySize < 0
|
|
81
|
+
) {
|
|
82
|
+
throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
|
|
86
|
+
throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.#maxEntrySize = opts.maxEntrySize
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (opts.maxCount !== undefined) {
|
|
93
|
+
if (
|
|
94
|
+
typeof opts.maxCount !== 'number' ||
|
|
95
|
+
!Number.isInteger(opts.maxCount) ||
|
|
96
|
+
opts.maxCount < 0
|
|
97
|
+
) {
|
|
98
|
+
throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')
|
|
99
|
+
}
|
|
100
|
+
this.#maxCount = opts.maxCount
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.#db = new DatabaseSync(opts?.location ?? ':memory:')
|
|
105
|
+
|
|
106
|
+
this.#db.exec(`
|
|
107
|
+
CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
|
|
108
|
+
-- Data specific to us
|
|
109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
url TEXT NOT NULL,
|
|
111
|
+
method TEXT NOT NULL,
|
|
112
|
+
|
|
113
|
+
-- Data returned to the interceptor
|
|
114
|
+
body BUF NULL,
|
|
115
|
+
deleteAt INTEGER NOT NULL,
|
|
116
|
+
statusCode INTEGER NOT NULL,
|
|
117
|
+
statusMessage TEXT NOT NULL,
|
|
118
|
+
headers TEXT NULL,
|
|
119
|
+
cacheControlDirectives TEXT NULL,
|
|
120
|
+
etag TEXT NULL,
|
|
121
|
+
vary TEXT NULL,
|
|
122
|
+
cachedAt INTEGER NOT NULL,
|
|
123
|
+
staleAt INTEGER NOT NULL
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_url ON cacheInterceptorV${VERSION}(url);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_method ON cacheInterceptorV${VERSION}(method);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteAt ON cacheInterceptorV${VERSION}(deleteAt);
|
|
129
|
+
`)
|
|
130
|
+
|
|
131
|
+
this.#getValuesQuery = this.#db.prepare(`
|
|
132
|
+
SELECT
|
|
133
|
+
id,
|
|
134
|
+
body,
|
|
135
|
+
deleteAt,
|
|
136
|
+
statusCode,
|
|
137
|
+
statusMessage,
|
|
138
|
+
headers,
|
|
139
|
+
etag,
|
|
140
|
+
cacheControlDirectives,
|
|
141
|
+
vary,
|
|
142
|
+
cachedAt,
|
|
143
|
+
staleAt
|
|
144
|
+
FROM cacheInterceptorV${VERSION}
|
|
145
|
+
WHERE
|
|
146
|
+
url = ?
|
|
147
|
+
AND method = ?
|
|
148
|
+
ORDER BY
|
|
149
|
+
deleteAt ASC
|
|
150
|
+
`)
|
|
151
|
+
|
|
152
|
+
this.#updateValueQuery = this.#db.prepare(`
|
|
153
|
+
UPDATE cacheInterceptorV${VERSION} SET
|
|
154
|
+
body = ?,
|
|
155
|
+
deleteAt = ?,
|
|
156
|
+
statusCode = ?,
|
|
157
|
+
statusMessage = ?,
|
|
158
|
+
headers = ?,
|
|
159
|
+
etag = ?,
|
|
160
|
+
cacheControlDirectives = ?,
|
|
161
|
+
cachedAt = ?,
|
|
162
|
+
staleAt = ?,
|
|
163
|
+
deleteAt = ?
|
|
164
|
+
WHERE
|
|
165
|
+
id = ?
|
|
166
|
+
`)
|
|
167
|
+
|
|
168
|
+
this.#insertValueQuery = this.#db.prepare(`
|
|
169
|
+
INSERT INTO cacheInterceptorV${VERSION} (
|
|
170
|
+
url,
|
|
171
|
+
method,
|
|
172
|
+
body,
|
|
173
|
+
deleteAt,
|
|
174
|
+
statusCode,
|
|
175
|
+
statusMessage,
|
|
176
|
+
headers,
|
|
177
|
+
etag,
|
|
178
|
+
cacheControlDirectives,
|
|
179
|
+
vary,
|
|
180
|
+
cachedAt,
|
|
181
|
+
staleAt,
|
|
182
|
+
deleteAt
|
|
183
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
184
|
+
`)
|
|
185
|
+
|
|
186
|
+
this.#deleteByUrlQuery = this.#db.prepare(
|
|
187
|
+
`DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
this.#countEntriesQuery = this.#db.prepare(
|
|
191
|
+
`SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
this.#deleteExpiredValuesQuery = this.#db.prepare(
|
|
195
|
+
`DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
this.#deleteOldValuesQuery = this.#maxCount === Infinity
|
|
199
|
+
? null
|
|
200
|
+
: this.#db.prepare(`
|
|
201
|
+
DELETE FROM cacheInterceptorV${VERSION}
|
|
202
|
+
WHERE id IN (
|
|
203
|
+
SELECT
|
|
204
|
+
id
|
|
205
|
+
FROM cacheInterceptorV${VERSION}
|
|
206
|
+
ORDER BY cachedAt DESC
|
|
207
|
+
LIMIT ?
|
|
208
|
+
)
|
|
209
|
+
`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
close () {
|
|
213
|
+
this.#db.close()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
|
|
218
|
+
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
|
|
219
|
+
*/
|
|
220
|
+
get (key) {
|
|
221
|
+
assertCacheKey(key)
|
|
222
|
+
|
|
223
|
+
const value = this.#findValue(key)
|
|
224
|
+
|
|
225
|
+
if (!value) {
|
|
226
|
+
return undefined
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.GetResult}
|
|
231
|
+
*/
|
|
232
|
+
const result = {
|
|
233
|
+
body: Buffer.from(value.body),
|
|
234
|
+
statusCode: value.statusCode,
|
|
235
|
+
statusMessage: value.statusMessage,
|
|
236
|
+
headers: value.headers ? JSON.parse(value.headers) : undefined,
|
|
237
|
+
etag: value.etag ? value.etag : undefined,
|
|
238
|
+
vary: value.vary ?? undefined,
|
|
239
|
+
cacheControlDirectives: value.cacheControlDirectives
|
|
240
|
+
? JSON.parse(value.cacheControlDirectives)
|
|
241
|
+
: undefined,
|
|
242
|
+
cachedAt: value.cachedAt,
|
|
243
|
+
staleAt: value.staleAt,
|
|
244
|
+
deleteAt: value.deleteAt
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return result
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
|
|
252
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value
|
|
253
|
+
* @returns {Writable | undefined}
|
|
254
|
+
*/
|
|
255
|
+
createWriteStream (key, value) {
|
|
256
|
+
assertCacheKey(key)
|
|
257
|
+
assertCacheValue(value)
|
|
258
|
+
|
|
259
|
+
const url = this.#makeValueUrl(key)
|
|
260
|
+
let size = 0
|
|
261
|
+
/**
|
|
262
|
+
* @type {Buffer[] | null}
|
|
263
|
+
*/
|
|
264
|
+
const body = []
|
|
265
|
+
const store = this
|
|
266
|
+
|
|
267
|
+
return new Writable({
|
|
268
|
+
write (chunk, encoding, callback) {
|
|
269
|
+
if (typeof chunk === 'string') {
|
|
270
|
+
chunk = Buffer.from(chunk, encoding)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
size += chunk.byteLength
|
|
274
|
+
|
|
275
|
+
if (size < store.#maxEntrySize) {
|
|
276
|
+
body.push(chunk)
|
|
277
|
+
} else {
|
|
278
|
+
this.destroy()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
callback()
|
|
282
|
+
},
|
|
283
|
+
final (callback) {
|
|
284
|
+
const existingValue = store.#findValue(key, true)
|
|
285
|
+
if (existingValue) {
|
|
286
|
+
// Updating an existing response, let's overwrite it
|
|
287
|
+
store.#updateValueQuery.run(
|
|
288
|
+
Buffer.concat(body),
|
|
289
|
+
value.deleteAt,
|
|
290
|
+
value.statusCode,
|
|
291
|
+
value.statusMessage,
|
|
292
|
+
value.headers ? JSON.stringify(value.headers) : null,
|
|
293
|
+
value.etag ? value.etag : null,
|
|
294
|
+
value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
|
|
295
|
+
value.cachedAt,
|
|
296
|
+
value.staleAt,
|
|
297
|
+
value.deleteAt,
|
|
298
|
+
existingValue.id
|
|
299
|
+
)
|
|
300
|
+
} else {
|
|
301
|
+
store.#prune()
|
|
302
|
+
// New response, let's insert it
|
|
303
|
+
store.#insertValueQuery.run(
|
|
304
|
+
url,
|
|
305
|
+
key.method,
|
|
306
|
+
Buffer.concat(body),
|
|
307
|
+
value.deleteAt,
|
|
308
|
+
value.statusCode,
|
|
309
|
+
value.statusMessage,
|
|
310
|
+
value.headers ? JSON.stringify(value.headers) : null,
|
|
311
|
+
value.etag ? value.etag : null,
|
|
312
|
+
value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
|
|
313
|
+
value.vary ? JSON.stringify(value.vary) : null,
|
|
314
|
+
value.cachedAt,
|
|
315
|
+
value.staleAt,
|
|
316
|
+
value.deleteAt
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
callback()
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
|
|
327
|
+
*/
|
|
328
|
+
delete (key) {
|
|
329
|
+
if (typeof key !== 'object') {
|
|
330
|
+
throw new TypeError(`expected key to be object, got ${typeof key}`)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#prune () {
|
|
337
|
+
if (this.size <= this.#maxCount) {
|
|
338
|
+
return 0
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
{
|
|
342
|
+
const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
|
|
343
|
+
if (removed > 0) {
|
|
344
|
+
return removed
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
{
|
|
349
|
+
const removed = this.#deleteOldValuesQuery.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
|
|
350
|
+
if (removed > 0) {
|
|
351
|
+
return removed
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return 0
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Counts the number of rows in the cache
|
|
360
|
+
* @returns {Number}
|
|
361
|
+
*/
|
|
362
|
+
get size () {
|
|
363
|
+
const { total } = this.#countEntriesQuery.get()
|
|
364
|
+
return total
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
|
|
369
|
+
* @returns {string}
|
|
370
|
+
*/
|
|
371
|
+
#makeValueUrl (key) {
|
|
372
|
+
return `${key.origin}/${key.path}`
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
|
|
377
|
+
* @param {boolean} [canBeExpired=false]
|
|
378
|
+
* @returns {(SqliteStoreValue & { vary?: Record<string, string[]> }) | undefined}
|
|
379
|
+
*/
|
|
380
|
+
#findValue (key, canBeExpired = false) {
|
|
381
|
+
const url = this.#makeValueUrl(key)
|
|
382
|
+
const { headers, method } = key
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* @type {SqliteStoreValue[]}
|
|
386
|
+
*/
|
|
387
|
+
const values = this.#getValuesQuery.all(url, method)
|
|
388
|
+
|
|
389
|
+
if (values.length === 0) {
|
|
390
|
+
return undefined
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const now = Date.now()
|
|
394
|
+
for (const value of values) {
|
|
395
|
+
if (now >= value.deleteAt && !canBeExpired) {
|
|
396
|
+
return undefined
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let matches = true
|
|
400
|
+
|
|
401
|
+
if (value.vary) {
|
|
402
|
+
if (!headers) {
|
|
403
|
+
return undefined
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
value.vary = JSON.parse(value.vary)
|
|
407
|
+
|
|
408
|
+
for (const header in value.vary) {
|
|
409
|
+
if (!headerValueEquals(headers[header], value.vary[header])) {
|
|
410
|
+
matches = false
|
|
411
|
+
break
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (matches) {
|
|
417
|
+
return value
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return undefined
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {string|string[]|null|undefined} lhs
|
|
427
|
+
* @param {string|string[]|null|undefined} rhs
|
|
428
|
+
* @returns {boolean}
|
|
429
|
+
*/
|
|
430
|
+
function headerValueEquals (lhs, rhs) {
|
|
431
|
+
if (Array.isArray(lhs) && Array.isArray(rhs)) {
|
|
432
|
+
if (lhs.length !== rhs.length) {
|
|
433
|
+
return false
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
437
|
+
if (rhs.includes(lhs[i])) {
|
|
438
|
+
return false
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return true
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return lhs === rhs
|
|
446
|
+
}
|
package/lib/core/util.js
CHANGED
|
@@ -511,6 +511,11 @@ function assertRequestHandler (handler, method, upgrade) {
|
|
|
511
511
|
throw new InvalidArgumentError('handler must be an object')
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
if (typeof handler.onRequestStart === 'function') {
|
|
515
|
+
// TODO (fix): More checks...
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
514
519
|
if (typeof handler.onConnect !== 'function') {
|
|
515
520
|
throw new InvalidArgumentError('invalid onConnect method')
|
|
516
521
|
}
|
|
@@ -769,8 +769,19 @@ async function connectH1 (client, socket) {
|
|
|
769
769
|
client[kSocket] = socket
|
|
770
770
|
|
|
771
771
|
if (!llhttpInstance) {
|
|
772
|
+
const noop = () => {}
|
|
773
|
+
socket.on('error', noop)
|
|
772
774
|
llhttpInstance = await llhttpPromise
|
|
773
775
|
llhttpPromise = null
|
|
776
|
+
socket.off('error', noop)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (socket.errored) {
|
|
780
|
+
throw socket.errored
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (socket.destroyed) {
|
|
784
|
+
throw new SocketError('destroyed')
|
|
774
785
|
}
|
|
775
786
|
|
|
776
787
|
socket[kNoRef] = false
|
|
@@ -32,6 +32,8 @@ const {
|
|
|
32
32
|
|
|
33
33
|
const kOpenStreams = Symbol('open streams')
|
|
34
34
|
|
|
35
|
+
let extractBody
|
|
36
|
+
|
|
35
37
|
// Experimental
|
|
36
38
|
let h2ExperimentalWarned = false
|
|
37
39
|
|
|
@@ -201,11 +203,12 @@ function onHttp2SessionGoAway (errorCode) {
|
|
|
201
203
|
util.destroy(this[kSocket], err)
|
|
202
204
|
|
|
203
205
|
// Fail head of pipeline.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
if (client[kRunningIdx] < client[kQueue].length) {
|
|
207
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
208
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
209
|
+
util.errorRequest(client, request, err)
|
|
210
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
211
|
+
}
|
|
209
212
|
|
|
210
213
|
assert(client[kRunning] === 0)
|
|
211
214
|
|
|
@@ -279,7 +282,8 @@ function shouldSendContentLength (method) {
|
|
|
279
282
|
|
|
280
283
|
function writeH2 (client, request) {
|
|
281
284
|
const session = client[kHTTP2Session]
|
|
282
|
-
const {
|
|
285
|
+
const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
|
|
286
|
+
let { body } = request
|
|
283
287
|
|
|
284
288
|
if (upgrade) {
|
|
285
289
|
util.errorRequest(client, request, new Error('Upgrade not supported for H2'))
|
|
@@ -407,6 +411,16 @@ function writeH2 (client, request) {
|
|
|
407
411
|
|
|
408
412
|
let contentLength = util.bodyLength(body)
|
|
409
413
|
|
|
414
|
+
if (util.isFormDataLike(body)) {
|
|
415
|
+
extractBody ??= require('../web/fetch/body.js').extractBody
|
|
416
|
+
|
|
417
|
+
const [bodyStream, contentType] = extractBody(body)
|
|
418
|
+
headers['content-type'] = contentType
|
|
419
|
+
|
|
420
|
+
body = bodyStream.stream
|
|
421
|
+
contentLength = bodyStream.length
|
|
422
|
+
}
|
|
423
|
+
|
|
410
424
|
if (contentLength == null) {
|
|
411
425
|
contentLength = request.contentLength
|
|
412
426
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const Dispatcher = require('./dispatcher')
|
|
4
|
+
const UnwrapHandler = require('../handler/unwrap-handler')
|
|
4
5
|
const {
|
|
5
6
|
ClientDestroyedError,
|
|
6
7
|
ClientClosedError,
|
|
@@ -142,7 +143,7 @@ class DispatcherBase extends Dispatcher {
|
|
|
142
143
|
throw new ClientClosedError()
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
return this[kDispatch](opts, handler)
|
|
146
|
+
return this[kDispatch](opts, UnwrapHandler.unwrap(handler))
|
|
146
147
|
} catch (err) {
|
|
147
148
|
if (typeof handler.onError !== 'function') {
|
|
148
149
|
throw new InvalidArgumentError('invalid onError method')
|