From d292e084a3bb27b55ee01f30d28e22f5f5adce63 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 13 Nov 2024 21:24:59 +0100 Subject: [PATCH] fix: memory store Simplify and fix memory leak --- lib/cache/memory-cache-store.js | 123 +++++++------------------------- 1 file changed, 24 insertions(+), 99 deletions(-) diff --git a/lib/cache/memory-cache-store.js b/lib/cache/memory-cache-store.js index 6a00568ca22..67b32f6962f 100644 --- a/lib/cache/memory-cache-store.js +++ b/lib/cache/memory-cache-store.js @@ -103,11 +103,7 @@ class MemoryCacheStore { const values = this.#getValuesForRequest(key, true) - /** - * @type {(MemoryStoreValue & { index: number }) | undefined} - */ let value = this.#findValue(key, values) - let valueIndex = value?.index if (!value) { // The value doesn't already exist, meaning we haven't cached this // response before. Let's assign it a value and insert it into our data @@ -118,53 +114,12 @@ class MemoryCacheStore { return undefined } - this.#entryCount++ - - value = { - locked: true, - opts + if (this.#entryCount++ > this.#maxEntries) { + this.#prune() } - // We want to sort our responses in decending order by their deleteAt - // timestamps so that deleting expired responses is faster - if ( - values.length === 0 || - opts.deleteAt < values[values.length - 1].deleteAt - ) { - // Our value is either the only response for this path or our deleteAt - // time is sooner than all the other responses - values.push(value) - valueIndex = values.length - 1 - } else if (opts.deleteAt >= values[0].deleteAt) { - // Our deleteAt is later than everyone elses - values.unshift(value) - valueIndex = 0 - } else { - // We're neither in the front or the end, let's just binary search to - // find our stop we need to be in - let startIndex = 0 - let endIndex = values.length - while (true) { - if (startIndex === endIndex) { - values.splice(startIndex, 0, value) - break - } - - const middleIndex = Math.floor((startIndex + endIndex) / 2) - const middleValue = values[middleIndex] - if (opts.deleteAt === middleIndex) { - values.splice(middleIndex, 0, value) - valueIndex = middleIndex - break - } else if (opts.deleteAt > middleValue.opts.deleteAt) { - endIndex = middleIndex - continue - } else { - startIndex = middleIndex - continue - } - } - } + value = { locked: true, opts } + values.push(value) } else { // Check if there's already another request writing to the value or // a request reading from it @@ -180,7 +135,7 @@ class MemoryCacheStore { /** * @type {Buffer[] | null} */ - let body = key.method !== 'HEAD' ? [] : null + let body = [] const maxEntrySize = this.#maxEntrySize const writable = new Writable({ @@ -202,7 +157,6 @@ class MemoryCacheStore { if (currentSize >= maxEntrySize) { body = null this.end() - shiftAtIndex(values, valueIndex) return callback() } @@ -263,63 +217,34 @@ class MemoryCacheStore { * to respond with. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req * @param {MemoryStoreValue[]} values - * @returns {(MemoryStoreValue & { index: number }) | undefined} + * @returns {(MemoryStoreValue) | undefined} */ #findValue (req, values) { - /** - * @type {MemoryStoreValue | undefined} - */ - let value const now = Date.now() - for (let i = values.length - 1; i >= 0; i--) { - const current = values[i] - const currentCacheValue = current.opts - if (now >= currentCacheValue.deleteAt) { - // We've reached expired values, let's delete them - this.#entryCount -= values.length - i - values.length = i - break - } - - let matches = true - - if (currentCacheValue.vary) { - if (!req.headers) { - matches = false - break - } + return values.find(({ opts: { deleteAt, vary }, body }) => ( + body != null && + deleteAt > now && + (!vary || Object.keys(vary).every(key => vary[key] === req.headers?.[key])) + )) + } - for (const key in currentCacheValue.vary) { - if (currentCacheValue.vary[key] !== req.headers[key]) { - matches = false - break + #prune () { + const now = Date.now() + for (const [key, cachedPaths] of this.#data) { + for (const [method, prev] of cachedPaths) { + const next = prev.filter(({ opts, body }) => body == null || opts.deleteAt > now) + if (next.length === 0) { + cachedPaths.delete(method) + if (cachedPaths.size === 0) { + this.#data.delete(key) } + } else if (next.length !== prev.length) { + this.#entryCount -= prev.length - next.length + cachedPaths.set(method, next) } } - - if (matches) { - value = { - ...current, - index: i - } - break - } } - - return value } } -/** - * @param {any[]} array Array to modify - * @param {number} idx Index to delete - */ -function shiftAtIndex (array, idx) { - for (let i = idx + 1; idx < array.length; i++) { - array[i - 1] = array[i] - } - - array.length-- -} - module.exports = MemoryCacheStore