diff --git a/packages/next/src/client/components/app-router-headers.ts b/packages/next/src/client/components/app-router-headers.ts index ac9b33615a129b..940ba3e7ba73be 100644 --- a/packages/next/src/client/components/app-router-headers.ts +++ b/packages/next/src/client/components/app-router-headers.ts @@ -25,5 +25,7 @@ export const FLIGHT_HEADERS = [ export const NEXT_RSC_UNION_QUERY = '_rsc' as const +// TODO: Rebase on Seb's PR +export const NEXT_ROUTER_STALE_TIME_HEADER = 'x-nextjs-stale-time' as const export const NEXT_DID_POSTPONE_HEADER = 'x-nextjs-postponed' as const export const NEXT_IS_PRERENDER_HEADER = 'x-nextjs-prerender' as const diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 634c6e2c79bbe2..46ef02e6e2be27 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -24,6 +24,7 @@ import { RSC_CONTENT_TYPE_HEADER, NEXT_HMR_REFRESH_HEADER, NEXT_DID_POSTPONE_HEADER, + NEXT_ROUTER_STALE_TIME_HEADER, } from '../app-router-headers' import { callServer } from '../../app-call-server' import { findSourceMapURL } from '../../app-find-source-map-url' @@ -48,6 +49,7 @@ export type FetchServerResponseResult = { couldBeIntercepted: boolean prerendered: boolean postponed: boolean + staleTime: number } function urlToUrlWithoutFlightMarker(url: string): URL { @@ -74,6 +76,7 @@ function doMpaNavigation(url: string): FetchServerResponseResult { couldBeIntercepted: false, prerendered: false, postponed: false, + staleTime: -1, } } @@ -177,6 +180,9 @@ export async function fetchServerResponse( const contentType = res.headers.get('content-type') || '' const interception = !!res.headers.get('vary')?.includes(NEXT_URL) const postponed = !!res.headers.get(NEXT_DID_POSTPONE_HEADER) + const staleTimeHeader = res.headers.get(NEXT_ROUTER_STALE_TIME_HEADER) + const staleTime = + staleTimeHeader !== null ? parseInt(staleTimeHeader, 10) : -1 let isFlightResponse = contentType.startsWith(RSC_CONTENT_TYPE_HEADER) if (process.env.NODE_ENV === 'production') { @@ -222,6 +228,7 @@ export async function fetchServerResponse( couldBeIntercepted: interception, prerendered: response.S, postponed, + staleTime, } } catch (err) { console.error( @@ -237,6 +244,7 @@ export async function fetchServerResponse( couldBeIntercepted: false, prerendered: false, postponed: false, + staleTime: -1, } } } diff --git a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts index 9e353cfc23ed53..4984dd52b8ec8e 100644 --- a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts +++ b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts @@ -297,6 +297,7 @@ export function createSeededPrefetchCacheEntry({ kind, prefetchTime: Date.now(), lastUsedTime: Date.now(), + staleTime: -1, key: prefetchCacheKey, status: PrefetchCacheEntryStatus.fresh, url, @@ -360,6 +361,11 @@ function createLazyPrefetchEntry({ ) if (existingCacheEntry) { existingCacheEntry.kind = PrefetchKind.FULL + if (prefetchResponse.staleTime !== -1) { + // This is the stale time that was collected by the server during + // static generation. Use this in place of the default stale time. + existingCacheEntry.staleTime = prefetchResponse.staleTime + } } } @@ -373,6 +379,7 @@ function createLazyPrefetchEntry({ kind, prefetchTime: Date.now(), lastUsedTime: null, + staleTime: -1, key: prefetchCacheKey, status: PrefetchCacheEntryStatus.fresh, url, @@ -408,7 +415,22 @@ function getPrefetchEntryCacheStatus({ kind, prefetchTime, lastUsedTime, + staleTime, }: PrefetchCacheEntry): PrefetchCacheEntryStatus { + if (staleTime !== -1) { + // `staleTime` is the value sent by the server during static generation. + // When this is available, it takes precedence over any of the heuristics + // that follow. + // + // TODO: When PPR is enabled, the server will *always* return a stale time + // when prefetching. We should never use a prefetch entry that hasn't yet + // received data from the server. So the only two cases should be 1) we use + // the server-generated stale time 2) the unresolved entry is discarded. + return Date.now() < prefetchTime + staleTime + ? PrefetchCacheEntryStatus.fresh + : PrefetchCacheEntryStatus.stale + } + // We will re-use the cache entry data for up to the `dynamic` staletime window. if (Date.now() < (lastUsedTime ?? prefetchTime) + DYNAMIC_STALETIME_MS) { return lastUsedTime diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index b5cb9a3fdf3a6c..0be11bee972725 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -205,6 +205,7 @@ export type PrefetchCacheEntry = { data: Promise kind: PrefetchKind prefetchTime: number + staleTime: number lastUsedTime: number | null key: string status: PrefetchCacheEntryStatus