diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index d6152bbd0a527..5846c72f79c61 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -254,30 +254,25 @@ export function createPatchedFetcher( `fetch ${input.toString()}` ) - if ( + const revalidateStore = workUnitStore && (workUnitStore.type === 'cache' || workUnitStore.type === 'prerender' || workUnitStore.type === 'prerender-ppr' || workUnitStore.type === 'prerender-legacy') - ) { + ? workUnitStore + : undefined + + if (revalidateStore) { if (Array.isArray(tags)) { // Collect tags onto parent caches or parent prerenders. const collectedTags = - workUnitStore.tags ?? (workUnitStore.tags = []) + revalidateStore.tags ?? (revalidateStore.tags = []) for (const tag of tags) { if (!collectedTags.includes(tag)) { collectedTags.push(tag) } } - - // Add tags of the current cache scope to the locally collected tags - // for this fetch call. - for (const tag of collectedTags) { - if (!tags.includes(tag)) { - tags.push(tag) - } - } } } @@ -286,12 +281,10 @@ export function createPatchedFetcher( ? [] : workUnitStore.implicitTags - // Inside unstable-cache or "use cache", we treat it the same as - // force-no-store on the page. + // Inside unstable-cache we treat it the same as force-no-store on the + // page. const pageFetchCacheMode = - workUnitStore && - (workUnitStore.type === 'unstable-cache' || - workUnitStore.type === 'cache') + workUnitStore && workUnitStore.type === 'unstable-cache' ? 'force-no-store' : workStore.fetchCache @@ -359,15 +352,6 @@ export function createPatchedFetcher( getRequestMeta('method')?.toLowerCase() || 'get' ) - const revalidateStore = - workUnitStore && - (workUnitStore.type === 'cache' || - workUnitStore.type === 'prerender' || - workUnitStore.type === 'prerender-ppr' || - workUnitStore.type === 'prerender-legacy') - ? workUnitStore - : undefined - /** * We automatically disable fetch caching under the following conditions: * - Fetch cache configs are not set. Specifically: @@ -515,7 +499,9 @@ export function createPatchedFetcher( } } - if (revalidateStore) { + // We only want to set the revalidate store's revalidate time if it + // was explicitly set for the fetch call, i.e. currentFetchRevalidate. + if (revalidateStore && currentFetchRevalidate === finalRevalidate) { revalidateStore.revalidate = finalRevalidate } } diff --git a/test/e2e/app-dir/use-cache/app/cache-tag/button.tsx b/test/e2e/app-dir/use-cache/app/cache-tag/button.tsx deleted file mode 100644 index c08ae38faed7e..0000000000000 --- a/test/e2e/app-dir/use-cache/app/cache-tag/button.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' -import { revalidateTag } from 'next/cache' - -export function RevalidateButtons() { - return ( -
- - - -
- ) -} diff --git a/test/e2e/app-dir/use-cache/app/cache-tag/buttons.tsx b/test/e2e/app-dir/use-cache/app/cache-tag/buttons.tsx new file mode 100644 index 0000000000000..59ec681bf6ceb --- /dev/null +++ b/test/e2e/app-dir/use-cache/app/cache-tag/buttons.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { revalidatePath, revalidateTag } from 'next/cache' + +export function RevalidateButtons() { + return ( +
+ {' '} + {' '} + {' '} + {' '} + {' '} + +
+ ) +} diff --git a/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx b/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx index 603ce92733547..ad50c97db7db4 100644 --- a/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx +++ b/test/e2e/app-dir/use-cache/app/cache-tag/page.tsx @@ -1,28 +1,57 @@ import React from 'react' import { unstable_cacheTag as cacheTag } from 'next/cache' -import { RevalidateButtons } from './button' +import { RevalidateButtons } from './buttons' -async function getCachedWithTag(tag: string) { +async function getCachedWithTag({ + tag, + fetchCache, +}: { + tag: string + fetchCache?: 'force' | 'revalidate' +}) { 'use cache' cacheTag(tag, 'c') + // If `force-cache` or `revalidate` is used for the fetch call, it creates + // basically an inner cache, and revalidating tag 'c' won't revalidate the + // fetch cache. If both are not used, the fetch is not cached at all in the + // fetch cache, and is included in the cached result of `getCachedWithTag` + // instead, thus also affected by revalidating 'c'. const response = await fetch( - `https://next-data-api-endpoint.vercel.app/api/random?tag=${tag}` + `https://next-data-api-endpoint.vercel.app/api/random?tag=${tag}`, + { + cache: fetchCache === 'force' ? 'force-cache' : undefined, + next: { revalidate: fetchCache === 'revalidate' ? 42 : undefined }, + } ) - return response.text() + const fetchedValue = await response.text() + + return [Math.random(), fetchedValue] } export default async function Page() { - const x = await getCachedWithTag('a') - const y = await getCachedWithTag('b') + const a = await getCachedWithTag({ tag: 'a' }) + const b = await getCachedWithTag({ tag: 'b' }) + + const [f1, f2] = await getCachedWithTag({ + tag: 'f', + fetchCache: 'force', + }) + + const [r1, r2] = await getCachedWithTag({ + tag: 'r', + fetchCache: 'revalidate', + }) return (
-

{x}

-
-

{y}

-
+

[a, c] {a.join(' ')}

+

[b, c] {b.join(' ')}

+

[f, c] {f1}

+

[-] {f2}

+

[r, c] {r1}

+

[-] {r2}

) 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 c772e8d11e0d2..35e001ea895f5 100644 --- a/test/e2e/app-dir/use-cache/use-cache.test.ts +++ b/test/e2e/app-dir/use-cache/use-cache.test.ts @@ -179,30 +179,93 @@ describe('use-cache', () => { if (!isNextDeploy) { it('should update after revalidateTag correctly', async () => { const browser = await next.browser('/cache-tag') + const initial = await browser.elementByCss('#a').text() - const initialX = await browser.elementByCss('#x').text() - const initialY = await browser.elementByCss('#y').text() - let updatedX: string | undefined - let updatedY: string | undefined + // Bust the ISR cache first, to populate the in-memory cache for the + // subsequent revalidateTag calls. + await browser.elementByCss('#revalidate-path').click() + await retry(async () => { + expect(await browser.elementByCss('#a').text()).not.toBe(initial) + }) + + let valueA = await browser.elementByCss('#a').text() + let valueB = await browser.elementByCss('#b').text() + let valueF1 = await browser.elementByCss('#f1').text() + let valueF2 = await browser.elementByCss('#f2').text() + let valueR1 = await browser.elementByCss('#r1').text() + let valueR2 = await browser.elementByCss('#r2').text() await browser.elementByCss('#revalidate-a').click() await retry(async () => { - updatedX = await browser.elementByCss('#x').text() - expect(updatedX).not.toBe(initialX) - expect(await browser.elementByCss('#y').text()).toBe(initialY) + expect(await browser.elementByCss('#a').text()).not.toBe(valueA) + expect(await browser.elementByCss('#b').text()).toBe(valueB) + expect(await browser.elementByCss('#f1').text()).toBe(valueF1) + expect(await browser.elementByCss('#f2').text()).toBe(valueF2) + expect(await browser.elementByCss('#r1').text()).toBe(valueR1) + expect(await browser.elementByCss('#r2').text()).toBe(valueR2) }) + valueA = await browser.elementByCss('#a').text() + await browser.elementByCss('#revalidate-b').click() await retry(async () => { - updatedY = await browser.elementByCss('#y').text() - expect(updatedY).not.toBe(initialY) - expect(await browser.elementByCss('#x').text()).toBe(updatedX) + expect(await browser.elementByCss('#a').text()).toBe(valueA) + expect(await browser.elementByCss('#b').text()).not.toBe(valueB) + expect(await browser.elementByCss('#f1').text()).toBe(valueF1) + expect(await browser.elementByCss('#f2').text()).toBe(valueF2) + expect(await browser.elementByCss('#r1').text()).toBe(valueR1) + expect(await browser.elementByCss('#r2').text()).toBe(valueR2) }) + valueB = await browser.elementByCss('#b').text() + await browser.elementByCss('#revalidate-c').click() await retry(async () => { - expect(await browser.elementByCss('#x').text()).not.toBe(updatedX) - expect(await browser.elementByCss('#y').text()).not.toBe(updatedY) + expect(await browser.elementByCss('#a').text()).not.toBe(valueA) + expect(await browser.elementByCss('#b').text()).not.toBe(valueB) + expect(await browser.elementByCss('#f1').text()).not.toBe(valueF1) + expect(await browser.elementByCss('#f2').text()).toBe(valueF2) + expect(await browser.elementByCss('#r1').text()).not.toBe(valueR1) + expect(await browser.elementByCss('#r2').text()).toBe(valueR2) + }) + + valueA = await browser.elementByCss('#a').text() + valueB = await browser.elementByCss('#b').text() + valueF1 = await browser.elementByCss('#f1').text() + valueR1 = await browser.elementByCss('#r1').text() + + await browser.elementByCss('#revalidate-f').click() + await retry(async () => { + expect(await browser.elementByCss('#a').text()).toBe(valueA) + expect(await browser.elementByCss('#b').text()).toBe(valueB) + expect(await browser.elementByCss('#f1').text()).not.toBe(valueF1) + expect(await browser.elementByCss('#f2').text()).toBe(valueF2) + expect(await browser.elementByCss('#r1').text()).toBe(valueR1) + expect(await browser.elementByCss('#r2').text()).toBe(valueR2) + }) + + valueF1 = await browser.elementByCss('#f1').text() + + await browser.elementByCss('#revalidate-r').click() + await retry(async () => { + expect(await browser.elementByCss('#a').text()).toBe(valueA) + expect(await browser.elementByCss('#b').text()).toBe(valueB) + expect(await browser.elementByCss('#f1').text()).toBe(valueF1) + expect(await browser.elementByCss('#f2').text()).toBe(valueF2) + expect(await browser.elementByCss('#r1').text()).not.toBe(valueR1) + expect(await browser.elementByCss('#r2').text()).toBe(valueR2) + }) + + valueR1 = await browser.elementByCss('#r1').text() + + await browser.elementByCss('#revalidate-path').click() + await retry(async () => { + expect(await browser.elementByCss('#a').text()).not.toBe(valueA) + expect(await browser.elementByCss('#b').text()).not.toBe(valueB) + expect(await browser.elementByCss('#f1').text()).not.toBe(valueF1) + expect(await browser.elementByCss('#f2').text()).not.toBe(valueF2) + expect(await browser.elementByCss('#r1').text()).not.toBe(valueR1) + expect(await browser.elementByCss('#r2').text()).not.toBe(valueR2) }) }) } @@ -217,6 +280,12 @@ describe('use-cache', () => { expect( prerenderManifest.routes['/cache-life'].initialRevalidateSeconds ).toBe(100) + + // The revalidate config from the fetch call should lower the revalidate + // config for the page. + expect( + prerenderManifest.routes['/cache-tag'].initialRevalidateSeconds + ).toBe(42) }) it('should match the expected stale config in the page header', async () => { @@ -230,7 +299,7 @@ describe('use-cache', () => { const meta = JSON.parse( await next.readFile('.next/server/app/cache-tag.meta') ) - expect(meta.headers['x-next-cache-tags']).toContain('a,c,b') + expect(meta.headers['x-next-cache-tags']).toContain('a,c,b,f,r') }) }