diff --git a/packages/next/src/server/lib/dedupe-fetch.ts b/packages/next/src/server/lib/dedupe-fetch.ts index 0d893a64d301a..06e4db5355f96 100644 --- a/packages/next/src/server/lib/dedupe-fetch.ts +++ b/packages/next/src/server/lib/dedupe-fetch.ts @@ -25,8 +25,10 @@ function generateCacheKey(request: Request): string { } export function createDedupeFetch(originalFetch: typeof fetch) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- url is the cache key - const getCacheEntries = React.cache((url: string): Array => []) + const getCacheEntries = React.cache( + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- url is the cache key + (url: string): Array<[key: string, promise: Promise]> => [] + ) return function dedupeFetch( resource: URL | RequestInfo, @@ -42,6 +44,7 @@ export function createDedupeFetch(originalFetch: typeof fetch) { // Request constructor. return originalFetch(resource, options) } + // Normalize the Request let url: string let cacheKey: string @@ -73,30 +76,36 @@ export function createDedupeFetch(originalFetch: typeof fetch) { url = request.url } + // Get the cache entries for the given URL. const cacheEntries = getCacheEntries(url) - let match - if (cacheEntries.length === 0) { - // We pass the original arguments here in case normalizing the Request - // doesn't include all the options in this environment. - match = originalFetch(resource, options) - cacheEntries.push(cacheKey, match) - } else { - // We use an array as the inner data structure since it's lighter and - // we typically only expect to see one or two entries here. - for (let i = 0, l = cacheEntries.length; i < l; i += 2) { - const key = cacheEntries[i] - const value = cacheEntries[i + 1] - if (key === cacheKey) { - match = value - // I would've preferred a labelled break but lint says no. - return match.then((response: Response) => response.clone()) - } + + // Check if there is a cached entry for the given cache key. If there is, we + // return the cached response (cloned). This will keep the cached promise to + // remain unused and can be cloned on future requests. + for (const [key, promise] of cacheEntries) { + if (key !== cacheKey) { + continue } - match = originalFetch(resource, options) - cacheEntries.push(cacheKey, match) + + return promise.then((response: Response) => response.clone()) } - // We clone the response so that each time you call this you get a new read - // of the body so that it can be read multiple times. - return match.then((response) => response.clone()) + + // We pass the original arguments here in case normalizing the Request + // doesn't include all the options in this environment. + const original = originalFetch(resource, options) + + // We then clone the original response. We store this in the cache so that + // any future requests will be using this cloned response. + const cloned = original.then((response) => response.clone()) + + // Attach an empty catch here so we don't get a "unhandled promise + // rejection" warning + cloned.catch(() => {}) + + cacheEntries.push([cacheKey, cloned]) + + // Return the promise so that the caller can await it. We pass back the + // original promise. + return original } }