diff --git a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts index d275d7147ebc5..2c6783f8a5a5b 100644 --- a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts @@ -28,6 +28,21 @@ export default class FetchCache implements CacheHandler { private cacheEndpoint?: string private debug: boolean + private hasMatchingTags(arr1: string[], arr2: string[]) { + if (arr1.length !== arr2.length) return false + + const set1 = new Set(arr1) + const set2 = new Set(arr2) + + if (set1.size !== set2.size) return false + + for (let tag of set1) { + if (!set2.has(tag)) return false + } + + return true + } + static isAvailable(ctx: { _requestHeaders: CacheHandlerContext['_requestHeaders'] }) { @@ -168,8 +183,13 @@ export default class FetchCache implements CacheHandler { // on successive requests let data = memoryCache?.get(key) - // get data from fetch cache - if (!data && this.cacheEndpoint) { + const hasFetchKindAndMatchingTags = + data?.value?.kind === 'FETCH' && + this.hasMatchingTags(tags ?? [], data.value.tags ?? []) + + // Get data from fetch cache. Also check if new tags have been + // specified with the same cache key (fetch URL) + if (this.cacheEndpoint && (!data || !hasFetchKindAndMatchingTags)) { try { const start = Date.now() const fetchParams: NextFetchCacheParams = { @@ -220,6 +240,16 @@ export default class FetchCache implements CacheHandler { throw new Error(`invalid cache value`) } + // if new tags were specified, merge those tags to the existing tags + if (cached.kind === 'FETCH') { + cached.tags ??= [] + for (const tag of tags ?? []) { + if (!cached.tags.include(tag)) { + cached.tag.push(tag) + } + } + } + const cacheState = res.headers.get(CACHE_STATE_HEADER) const age = res.headers.get('age') diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 08b1beaf28637..90c735f2f0670 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -49,6 +49,49 @@ createNextDescribe( }) }) + if (isNextDeploy) { + describe('new tags have been specified on subsequent fetch', () => { + it('should not fetch from memory cache', async () => { + const res1 = await next.fetch('/specify-new-tags/one-tag') + expect(res1.status).toBe(200) + + const res2 = await next.fetch('/specify-new-tags/two-tags') + expect(res2.status).toBe(200) + + const html1 = await res1.text() + const html2 = await res2.text() + const $1 = cheerio.load(html1) + const $2 = cheerio.load(html2) + + const data1 = $1('#page-data').text() + const data2 = $2('#page-data').text() + expect(data1).not.toBe(data2) + }) + + it('should not fetch from memory cache after revalidateTag is used', async () => { + const res1 = await next.fetch('/specify-new-tags/one-tag') + expect(res1.status).toBe(200) + + const revalidateRes = await next.fetch( + '/api/revlidate-tag-node?tag=thankyounext' + ) + expect((await revalidateRes.json()).revalidated).toBe(true) + + const res2 = await next.fetch('/specify-new-tags/two-tags') + expect(res2.status).toBe(200) + + const html1 = await res1.text() + const html2 = await res2.text() + const $1 = cheerio.load(html1) + const $2 = cheerio.load(html2) + + const data1 = $1('#page-data').text() + const data2 = $2('#page-data').text() + expect(data1).not.toBe(data2) + }) + }) + } + if (isNextStart) { it('should propagate unstable_cache tags correctly', async () => { const meta = JSON.parse( @@ -717,6 +760,10 @@ createNextDescribe( "route-handler/revalidate-360-isr/route.js", "route-handler/revalidate-360/route.js", "route-handler/static-cookies/route.js", + "specify-new-tags/one-tag/page.js", + "specify-new-tags/one-tag/page_client-reference-manifest.js", + "specify-new-tags/two-tags/page.js", + "specify-new-tags/two-tags/page_client-reference-manifest.js", "ssg-draft-mode.html", "ssg-draft-mode.rsc", "ssg-draft-mode/[[...route]]/page.js", diff --git a/test/e2e/app-dir/app-static/app/specify-new-tags/one-tag/page.tsx b/test/e2e/app-dir/app-static/app/specify-new-tags/one-tag/page.tsx new file mode 100644 index 0000000000000..6b940fa39f78c --- /dev/null +++ b/test/e2e/app-dir/app-static/app/specify-new-tags/one-tag/page.tsx @@ -0,0 +1,13 @@ +export const dynamic = 'force-dynamic' + +export default async function Page() { + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?sam=iam', + { + cache: 'force-cache', + next: { tags: ['thankyounext'] }, + } + ).then((res) => res.text()) + + return

data: {data}

+} diff --git a/test/e2e/app-dir/app-static/app/specify-new-tags/two-tags/page.tsx b/test/e2e/app-dir/app-static/app/specify-new-tags/two-tags/page.tsx new file mode 100644 index 0000000000000..7406a0445a392 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/specify-new-tags/two-tags/page.tsx @@ -0,0 +1,13 @@ +export const dynamic = 'force-dynamic' + +export default async function Page() { + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?sam=iam', + { + cache: 'force-cache', + next: { tags: ['thankyounext', 'justputit'] }, + } + ).then((res) => res.text()) + + return

data: {data}

+}