Repository: vercel/next.js
Pull request: [backport] bugfix: propagate staleTime to seeded prefetch entry (#81263)

Backports:
- #81263

Changed files:
filename: packages/next/src/client/components/router-reducer/create-initial-router-state.ts
patch: @@ -4,7 +4,10 @@ import type { FlightDataPath } from '../../../server/app-render/types'
 import { createHrefFromUrl } from './create-href-from-url'
 import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head'
 import { extractPathFromFlightRouterState } from './compute-changed-path'
-import { createSeededPrefetchCacheEntry } from './prefetch-cache-utils'
+import {
+  createSeededPrefetchCacheEntry,
+  STATIC_STALETIME_MS,
+} from './prefetch-cache-utils'
 import { PrefetchKind, type PrefetchCacheEntry } from './router-reducer-types'
 import { addRefreshMarkerToActiveParallelSegments } from './refetch-inactive-parallel-segments'
 import { getFlightDataPartsFromPath } from '../../flight-data-helpers'
@@ -132,7 +135,13 @@ export function createInitialRouterState({
         // add a way to split a single Flight stream into static and dynamic
         // parts. But in the meantime we should at least make this work for
         // fully static pages.
-        staleTime: -1,
+        staleTime:
+          // In the old router, there was only a single configurable staleTime (experimental.staleTimes)
+          // As an abundance of caution, this will only set the initial staleTime to the configured value
+          // if we're not leveraging the segment cache, which has its own prefetching semantics.
+          prerendered && !process.env.__NEXT_CLIENT_SEGMENT_CACHE
+            ? STATIC_STALETIME_MS
+            : -1,
       },
       tree: initialState.tree,
       prefetchCache: initialState.prefetchCache,

filename: packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts
patch: @@ -291,7 +291,7 @@ export function createSeededPrefetchCacheEntry({
     kind,
     prefetchTime: Date.now(),
     lastUsedTime: Date.now(),
-    staleTime: -1,
+    staleTime: data.staleTime,
     key: prefetchCacheKey,
     status: PrefetchCacheEntryStatus.fresh,
     url,

filename: test/e2e/app-dir/app-prefetch/app/page.js
patch: @@ -8,6 +8,9 @@ export default function HomePage() {
       <Link href="/static-page" id="to-static-page">
         To Static Page
       </Link>
+      <Link href="/static-page-no-prefetch" id="to-static-page-no-prefetch">
+        To Static Page No Prefetch
+      </Link>
       <Link href="/dynamic-page" id="to-dynamic-page-no-params">
         To Dynamic Page
       </Link>

filename: test/e2e/app-dir/app-prefetch/app/static-page-no-prefetch/page.js
patch: @@ -0,0 +1,14 @@
+import Link from 'next/link'
+
+export default async function Page() {
+  return (
+    <>
+      <p id="static-page-no-prefetch">Static Page No Prefetch</p>
+      <p>
+        <Link href="/" id="to-home">
+          To home
+        </Link>
+      </p>
+    </>
+  )
+}

filename: test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts
patch: @@ -8,8 +8,8 @@ describe('app dir - prefetching (custom staleTime)', () => {
     nextConfig: {
       experimental: {
         staleTimes: {
-          static: 30,
-          dynamic: 20,
+          static: 10,
+          dynamic: 5,
         },
       },
     },
@@ -86,7 +86,7 @@ describe('app dir - prefetching (custom staleTime)', () => {
       .waitForElementByCss('#to-static-page')
 
     // Wait for the stale time to pass.
-    await waitFor(30000)
+    await waitFor(10000)
     // Click on the link to the static page again
     await linkToStaticPage.click()
     // Wait for the static page to load again
@@ -131,4 +131,37 @@ describe('app dir - prefetching (custom staleTime)', () => {
     // the initial fetch, 2 sub-page fetches, and a final fetch when reloading the page
     expect(await browser.elementById('count').text()).toBe('4')
   })
+
+  it('should fetch again when the initially visited static page is visited after the stale time has passed', async () => {
+    const browser = await next.browser('/404')
+    let requests: string[] = []
+
+    browser.on('request', (req) => {
+      // only consider requests that have RSC header but not the prefetch header
+      if (req.headers()['rsc'] && !req.headers()['next-router-prefetch']) {
+        requests.push(new URL(req.url()).pathname)
+      }
+    })
+
+    await browser.eval('location.href = "/static-page-no-prefetch"')
+
+    await browser
+      .elementByCss('#to-home')
+      .click()
+      .waitForElementByCss('#to-static-page')
+
+    // Wait for the stale time to pass.
+    await waitFor(10000)
+
+    await browser.elementByCss('#to-static-page-no-prefetch').click()
+
+    // Wait for the static page to load again
+    await browser.waitForElementByCss('#static-page-no-prefetch')
+
+    await retry(async () => {
+      expect(
+        requests.filter((request) => request === '/static-page-no-prefetch')
+      ).toHaveLength(1)
+    })
+  })
 })