diff --git a/packages/next/src/server/resume-data-cache/cache-store.ts b/packages/next/src/server/resume-data-cache/cache-store.ts index c01a2aa0b95e9..c59b812b127b9 100644 --- a/packages/next/src/server/resume-data-cache/cache-store.ts +++ b/packages/next/src/server/resume-data-cache/cache-store.ts @@ -8,7 +8,10 @@ import type { CachedFetchValue } from '../response-cache/types' /** * A generic cache store type that provides a subset of Map functionality */ -type CacheStore = Pick, 'entries' | 'size' | 'get' | 'set'> +type CacheStore = Pick< + Map, + 'entries' | 'keys' | 'size' | 'get' | 'set' +> /** * A cache store specifically for fetch cache values diff --git a/test/e2e/app-dir/use-cache/app/rdc/page.tsx b/test/e2e/app-dir/use-cache/app/rdc/page.tsx new file mode 100644 index 0000000000000..1a668e45fea05 --- /dev/null +++ b/test/e2e/app-dir/use-cache/app/rdc/page.tsx @@ -0,0 +1,33 @@ +import { connection } from 'next/server' +import { Suspense } from 'react' + +async function outermost(id: string) { + 'use cache' + return id + middle('middle') +} + +async function middle(id: string) { + 'use cache' + return id + innermost('inner') +} + +async function innermost(id: string) { + 'use cache' + return id +} + +async function Dynamic() { + await connection() + return null +} + +export default async function Page() { + await outermost('outer') + await innermost('inner') + + return ( + + + + ) +} diff --git a/test/e2e/app-dir/use-cache/use-cache.test.ts b/test/e2e/app-dir/use-cache/use-cache.test.ts index c565d07466466..48b608b7f375c 100644 --- a/test/e2e/app-dir/use-cache/use-cache.test.ts +++ b/test/e2e/app-dir/use-cache/use-cache.test.ts @@ -3,6 +3,10 @@ import { retry, waitFor } from 'next-test-utils' import stripAnsi from 'strip-ansi' import { format } from 'util' import { BrowserInterface } from 'next-webdriver' +import { + createRenderResumeDataCache, + RenderResumeDataCache, +} from 'next/dist/server/resume-data-cache/resume-data-cache' const GENERIC_RSC_ERROR = 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.' @@ -596,6 +600,27 @@ describe('use-cache', () => { }) }) } + + if (isNextStart && process.env.__NEXT_EXPERIMENTAL_PPR === 'true') { + it('should exclude inner caches from the resume data cache (RDC)', async () => { + await next.fetch('/rdc') + + const resumeDataCache = extractResumeDataCacheFromPostponedState( + JSON.parse(await next.readFile('.next/server/app/rdc.meta')).postponed + ) + + const cacheKeys = Array.from(resumeDataCache.cache.keys()) + + // There should be no cache entry for the "middle" cache function, because + // it's only used inside another cache scope ("outer"). Whereas "inner" is + // also used inside a prerender scope (the page). Note: We're matching on + // the "id" args that are encoded into the respective cache keys. + expect(cacheKeys).toMatchObject([ + expect.stringContaining('["outer"]'), + expect.stringContaining('["inner"]'), + ]) + }) + } }) async function getSanitizedLogs(browser: BrowserInterface): Promise { @@ -607,3 +632,14 @@ async function getSanitizedLogs(browser: BrowserInterface): Promise { ) ) } + +function extractResumeDataCacheFromPostponedState( + state: string +): RenderResumeDataCache { + const postponedStringLengthMatch = state.match(/^([0-9]*):/)![1] + const postponedStringLength = parseInt(postponedStringLengthMatch) + + return createRenderResumeDataCache( + state.slice(postponedStringLengthMatch.length + postponedStringLength + 1) + ) +}