Skip to content

Update cache handler interface #76687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -664,5 +664,6 @@
"663": "Invariant: client chunk changed but failed to detect hash %s",
"664": "Missing 'next-action' header.",
"665": "Failed to find Server Action \"%s\". This request might be from an older or newer deployment.\\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action",
"666": "Turbopack builds are only available in canary builds of Next.js."
"666": "Turbopack builds are only available in canary builds of Next.js.",
"667": "receiveExpiredTags is deprecated, and not expected to be called."
}
1 change: 1 addition & 0 deletions packages/next/src/build/static-paths/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export async function buildAppStaticPaths({
onAfterTaskError: afterRunner.context.onTaskError,
},
buildId,
previouslyRevalidatedTags: [],
})

const routeParams = await ComponentMod.workAsyncStorage.run(
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/after/after-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ const createMockWorkStore = (afterContext: AfterContext): WorkStore => {
forceDynamic: false,
dynamicShouldError: false,
isStaticGeneration: false,
revalidatedTags: [],
pendingRevalidatedTags: [],
pendingRevalidates: undefined,
pendingRevalidateWrites: undefined,
incrementalCache: undefined,
Expand Down
16 changes: 10 additions & 6 deletions packages/next/src/server/after/revalidation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ export async function withExecuteRevalidates<T>(
type RevalidationState = Required<
Pick<
WorkStore,
'revalidatedTags' | 'pendingRevalidates' | 'pendingRevalidateWrites'
'pendingRevalidatedTags' | 'pendingRevalidates' | 'pendingRevalidateWrites'
>
>

function cloneRevalidationState(store: WorkStore): RevalidationState {
return {
revalidatedTags: store.revalidatedTags ? [...store.revalidatedTags] : [],
pendingRevalidatedTags: store.pendingRevalidatedTags
? [...store.pendingRevalidatedTags]
: [],
pendingRevalidates: { ...store.pendingRevalidates },
pendingRevalidateWrites: store.pendingRevalidateWrites
? [...store.pendingRevalidateWrites]
Expand All @@ -44,10 +46,12 @@ function diffRevalidationState(
prev: RevalidationState,
curr: RevalidationState
): RevalidationState {
const prevTags = new Set(prev.revalidatedTags)
const prevTags = new Set(prev.pendingRevalidatedTags)
const prevRevalidateWrites = new Set(prev.pendingRevalidateWrites)
return {
revalidatedTags: curr.revalidatedTags.filter((tag) => !prevTags.has(tag)),
pendingRevalidatedTags: curr.pendingRevalidatedTags.filter(
(tag) => !prevTags.has(tag)
),
pendingRevalidates: Object.fromEntries(
Object.entries(curr.pendingRevalidates).filter(
([key]) => !(key in prev.pendingRevalidates)
Expand All @@ -62,13 +66,13 @@ function diffRevalidationState(
async function executeRevalidates(
workStore: WorkStore,
{
revalidatedTags,
pendingRevalidatedTags,
pendingRevalidates,
pendingRevalidateWrites,
}: RevalidationState
) {
return Promise.all([
workStore.incrementalCache?.revalidateTag(revalidatedTags),
workStore.incrementalCache?.revalidateTag(pendingRevalidatedTags),
...Object.values(pendingRevalidates),
...pendingRevalidateWrites,
])
Expand Down
59 changes: 23 additions & 36 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function getForwardedHeaders(
return new Headers(mergedHeaders)
}

async function addRevalidationHeader(
function addRevalidationHeader(
res: BaseNextResponse,
{
workStore,
Expand All @@ -123,12 +123,6 @@ async function addRevalidationHeader(
requestStore: RequestStore
}
) {
await Promise.all([
workStore.incrementalCache?.revalidateTag(workStore.revalidatedTags || []),
...Object.values(workStore.pendingRevalidates || {}),
...(workStore.pendingRevalidateWrites || []),
])

// If a tag was revalidated, the client router needs to invalidate all the
// client router cache as they may be stale. And if a path was revalidated, the
// client needs to invalidate all subtrees below that path.
Expand All @@ -142,7 +136,7 @@ async function addRevalidationHeader(
// TODO-APP: Currently paths are treated as tags, so the second element of the tuple
// is always empty.

const isTagRevalidated = workStore.revalidatedTags?.length ? 1 : 0
const isTagRevalidated = workStore.pendingRevalidatedTags?.length ? 1 : 0
const isCookieRevalidated = getModifiedCookieValues(
requestStore.mutableCookies
).length
Expand Down Expand Up @@ -320,10 +314,10 @@ async function createRedirectRenderResult(
`${origin}${appRelativeRedirectUrl.pathname}${appRelativeRedirectUrl.search}`
)

if (workStore.revalidatedTags) {
if (workStore.pendingRevalidatedTags) {
forwardedHeaders.set(
NEXT_CACHE_REVALIDATED_TAGS_HEADER,
workStore.revalidatedTags.join(',')
workStore.pendingRevalidatedTags.join(',')
)
forwardedHeaders.set(
NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER,
Expand Down Expand Up @@ -516,6 +510,17 @@ export async function handleAction({

requestStore.phase = 'action'

const resolvePendingRevalidations = async () =>
workUnitAsyncStorage.run(requestStore, () =>
Promise.all([
workStore.incrementalCache?.revalidateTag(
workStore.pendingRevalidatedTags || []
),
...Object.values(workStore.pendingRevalidates || {}),
...(workStore.pendingRevalidateWrites || []),
])
)

// When running actions the default is no-store, you can still `cache: 'force-cache'`
workStore.fetchCache = 'default-no-store'

Expand Down Expand Up @@ -567,13 +572,7 @@ export async function handleAction({

if (isFetchAction) {
res.statusCode = 500
await Promise.all([
workStore.incrementalCache?.revalidateTag(
workStore.revalidatedTags || []
),
...Object.values(workStore.pendingRevalidates || {}),
...(workStore.pendingRevalidateWrites || []),
])
await resolvePendingRevalidations()

const promise = Promise.reject(error)
try {
Expand Down Expand Up @@ -928,10 +927,8 @@ export async function handleAction({

// For form actions, we need to continue rendering the page.
if (isFetchAction) {
await addRevalidationHeader(res, {
workStore,
requestStore,
})
await resolvePendingRevalidations()
addRevalidationHeader(res, { workStore, requestStore })

actionResult = await finalizeAndGenerateFlight(req, ctx, requestStore, {
actionResult: Promise.resolve(returnVal),
Expand All @@ -952,10 +949,8 @@ export async function handleAction({
const redirectUrl = getURLFromRedirectError(err)
const redirectType = getRedirectTypeFromError(err)

await addRevalidationHeader(res, {
workStore,
requestStore,
})
await resolvePendingRevalidations()
addRevalidationHeader(res, { workStore, requestStore })

// if it's a fetch action, we'll set the status code for logging/debugging purposes
// but we won't set a Location header, as the redirect will be handled by the client router
Expand Down Expand Up @@ -984,10 +979,8 @@ export async function handleAction({
} else if (isHTTPAccessFallbackError(err)) {
res.statusCode = getAccessFallbackHTTPStatus(err)

await addRevalidationHeader(res, {
workStore,
requestStore,
})
await resolvePendingRevalidations()
addRevalidationHeader(res, { workStore, requestStore })

if (isFetchAction) {
const promise = Promise.reject(err)
Expand Down Expand Up @@ -1016,13 +1009,7 @@ export async function handleAction({

if (isFetchAction) {
res.statusCode = 500
await Promise.all([
workStore.incrementalCache?.revalidateTag(
workStore.revalidatedTags || []
),
...Object.values(workStore.pendingRevalidates || {}),
...(workStore.pendingRevalidateWrites || []),
])
await resolvePendingRevalidations()
const promise = Promise.reject(err)
try {
// we need to await the promise to trigger the rejection early
Expand Down
Loading
Loading