Skip to content

Commit 79f9d2f

Browse files
authored
fix: cache fixes (#3871)
* fix: cache fixes * Update cache-interceptor.d.ts * fixup * rfixupo * fixup * fixup * fixup * fixup
1 parent a781473 commit 79f9d2f

File tree

6 files changed

+83
-45
lines changed

6 files changed

+83
-45
lines changed

lib/cache/memory-cache-store.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ class MemoryCacheStore {
4242
this.#maxCount = opts.maxCount
4343
}
4444

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+
4556
if (opts.maxEntrySize !== undefined) {
4657
if (
4758
typeof opts.maxEntrySize !== 'number' ||
@@ -126,7 +137,7 @@ class MemoryCacheStore {
126137
store.#size += entry.size
127138
store.#count += 1
128139

129-
if (store.#size > store.#maxEntrySize || store.#count > store.#maxCount) {
140+
if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
130141
for (const [key, entries] of store.#entries) {
131142
for (const entry of entries.splice(0, entries.length / 2)) {
132143
store.#size -= entry.size

lib/cache/sqlite-cache-store.js

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ class SqliteCacheStore {
248248
const body = []
249249
const store = this
250250

251-
const writable = new Writable({
251+
return new Writable({
252252
write (chunk, encoding, callback) {
253253
if (typeof chunk === 'string') {
254254
chunk = Buffer.from(chunk, encoding)
@@ -267,9 +267,6 @@ class SqliteCacheStore {
267267
final (callback) {
268268
store.prune()
269269

270-
/**
271-
* @type {SqliteStoreValue | undefined}
272-
*/
273270
const existingValue = store.#findValue(key, true)
274271
if (existingValue) {
275272
// Updating an existing response, let's overwrite it
@@ -306,8 +303,6 @@ class SqliteCacheStore {
306303
callback()
307304
}
308305
})
309-
310-
return writable
311306
}
312307

313308
/**
@@ -373,14 +368,14 @@ class SqliteCacheStore {
373368
*/
374369
#findValue (key, canBeExpired = false) {
375370
const url = this.#makeValueUrl(key)
371+
const { headers, method } = key
376372

377373
/**
378374
* @type {SqliteStoreValue[]}
379375
*/
380-
const values = this.#getValuesQuery.all(url, key.method)
376+
const values = this.#getValuesQuery.all(url, method)
381377

382378
if (values.length === 0) {
383-
// No responses, let's just return early
384379
return undefined
385380
}
386381

@@ -393,16 +388,14 @@ class SqliteCacheStore {
393388
let matches = true
394389

395390
if (value.vary) {
396-
if (!key.headers) {
397-
// Request doesn't have headers so it can't fulfill the vary
398-
// requirements no matter what, let's return early
391+
if (!headers) {
399392
return undefined
400393
}
401394

402-
value.vary = JSON.parse(value.vary)
395+
const vary = JSON.parse(value.vary)
403396

404-
for (const header in value.vary) {
405-
if (key.headers[header] !== value.vary[header]) {
397+
for (const header in vary) {
398+
if (headerValueEquals(headers[header], vary[header])) {
406399
matches = false
407400
break
408401
}
@@ -418,6 +411,29 @@ class SqliteCacheStore {
418411
}
419412
}
420413

414+
/**
415+
* @param {string|string[]|null|undefined} lhs
416+
* @param {string|string[]|null|undefined} rhs
417+
* @returns {boolean}
418+
*/
419+
function headerValueEquals (lhs, rhs) {
420+
if (Array.isArray(lhs) && Array.isArray(rhs)) {
421+
if (lhs.length !== rhs.length) {
422+
return false
423+
}
424+
425+
for (let i = 0; i < lhs.length; i++) {
426+
if (rhs.includes(lhs[i])) {
427+
return false
428+
}
429+
}
430+
431+
return true
432+
}
433+
434+
return lhs === rhs
435+
}
436+
421437
/**
422438
* @param {Buffer[]} buffers
423439
* @returns {string[]}

lib/handler/cache-handler.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class CacheHandler extends DecoratorHandler {
3535
#writeStream
3636

3737
/**
38-
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
38+
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
3939
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
4040
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
4141
*/
@@ -106,17 +106,11 @@ class CacheHandler extends DecoratorHandler {
106106
const headers = util.parseHeaders(parsedRawHeaders)
107107

108108
const cacheControlHeader = headers['cache-control']
109-
const isCacheFull = typeof this.#store.isFull !== 'undefined'
110-
? this.#store.isFull
111-
: false
112-
113-
if (
114-
!cacheControlHeader ||
115-
isCacheFull
116-
) {
109+
if (!cacheControlHeader) {
117110
// Don't have the cache control header or the cache is full
118111
return downstreamOnHeaders()
119112
}
113+
120114
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
121115
if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
122116
return downstreamOnHeaders()
@@ -236,10 +230,7 @@ class CacheHandler extends DecoratorHandler {
236230
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
237231
*/
238232
function canCacheResponse (statusCode, headers, cacheControlDirectives) {
239-
if (
240-
statusCode !== 200 &&
241-
statusCode !== 307
242-
) {
233+
if (statusCode !== 200 && statusCode !== 307) {
243234
return false
244235
}
245236

@@ -309,7 +300,7 @@ function determineStaleAt (now, headers, cacheControlDirectives) {
309300
if (headers.expire && typeof headers.expire === 'string') {
310301
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
311302
const expiresDate = new Date(headers.expire)
312-
if (expiresDate instanceof Date && !isNaN(expiresDate)) {
303+
if (expiresDate instanceof Date && Number.isFinite(expiresDate.valueOf())) {
313304
return now + (Date.now() - expiresDate.getTime())
314305
}
315306
}
@@ -391,9 +382,7 @@ function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirect
391382
strippedHeaders.length -= offset
392383
}
393384

394-
return strippedHeaders
395-
? util.encodeRawHeaders(strippedHeaders)
396-
: rawHeaders
385+
return strippedHeaders ? util.encodeRawHeaders(strippedHeaders) : rawHeaders
397386
}
398387

399388
module.exports = CacheHandler

lib/util/cache.js

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,38 @@ function makeCacheKey (opts) {
1313
throw new Error('opts.origin is undefined')
1414
}
1515

16-
/**
17-
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
18-
*/
19-
const cacheKey = {
16+
/** @type {Record<string, string[] | string>} */
17+
let headers
18+
if (opts.headers == null) {
19+
headers = {}
20+
} else if (typeof opts.headers[Symbol.iterator] === 'function') {
21+
headers = {}
22+
for (const x of opts.headers) {
23+
if (!Array.isArray(x)) {
24+
throw new Error('opts.headers is not a valid header map')
25+
}
26+
const [key, val] = x
27+
if (typeof key !== 'string' || typeof val !== 'string') {
28+
throw new Error('opts.headers is not a valid header map')
29+
}
30+
headers[key] = val
31+
}
32+
} else if (typeof opts.headers === 'object') {
33+
headers = opts.headers
34+
} else {
35+
throw new Error('opts.headers is not an object')
36+
}
37+
38+
return {
2039
origin: opts.origin.toString(),
2140
method: opts.method,
2241
path: opts.path,
23-
headers: opts.headers
42+
headers
2443
}
25-
26-
return cacheKey
2744
}
2845

2946
/**
30-
* @param {any} value
47+
* @param {any} key
3148
*/
3249
function assertCacheKey (key) {
3350
if (typeof key !== 'object') {
@@ -299,10 +316,6 @@ function assertCacheStore (store, name = 'CacheStore') {
299316
throw new TypeError(`${name} needs to have a \`${fn}()\` function`)
300317
}
301318
}
302-
303-
if (typeof store.isFull !== 'undefined' && typeof store.isFull !== 'boolean') {
304-
throw new TypeError(`${name} needs a isFull getter with type boolean or undefined, current type: ${typeof store.isFull}`)
305-
}
306319
}
307320
/**
308321
* @param {unknown} methods

types/cache-interceptor.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export default CacheHandler
55
declare namespace CacheHandler {
66
export type CacheMethods = 'GET' | 'HEAD' | 'OPTIONS' | 'TRACE'
77

8+
export interface CacheHandlerOptions {
9+
store: CacheStore
10+
}
11+
812
export interface CacheOptions {
913
store?: CacheStore
1014

@@ -75,6 +79,11 @@ declare namespace CacheHandler {
7579
*/
7680
maxSize?: number
7781

82+
/**
83+
* @default Infinity
84+
*/
85+
maxEntrySize?: number
86+
7887
errorCallback?: (err: Error) => void
7988
}
8089

types/dispatcher.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ declare namespace Dispatcher {
103103
/** Default: `null` */
104104
body?: string | Buffer | Uint8Array | Readable | null | FormData;
105105
/** Default: `null` */
106-
headers?: IncomingHttpHeaders | string[] | Iterable<[string, string | string[] | undefined]> | null;
106+
headers?: Record<string, string | string[]> | IncomingHttpHeaders | string[] | Iterable<[string, string | string[] | undefined]> | null;
107107
/** Query string params to be embedded in the request URL. Default: `null` */
108108
query?: Record<string, any>;
109109
/** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */

0 commit comments

Comments
 (0)