Skip to content

Commit

Permalink
fix: fixed dedupe response cloning
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Nov 28, 2024
1 parent 3a10396 commit fa638ca
Showing 1 changed file with 33 additions and 24 deletions.
57 changes: 33 additions & 24 deletions packages/next/src/server/lib/dedupe-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> => [])
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<Response>]> => []
)

return function dedupeFetch(
resource: URL | RequestInfo,
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}

0 comments on commit fa638ca

Please sign in to comment.