undici 6.21.0 → 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 +27 -46
- package/docs/docs/api/Agent.md +14 -17
- package/docs/docs/api/BalancedPool.md +16 -16
- package/docs/docs/api/CacheStore.md +131 -0
- package/docs/docs/api/Client.md +12 -14
- package/docs/docs/api/Debug.md +1 -1
- package/docs/docs/api/Dispatcher.md +98 -194
- package/docs/docs/api/EnvHttpProxyAgent.md +12 -13
- 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 -16
- 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 -7
- package/lib/api/abort-signal.js +2 -0
- package/lib/api/api-connect.js +3 -1
- package/lib/api/api-pipeline.js +7 -6
- package/lib/api/api-request.js +33 -48
- package/lib/api/api-stream.js +39 -50
- package/lib/api/api-upgrade.js +5 -3
- package/lib/api/readable.js +235 -62
- 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/constants.js +35 -10
- package/lib/core/diagnostics.js +122 -128
- package/lib/core/errors.js +6 -6
- package/lib/core/request.js +13 -11
- package/lib/core/symbols.js +2 -1
- package/lib/core/tree.js +9 -1
- package/lib/core/util.js +237 -49
- package/lib/dispatcher/agent.js +3 -17
- package/lib/dispatcher/balanced-pool.js +5 -8
- package/lib/dispatcher/client-h1.js +379 -134
- package/lib/dispatcher/client-h2.js +173 -107
- package/lib/dispatcher/client.js +19 -32
- package/lib/dispatcher/dispatcher-base.js +6 -35
- package/lib/dispatcher/dispatcher.js +7 -24
- package/lib/dispatcher/fixed-queue.js +91 -49
- package/lib/dispatcher/pool-stats.js +2 -0
- package/lib/dispatcher/pool.js +3 -6
- package/lib/dispatcher/proxy-agent.js +3 -6
- package/lib/handler/cache-handler.js +393 -0
- package/lib/handler/cache-revalidation-handler.js +124 -0
- package/lib/handler/decorator-handler.js +27 -0
- package/lib/handler/redirect-handler.js +54 -59
- package/lib/handler/retry-handler.js +77 -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/dump.js +2 -2
- package/lib/interceptor/redirect.js +11 -14
- package/lib/interceptor/response-error.js +18 -7
- package/lib/llhttp/constants.d.ts +97 -0
- package/lib/llhttp/constants.js +412 -192
- package/lib/llhttp/constants.js.map +1 -0
- package/lib/llhttp/llhttp-wasm.js +11 -1
- package/lib/llhttp/llhttp_simd-wasm.js +11 -1
- package/lib/llhttp/utils.d.ts +2 -0
- package/lib/llhttp/utils.js +9 -9
- package/lib/llhttp/utils.js.map +1 -0
- package/lib/mock/mock-agent.js +5 -8
- package/lib/mock/mock-client.js +9 -4
- package/lib/mock/mock-errors.js +3 -1
- package/lib/mock/mock-interceptor.js +8 -6
- package/lib/mock/mock-pool.js +9 -4
- package/lib/mock/mock-symbols.js +3 -1
- package/lib/mock/mock-utils.js +29 -5
- package/lib/util/cache.js +360 -0
- package/lib/web/cache/cache.js +24 -21
- package/lib/web/cache/cachestorage.js +1 -1
- package/lib/web/cookies/index.js +29 -14
- package/lib/web/cookies/parse.js +8 -3
- package/lib/web/eventsource/eventsource-stream.js +9 -8
- package/lib/web/eventsource/eventsource.js +10 -6
- package/lib/web/fetch/body.js +43 -41
- package/lib/web/fetch/constants.js +12 -5
- package/lib/web/fetch/data-url.js +3 -3
- package/lib/web/fetch/formdata-parser.js +72 -45
- package/lib/web/fetch/formdata.js +65 -54
- package/lib/web/fetch/headers.js +118 -86
- package/lib/web/fetch/index.js +58 -67
- package/lib/web/fetch/request.js +136 -77
- package/lib/web/fetch/response.js +87 -56
- package/lib/web/fetch/util.js +259 -109
- package/lib/web/fetch/webidl.js +113 -68
- package/lib/web/websocket/connection.js +76 -147
- package/lib/web/websocket/constants.js +70 -10
- package/lib/web/websocket/events.js +4 -2
- package/lib/web/websocket/frame.js +45 -3
- package/lib/web/websocket/receiver.js +29 -33
- package/lib/web/websocket/sender.js +18 -13
- package/lib/web/websocket/stream/websocketerror.js +83 -0
- package/lib/web/websocket/stream/websocketstream.js +485 -0
- package/lib/web/websocket/util.js +128 -77
- package/lib/web/websocket/websocket.js +234 -135
- package/package.json +24 -36
- package/scripts/strip-comments.js +3 -1
- package/types/agent.d.ts +7 -7
- package/types/api.d.ts +24 -24
- package/types/balanced-pool.d.ts +11 -11
- package/types/cache-interceptor.d.ts +172 -0
- package/types/client.d.ts +11 -12
- package/types/cookies.d.ts +2 -0
- package/types/diagnostics-channel.d.ts +10 -10
- package/types/dispatcher.d.ts +113 -90
- package/types/env-http-proxy-agent.d.ts +2 -2
- package/types/errors.d.ts +53 -47
- package/types/fetch.d.ts +17 -16
- package/types/formdata.d.ts +7 -7
- package/types/global-dispatcher.d.ts +4 -4
- package/types/global-origin.d.ts +5 -5
- package/types/handlers.d.ts +7 -7
- package/types/header.d.ts +157 -1
- package/types/index.d.ts +44 -46
- package/types/interceptors.d.ts +25 -8
- package/types/mock-agent.d.ts +21 -18
- package/types/mock-client.d.ts +4 -4
- package/types/mock-errors.d.ts +3 -3
- package/types/mock-interceptor.d.ts +19 -19
- package/types/mock-pool.d.ts +4 -4
- package/types/patch.d.ts +0 -4
- package/types/pool-stats.d.ts +8 -8
- package/types/pool.d.ts +12 -12
- package/types/proxy-agent.d.ts +4 -4
- package/types/readable.d.ts +18 -15
- package/types/retry-agent.d.ts +1 -1
- package/types/retry-handler.d.ts +10 -10
- package/types/util.d.ts +3 -3
- package/types/utility.d.ts +7 -0
- package/types/webidl.d.ts +44 -6
- package/types/websocket.d.ts +34 -1
- package/docs/docs/api/DispatchInterceptor.md +0 -60
- package/lib/interceptor/redirect-interceptor.js +0 -21
- package/lib/mock/pluralizer.js +0 -29
- package/lib/web/cache/symbols.js +0 -5
- package/lib/web/fetch/file.js +0 -126
- package/lib/web/fetch/symbols.js +0 -9
- package/lib/web/fileapi/encoding.js +0 -290
- package/lib/web/fileapi/filereader.js +0 -344
- package/lib/web/fileapi/progressevent.js +0 -78
- package/lib/web/fileapi/symbols.js +0 -10
- package/lib/web/fileapi/util.js +0 -391
- package/lib/web/websocket/symbols.js +0 -12
- package/types/file.d.ts +0 -39
- package/types/filereader.d.ts +0 -54
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Writable } = require('node:stream')
|
|
4
|
+
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
|
|
8
|
+
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue} CacheValue
|
|
9
|
+
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
|
|
10
|
+
* @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult} GetResult
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @implements {CacheStore}
|
|
15
|
+
*/
|
|
16
|
+
class MemoryCacheStore {
|
|
17
|
+
#maxCount = Infinity
|
|
18
|
+
#maxSize = Infinity
|
|
19
|
+
#maxEntrySize = Infinity
|
|
20
|
+
|
|
21
|
+
#size = 0
|
|
22
|
+
#count = 0
|
|
23
|
+
#entries = new Map()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
|
|
27
|
+
*/
|
|
28
|
+
constructor (opts) {
|
|
29
|
+
if (opts) {
|
|
30
|
+
if (typeof opts !== 'object') {
|
|
31
|
+
throw new TypeError('MemoryCacheStore options must be an object')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (opts.maxCount !== undefined) {
|
|
35
|
+
if (
|
|
36
|
+
typeof opts.maxCount !== 'number' ||
|
|
37
|
+
!Number.isInteger(opts.maxCount) ||
|
|
38
|
+
opts.maxCount < 0
|
|
39
|
+
) {
|
|
40
|
+
throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
|
|
41
|
+
}
|
|
42
|
+
this.#maxCount = opts.maxCount
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (opts.maxSize !== undefined) {
|
|
46
|
+
if (
|
|
47
|
+
typeof opts.maxSize !== 'number' ||
|
|
48
|
+
!Number.isInteger(opts.maxSize) ||
|
|
49
|
+
opts.maxSize < 0
|
|
50
|
+
) {
|
|
51
|
+
throw new TypeError('MemoryCacheStore options.maxSize must be a non-negative integer')
|
|
52
|
+
}
|
|
53
|
+
this.#maxSize = opts.maxSize
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.maxEntrySize !== undefined) {
|
|
57
|
+
if (
|
|
58
|
+
typeof opts.maxEntrySize !== 'number' ||
|
|
59
|
+
!Number.isInteger(opts.maxEntrySize) ||
|
|
60
|
+
opts.maxEntrySize < 0
|
|
61
|
+
) {
|
|
62
|
+
throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
|
|
63
|
+
}
|
|
64
|
+
this.#maxEntrySize = opts.maxEntrySize
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
|
|
71
|
+
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
|
|
72
|
+
*/
|
|
73
|
+
get (key) {
|
|
74
|
+
assertCacheKey(key)
|
|
75
|
+
|
|
76
|
+
const topLevelKey = `${key.origin}:${key.path}`
|
|
77
|
+
|
|
78
|
+
const now = Date.now()
|
|
79
|
+
const entry = this.#entries.get(topLevelKey)?.find((entry) => (
|
|
80
|
+
entry.deleteAt > now &&
|
|
81
|
+
entry.method === key.method &&
|
|
82
|
+
(entry.vary == null || Object.keys(entry.vary).every(headerName => entry.vary[headerName] === key.headers?.[headerName]))
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
return entry == null
|
|
86
|
+
? undefined
|
|
87
|
+
: {
|
|
88
|
+
statusMessage: entry.statusMessage,
|
|
89
|
+
statusCode: entry.statusCode,
|
|
90
|
+
headers: entry.headers,
|
|
91
|
+
body: entry.body,
|
|
92
|
+
vary: entry.vary ? entry.vary : undefined,
|
|
93
|
+
etag: entry.etag,
|
|
94
|
+
cacheControlDirectives: entry.cacheControlDirectives,
|
|
95
|
+
cachedAt: entry.cachedAt,
|
|
96
|
+
staleAt: entry.staleAt,
|
|
97
|
+
deleteAt: entry.deleteAt
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
|
|
103
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val
|
|
104
|
+
* @returns {Writable | undefined}
|
|
105
|
+
*/
|
|
106
|
+
createWriteStream (key, val) {
|
|
107
|
+
assertCacheKey(key)
|
|
108
|
+
assertCacheValue(val)
|
|
109
|
+
|
|
110
|
+
const topLevelKey = `${key.origin}:${key.path}`
|
|
111
|
+
|
|
112
|
+
const store = this
|
|
113
|
+
const entry = { ...key, ...val, body: [], size: 0 }
|
|
114
|
+
|
|
115
|
+
return new Writable({
|
|
116
|
+
write (chunk, encoding, callback) {
|
|
117
|
+
if (typeof chunk === 'string') {
|
|
118
|
+
chunk = Buffer.from(chunk, encoding)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
entry.size += chunk.byteLength
|
|
122
|
+
|
|
123
|
+
if (entry.size >= store.#maxEntrySize) {
|
|
124
|
+
this.destroy()
|
|
125
|
+
} else {
|
|
126
|
+
entry.body.push(chunk)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
callback(null)
|
|
130
|
+
},
|
|
131
|
+
final (callback) {
|
|
132
|
+
let entries = store.#entries.get(topLevelKey)
|
|
133
|
+
if (!entries) {
|
|
134
|
+
entries = []
|
|
135
|
+
store.#entries.set(topLevelKey, entries)
|
|
136
|
+
}
|
|
137
|
+
entries.push(entry)
|
|
138
|
+
|
|
139
|
+
store.#size += entry.size
|
|
140
|
+
store.#count += 1
|
|
141
|
+
|
|
142
|
+
if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
|
|
143
|
+
for (const [key, entries] of store.#entries) {
|
|
144
|
+
for (const entry of entries.splice(0, entries.length / 2)) {
|
|
145
|
+
store.#size -= entry.size
|
|
146
|
+
store.#count -= 1
|
|
147
|
+
}
|
|
148
|
+
if (entries.length === 0) {
|
|
149
|
+
store.#entries.delete(key)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
callback(null)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {CacheKey} key
|
|
161
|
+
*/
|
|
162
|
+
delete (key) {
|
|
163
|
+
if (typeof key !== 'object') {
|
|
164
|
+
throw new TypeError(`expected key to be object, got ${typeof key}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const topLevelKey = `${key.origin}:${key.path}`
|
|
168
|
+
|
|
169
|
+
for (const entry of this.#entries.get(topLevelKey) ?? []) {
|
|
170
|
+
this.#size -= entry.size
|
|
171
|
+
this.#count -= 1
|
|
172
|
+
}
|
|
173
|
+
this.#entries.delete(topLevelKey)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = MemoryCacheStore
|
|
@@ -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/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
|
}
|