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.
Files changed (156) hide show
  1. package/README.md +27 -46
  2. package/docs/docs/api/Agent.md +14 -17
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +131 -0
  5. package/docs/docs/api/Client.md +12 -14
  6. package/docs/docs/api/Debug.md +1 -1
  7. package/docs/docs/api/Dispatcher.md +98 -194
  8. package/docs/docs/api/EnvHttpProxyAgent.md +12 -13
  9. package/docs/docs/api/MockAgent.md +5 -3
  10. package/docs/docs/api/MockClient.md +5 -5
  11. package/docs/docs/api/MockPool.md +4 -3
  12. package/docs/docs/api/Pool.md +15 -16
  13. package/docs/docs/api/PoolStats.md +1 -1
  14. package/docs/docs/api/ProxyAgent.md +3 -3
  15. package/docs/docs/api/RedirectHandler.md +1 -1
  16. package/docs/docs/api/RetryAgent.md +1 -1
  17. package/docs/docs/api/RetryHandler.md +4 -4
  18. package/docs/docs/api/WebSocket.md +46 -4
  19. package/docs/docs/api/api-lifecycle.md +11 -11
  20. package/docs/docs/best-practices/mocking-request.md +2 -2
  21. package/docs/docs/best-practices/proxy.md +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.js +23 -7
  24. package/lib/api/abort-signal.js +2 -0
  25. package/lib/api/api-connect.js +3 -1
  26. package/lib/api/api-pipeline.js +7 -6
  27. package/lib/api/api-request.js +33 -48
  28. package/lib/api/api-stream.js +39 -50
  29. package/lib/api/api-upgrade.js +5 -3
  30. package/lib/api/readable.js +235 -62
  31. package/lib/api/util.js +2 -0
  32. package/lib/cache/memory-cache-store.js +177 -0
  33. package/lib/cache/sqlite-cache-store.js +446 -0
  34. package/lib/core/constants.js +35 -10
  35. package/lib/core/diagnostics.js +122 -128
  36. package/lib/core/errors.js +6 -6
  37. package/lib/core/request.js +13 -11
  38. package/lib/core/symbols.js +2 -1
  39. package/lib/core/tree.js +9 -1
  40. package/lib/core/util.js +237 -49
  41. package/lib/dispatcher/agent.js +3 -17
  42. package/lib/dispatcher/balanced-pool.js +5 -8
  43. package/lib/dispatcher/client-h1.js +379 -134
  44. package/lib/dispatcher/client-h2.js +173 -107
  45. package/lib/dispatcher/client.js +19 -32
  46. package/lib/dispatcher/dispatcher-base.js +6 -35
  47. package/lib/dispatcher/dispatcher.js +7 -24
  48. package/lib/dispatcher/fixed-queue.js +91 -49
  49. package/lib/dispatcher/pool-stats.js +2 -0
  50. package/lib/dispatcher/pool.js +3 -6
  51. package/lib/dispatcher/proxy-agent.js +3 -6
  52. package/lib/handler/cache-handler.js +393 -0
  53. package/lib/handler/cache-revalidation-handler.js +124 -0
  54. package/lib/handler/decorator-handler.js +27 -0
  55. package/lib/handler/redirect-handler.js +54 -59
  56. package/lib/handler/retry-handler.js +77 -109
  57. package/lib/handler/unwrap-handler.js +96 -0
  58. package/lib/handler/wrap-handler.js +98 -0
  59. package/lib/interceptor/cache.js +350 -0
  60. package/lib/interceptor/dns.js +375 -0
  61. package/lib/interceptor/dump.js +2 -2
  62. package/lib/interceptor/redirect.js +11 -14
  63. package/lib/interceptor/response-error.js +18 -7
  64. package/lib/llhttp/constants.d.ts +97 -0
  65. package/lib/llhttp/constants.js +412 -192
  66. package/lib/llhttp/constants.js.map +1 -0
  67. package/lib/llhttp/llhttp-wasm.js +11 -1
  68. package/lib/llhttp/llhttp_simd-wasm.js +11 -1
  69. package/lib/llhttp/utils.d.ts +2 -0
  70. package/lib/llhttp/utils.js +9 -9
  71. package/lib/llhttp/utils.js.map +1 -0
  72. package/lib/mock/mock-agent.js +5 -8
  73. package/lib/mock/mock-client.js +9 -4
  74. package/lib/mock/mock-errors.js +3 -1
  75. package/lib/mock/mock-interceptor.js +8 -6
  76. package/lib/mock/mock-pool.js +9 -4
  77. package/lib/mock/mock-symbols.js +3 -1
  78. package/lib/mock/mock-utils.js +29 -5
  79. package/lib/util/cache.js +360 -0
  80. package/lib/web/cache/cache.js +24 -21
  81. package/lib/web/cache/cachestorage.js +1 -1
  82. package/lib/web/cookies/index.js +29 -14
  83. package/lib/web/cookies/parse.js +8 -3
  84. package/lib/web/eventsource/eventsource-stream.js +9 -8
  85. package/lib/web/eventsource/eventsource.js +10 -6
  86. package/lib/web/fetch/body.js +43 -41
  87. package/lib/web/fetch/constants.js +12 -5
  88. package/lib/web/fetch/data-url.js +3 -3
  89. package/lib/web/fetch/formdata-parser.js +72 -45
  90. package/lib/web/fetch/formdata.js +65 -54
  91. package/lib/web/fetch/headers.js +118 -86
  92. package/lib/web/fetch/index.js +58 -67
  93. package/lib/web/fetch/request.js +136 -77
  94. package/lib/web/fetch/response.js +87 -56
  95. package/lib/web/fetch/util.js +259 -109
  96. package/lib/web/fetch/webidl.js +113 -68
  97. package/lib/web/websocket/connection.js +76 -147
  98. package/lib/web/websocket/constants.js +70 -10
  99. package/lib/web/websocket/events.js +4 -2
  100. package/lib/web/websocket/frame.js +45 -3
  101. package/lib/web/websocket/receiver.js +29 -33
  102. package/lib/web/websocket/sender.js +18 -13
  103. package/lib/web/websocket/stream/websocketerror.js +83 -0
  104. package/lib/web/websocket/stream/websocketstream.js +485 -0
  105. package/lib/web/websocket/util.js +128 -77
  106. package/lib/web/websocket/websocket.js +234 -135
  107. package/package.json +24 -36
  108. package/scripts/strip-comments.js +3 -1
  109. package/types/agent.d.ts +7 -7
  110. package/types/api.d.ts +24 -24
  111. package/types/balanced-pool.d.ts +11 -11
  112. package/types/cache-interceptor.d.ts +172 -0
  113. package/types/client.d.ts +11 -12
  114. package/types/cookies.d.ts +2 -0
  115. package/types/diagnostics-channel.d.ts +10 -10
  116. package/types/dispatcher.d.ts +113 -90
  117. package/types/env-http-proxy-agent.d.ts +2 -2
  118. package/types/errors.d.ts +53 -47
  119. package/types/fetch.d.ts +17 -16
  120. package/types/formdata.d.ts +7 -7
  121. package/types/global-dispatcher.d.ts +4 -4
  122. package/types/global-origin.d.ts +5 -5
  123. package/types/handlers.d.ts +7 -7
  124. package/types/header.d.ts +157 -1
  125. package/types/index.d.ts +44 -46
  126. package/types/interceptors.d.ts +25 -8
  127. package/types/mock-agent.d.ts +21 -18
  128. package/types/mock-client.d.ts +4 -4
  129. package/types/mock-errors.d.ts +3 -3
  130. package/types/mock-interceptor.d.ts +19 -19
  131. package/types/mock-pool.d.ts +4 -4
  132. package/types/patch.d.ts +0 -4
  133. package/types/pool-stats.d.ts +8 -8
  134. package/types/pool.d.ts +12 -12
  135. package/types/proxy-agent.d.ts +4 -4
  136. package/types/readable.d.ts +18 -15
  137. package/types/retry-agent.d.ts +1 -1
  138. package/types/retry-handler.d.ts +10 -10
  139. package/types/util.d.ts +3 -3
  140. package/types/utility.d.ts +7 -0
  141. package/types/webidl.d.ts +44 -6
  142. package/types/websocket.d.ts +34 -1
  143. package/docs/docs/api/DispatchInterceptor.md +0 -60
  144. package/lib/interceptor/redirect-interceptor.js +0 -21
  145. package/lib/mock/pluralizer.js +0 -29
  146. package/lib/web/cache/symbols.js +0 -5
  147. package/lib/web/fetch/file.js +0 -126
  148. package/lib/web/fetch/symbols.js +0 -9
  149. package/lib/web/fileapi/encoding.js +0 -290
  150. package/lib/web/fileapi/filereader.js +0 -344
  151. package/lib/web/fileapi/progressevent.js +0 -78
  152. package/lib/web/fileapi/symbols.js +0 -10
  153. package/lib/web/fileapi/util.js +0 -391
  154. package/lib/web/websocket/symbols.js +0 -12
  155. package/types/file.d.ts +0 -39
  156. 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
+ }
@@ -1,10 +1,9 @@
1
1
  'use strict'
2
2
 
3
- /** @type {Record<string, string | undefined>} */
4
- const headerNameLowerCasedRecord = {}
5
-
6
- // https://developer.mozilla.org/docs/Web/HTTP/Headers
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
  }