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,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/connect.js
CHANGED
|
@@ -6,6 +6,8 @@ const util = require('./util')
|
|
|
6
6
|
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
|
|
7
7
|
const timers = require('../util/timers')
|
|
8
8
|
|
|
9
|
+
function noop () {}
|
|
10
|
+
|
|
9
11
|
let tls // include tls conditionally since it is not always available
|
|
10
12
|
|
|
11
13
|
// TODO: session re-use does not wait for the first
|
|
@@ -92,9 +94,11 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
92
94
|
servername = servername || options.servername || util.getServerName(host) || null
|
|
93
95
|
|
|
94
96
|
const sessionKey = servername || hostname
|
|
97
|
+
assert(sessionKey)
|
|
98
|
+
|
|
95
99
|
const session = customSession || sessionCache.get(sessionKey) || null
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
port = port || 443
|
|
98
102
|
|
|
99
103
|
socket = tls.connect({
|
|
100
104
|
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
|
|
@@ -105,7 +109,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
105
109
|
// TODO(HTTP/2): Add support for h2c
|
|
106
110
|
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
|
|
107
111
|
socket: httpSocket, // upgrade socket connection
|
|
108
|
-
port
|
|
112
|
+
port,
|
|
109
113
|
host: hostname
|
|
110
114
|
})
|
|
111
115
|
|
|
@@ -116,11 +120,14 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
116
120
|
})
|
|
117
121
|
} else {
|
|
118
122
|
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
|
|
123
|
+
|
|
124
|
+
port = port || 80
|
|
125
|
+
|
|
119
126
|
socket = net.connect({
|
|
120
127
|
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
|
|
121
128
|
...options,
|
|
122
129
|
localAddress,
|
|
123
|
-
port
|
|
130
|
+
port,
|
|
124
131
|
host: hostname
|
|
125
132
|
})
|
|
126
133
|
}
|
|
@@ -131,12 +138,12 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
131
138
|
socket.setKeepAlive(true, keepAliveInitialDelay)
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
const
|
|
141
|
+
const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
|
|
135
142
|
|
|
136
143
|
socket
|
|
137
144
|
.setNoDelay(true)
|
|
138
145
|
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
|
|
139
|
-
|
|
146
|
+
queueMicrotask(clearConnectTimeout)
|
|
140
147
|
|
|
141
148
|
if (callback) {
|
|
142
149
|
const cb = callback
|
|
@@ -145,7 +152,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
145
152
|
}
|
|
146
153
|
})
|
|
147
154
|
.on('error', function (err) {
|
|
148
|
-
|
|
155
|
+
queueMicrotask(clearConnectTimeout)
|
|
149
156
|
|
|
150
157
|
if (callback) {
|
|
151
158
|
const cb = callback
|
|
@@ -158,50 +165,75 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
/**
|
|
169
|
+
* @param {WeakRef<net.Socket>} socketWeakRef
|
|
170
|
+
* @param {object} opts
|
|
171
|
+
* @param {number} opts.timeout
|
|
172
|
+
* @param {string} opts.hostname
|
|
173
|
+
* @param {number} opts.port
|
|
174
|
+
* @returns {() => void}
|
|
175
|
+
*/
|
|
161
176
|
const setupConnectTimeout = process.platform === 'win32'
|
|
162
|
-
? (
|
|
163
|
-
if (!timeout) {
|
|
164
|
-
return
|
|
177
|
+
? (socketWeakRef, opts) => {
|
|
178
|
+
if (!opts.timeout) {
|
|
179
|
+
return noop
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
let s1 = null
|
|
168
183
|
let s2 = null
|
|
169
|
-
const
|
|
184
|
+
const fastTimer = timers.setFastTimeout(() => {
|
|
170
185
|
// setImmediate is added to make sure that we prioritize socket error events over timeouts
|
|
171
186
|
s1 = setImmediate(() => {
|
|
172
187
|
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
|
|
173
|
-
s2 = setImmediate(() => onConnectTimeout(
|
|
188
|
+
s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
|
|
174
189
|
})
|
|
175
|
-
}, timeout)
|
|
190
|
+
}, opts.timeout)
|
|
176
191
|
return () => {
|
|
177
|
-
timers.
|
|
192
|
+
timers.clearFastTimeout(fastTimer)
|
|
178
193
|
clearImmediate(s1)
|
|
179
194
|
clearImmediate(s2)
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
|
-
: (
|
|
183
|
-
if (!timeout) {
|
|
184
|
-
return
|
|
197
|
+
: (socketWeakRef, opts) => {
|
|
198
|
+
if (!opts.timeout) {
|
|
199
|
+
return noop
|
|
185
200
|
}
|
|
186
201
|
|
|
187
202
|
let s1 = null
|
|
188
|
-
const
|
|
203
|
+
const fastTimer = timers.setFastTimeout(() => {
|
|
189
204
|
// setImmediate is added to make sure that we prioritize socket error events over timeouts
|
|
190
205
|
s1 = setImmediate(() => {
|
|
191
|
-
onConnectTimeout(
|
|
206
|
+
onConnectTimeout(socketWeakRef.deref(), opts)
|
|
192
207
|
})
|
|
193
|
-
}, timeout)
|
|
208
|
+
}, opts.timeout)
|
|
194
209
|
return () => {
|
|
195
|
-
timers.
|
|
210
|
+
timers.clearFastTimeout(fastTimer)
|
|
196
211
|
clearImmediate(s1)
|
|
197
212
|
}
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
|
|
215
|
+
/**
|
|
216
|
+
* @param {net.Socket} socket
|
|
217
|
+
* @param {object} opts
|
|
218
|
+
* @param {number} opts.timeout
|
|
219
|
+
* @param {string} opts.hostname
|
|
220
|
+
* @param {number} opts.port
|
|
221
|
+
*/
|
|
222
|
+
function onConnectTimeout (socket, opts) {
|
|
223
|
+
// The socket could be already garbage collected
|
|
224
|
+
if (socket == null) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
let message = 'Connect Timeout Error'
|
|
202
229
|
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
|
|
203
|
-
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')}
|
|
230
|
+
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
|
|
231
|
+
} else {
|
|
232
|
+
message += ` (attempted address: ${opts.hostname}:${opts.port},`
|
|
204
233
|
}
|
|
234
|
+
|
|
235
|
+
message += ` timeout: ${opts.timeout}ms)`
|
|
236
|
+
|
|
205
237
|
util.destroy(socket, new ConnectTimeoutError(message))
|
|
206
238
|
}
|
|
207
239
|
|
package/lib/core/constants.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const wellknownHeaderNames = [
|
|
3
|
+
/**
|
|
4
|
+
* @see https://developer.mozilla.org/docs/Web/HTTP/Headers
|
|
5
|
+
*/
|
|
6
|
+
const wellknownHeaderNames = /** @type {const} */ ([
|
|
8
7
|
'Accept',
|
|
9
8
|
'Accept-Encoding',
|
|
10
9
|
'Accept-Language',
|
|
@@ -100,7 +99,35 @@ const wellknownHeaderNames = [
|
|
|
100
99
|
'X-Powered-By',
|
|
101
100
|
'X-Requested-With',
|
|
102
101
|
'X-XSS-Protection'
|
|
103
|
-
]
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
/** @type {Record<typeof wellknownHeaderNames[number]|Lowercase<typeof wellknownHeaderNames[number]>, string>} */
|
|
105
|
+
const headerNameLowerCasedRecord = {}
|
|
106
|
+
|
|
107
|
+
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
|
108
|
+
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
|
|
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
|
+
}
|
|
104
131
|
|
|
105
132
|
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
|
106
133
|
const key = wellknownHeaderNames[i]
|
|
@@ -109,10 +136,8 @@ for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
|
|
109
136
|
lowerCasedKey
|
|
110
137
|
}
|
|
111
138
|
|
|
112
|
-
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
|
113
|
-
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
|
|
114
|
-
|
|
115
139
|
module.exports = {
|
|
116
140
|
wellknownHeaderNames,
|
|
117
|
-
headerNameLowerCasedRecord
|
|
141
|
+
headerNameLowerCasedRecord,
|
|
142
|
+
getHeaderNameAsBuffer
|
|
118
143
|
}
|