From bc216b4c26b682bb46caa2e63653c105a67e19e6 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Sat, 2 Dec 2023 23:17:59 +0000 Subject: [PATCH] some support for suspense cache soft/implicit tags (#569) --- .changeset/good-squids-join.md | 5 +++ .changeset/shaggy-wombats-happen.md | 5 +++ .../templates/_worker.js/utils/cache.ts | 22 +++++++----- .../next-on-pages/templates/cache/adaptor.ts | 34 +++++++++++++++---- 4 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 .changeset/good-squids-join.md create mode 100644 .changeset/shaggy-wombats-happen.md diff --git a/.changeset/good-squids-join.md b/.changeset/good-squids-join.md new file mode 100644 index 000000000..d33dc66b6 --- /dev/null +++ b/.changeset/good-squids-join.md @@ -0,0 +1,5 @@ +--- +'@cloudflare/next-on-pages': patch +--- + +Improved support for newer version of Next.js with the suspense cache through trying to handle soft/implicit tags properly diff --git a/.changeset/shaggy-wombats-happen.md b/.changeset/shaggy-wombats-happen.md new file mode 100644 index 000000000..8073b2bcb --- /dev/null +++ b/.changeset/shaggy-wombats-happen.md @@ -0,0 +1,5 @@ +--- +'@cloudflare/next-on-pages': patch +--- + +Fix old version of Next.js not updating a cache entry properly due to not receving the correct shape they expected. diff --git a/packages/next-on-pages/templates/_worker.js/utils/cache.ts b/packages/next-on-pages/templates/_worker.js/utils/cache.ts index 12f333d7f..5030fa70d 100644 --- a/packages/next-on-pages/templates/_worker.js/utils/cache.ts +++ b/packages/next-on-pages/templates/_worker.js/utils/cache.ts @@ -3,6 +3,8 @@ import { SUSPENSE_CACHE_URL } from '../../cache'; // https://github.com/vercel/next.js/blob/48a566bc/packages/next/src/server/lib/incremental-cache/fetch-cache.ts#L19 const CACHE_TAGS_HEADER = 'x-vercel-cache-tags'; +// https://github.com/vercel/next.js/blob/ba23d986/packages/next/src/lib/constants.ts#L18 +const NEXT_CACHE_SOFT_TAGS_HEADER = 'x-next-cache-soft-tags'; /** * Handles an internal request to the suspense cache. @@ -37,8 +39,13 @@ export async function handleSuspenseCacheRequest(request: Request) { switch (request.method) { case 'GET': { + const softTags = getTagsFromHeader( + request, + NEXT_CACHE_SOFT_TAGS_HEADER, + ); + // Retrieve the value from the cache. - const data = await cache.get(cacheKey); + const data = await cache.get(cacheKey, { softTags }); if (!data) return new Response(null, { status: 404 }); return new Response(JSON.stringify(data.value), { @@ -55,14 +62,7 @@ export async function handleSuspenseCacheRequest(request: Request) { const body = await request.json(); // Falling back to the cache tags header for Next.js 13.5+ if (body.data.tags === undefined) { - body.tags ??= - request.headers - .get(CACHE_TAGS_HEADER) - ?.split(',') - ?.filter(Boolean) ?? []; - } else { - body.tags = body.data.tags; - delete body.data.tags; + body.tags ??= getTagsFromHeader(request, CACHE_TAGS_HEADER) ?? []; } await cache.set(cacheKey, body); @@ -104,3 +104,7 @@ async function getInternalCacheAdaptor( const adaptor = await import(`./__next-on-pages-dist__/cache/${type}.js`); return new adaptor.default(); } + +function getTagsFromHeader(req: Request, key: string): string[] | undefined { + return req.headers.get(key)?.split(',')?.filter(Boolean); +} diff --git a/packages/next-on-pages/templates/cache/adaptor.ts b/packages/next-on-pages/templates/cache/adaptor.ts index f39b1af91..608877882 100644 --- a/packages/next-on-pages/templates/cache/adaptor.ts +++ b/packages/next-on-pages/templates/cache/adaptor.ts @@ -1,6 +1,9 @@ // NOTE: This is given the same name that the environment variable has in the Next.js source code. export const SUSPENSE_CACHE_URL = 'INTERNAL_SUSPENSE_CACHE_HOSTNAME.local'; +// https://github.com/vercel/next.js/blob/f6babb4/packages/next/src/lib/constants.ts#23 +const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_'; + // Set to track the revalidated tags in requests. const revalidatedTags = new Set(); @@ -54,10 +57,17 @@ export class CacheAdaptor { switch (newEntry.value?.kind) { case 'FETCH': { // Update the tags with the cache key. - const tags = newEntry.value.tags ?? []; + const tags = getTagsFromEntry(newEntry); await this.setTags(tags, { cacheKey: key }); - getDerivedTags(tags).forEach(tag => revalidatedTags.delete(tag)); + const derivedTags = getDerivedTags(tags); + const implicitTags = derivedTags.map( + tag => `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}`, + ); + + [...derivedTags, ...implicitTags].forEach(tag => + revalidatedTags.delete(tag), + ); } } } @@ -66,9 +76,13 @@ export class CacheAdaptor { * Retrieves an entry from the suspense cache. * * @param key Key for the item in the suspense cache. + * @param opts Soft cache tags used when checking if an entry is stale. * @returns The cached value, or null if no entry exists. */ - public async get(key: string): Promise { + public async get( + key: string, + { softTags }: { softTags?: string[] }, + ): Promise { // Get entry from the cache. const entry = await this.retrieve(key); if (!entry) return null; @@ -87,10 +101,12 @@ export class CacheAdaptor { await this.loadTagsManifest(); // Check if the cache entry is stale or fresh based on the tags. - const tags = getDerivedTags( - data.value.tags ?? data.value.data.tags ?? [], - ); - const isStale = tags.some(tag => { + const tags = getTagsFromEntry(data); + const combinedTags = softTags + ? [...tags, ...softTags] + : getDerivedTags(tags); + + const isStale = combinedTags.some(tag => { // If a revalidation has been triggered, the current entry is stale. if (revalidatedTags.has(tag)) return true; @@ -257,3 +273,7 @@ export function getDerivedTags(tags: string[]): string[] { } return derivedTags; } + +export function getTagsFromEntry(entry: CacheHandlerValue): string[] { + return entry.value?.tags ?? entry.value?.data?.tags ?? []; +}