Skip to content

Commit 5195704

Browse files
authoredMar 31, 2025
Track navigation timestamp on CacheNode (#77251)
Adds a field `navigatedAt` to the CacheNode type. It represents the timestamp of the navigation that last updated the CacheNode's data. This will be used to implement the `staleTimes.dynamic` configuration option, which allows dynamic data to be stale in the UI up to the given threshold. This first PR adds the field without changing any behavior. Originally I was going to call this `requestedAt`, but I have intentionally chosen not to distinguish between whether the data was fetched from the network or from the prefetch cache. This means that even fully static data will respect the `staleTimes.dynamic` configuration option. In practice, this only matters if the dynamic stale time is longer than the static one, although usually static stale times will be larger anyway.
1 parent f691a32 commit 5195704

33 files changed

+390
-48
lines changed
 

‎packages/next/src/client/app-index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,12 @@ const pendingActionQueue: Promise<AppRouterActionQueue> = new Promise(
171171
// and before any components have hydrated.
172172
setAppBuildId(initialRSCPayload.b)
173173

174+
const initialTimestamp = Date.now()
175+
174176
resolve(
175177
createMutableActionQueue(
176178
createInitialRouterState({
179+
navigatedAt: initialTimestamp,
177180
initialFlightData: initialRSCPayload.f,
178181
initialCanonicalUrlParts: initialRSCPayload.c,
179182
initialParallelRoutes: new Map(),

‎packages/next/src/client/components/app-router.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export function createEmptyCacheNode(): CacheNode {
149149
prefetchHead: null,
150150
parallelRoutes: new Map(),
151151
loading: null,
152+
navigatedAt: -1,
152153
}
153154
}
154155

‎packages/next/src/client/components/layout-router.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ function InnerLayoutRouter({
381381
// TODO-APP: remove ''
382382
const refetchTree = walkAddRefetch(['', ...segmentPath], fullTree)
383383
const includeNextUrl = hasInterceptionRouteInCurrentTree(fullTree)
384+
const navigatedAt = Date.now()
384385
cacheNode.lazyData = lazyData = fetchServerResponse(
385386
new URL(url, location.origin),
386387
{
@@ -393,6 +394,7 @@ function InnerLayoutRouter({
393394
type: ACTION_SERVER_PATCH,
394395
previousTree: fullTree,
395396
serverResponse,
397+
navigatedAt,
396398
})
397399
})
398400

@@ -565,6 +567,7 @@ export default function OuterLayoutRouter({
565567
prefetchHead: null,
566568
parallelRoutes: new Map(),
567569
loading: null,
570+
navigatedAt: -1,
568571
}
569572

570573
// Flight data fetch kicked off during render and put into the cache.

‎packages/next/src/client/components/router-reducer/aliased-prefetch-navigations.ts

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { Mutable, ReadonlyReducerState } from './router-reducer-types'
2424
* more granular segment map and so the router will be able to simply re-use the loading segment for the new navigation.
2525
*/
2626
export function handleAliasedPrefetchEntry(
27+
navigatedAt: number,
2728
state: ReadonlyReducerState,
2829
flightData: string | NormalizedFlightData[],
2930
url: URL,
@@ -85,6 +86,7 @@ export function handleAliasedPrefetchEntry(
8586

8687
// Construct a new tree and apply the aliased loading state for each parallel route
8788
fillNewTreeWithOnlyLoadingSegments(
89+
navigatedAt,
8890
newCache,
8991
currentCache,
9092
treePatch,
@@ -99,6 +101,7 @@ export function handleAliasedPrefetchEntry(
99101

100102
// copy the loading state only into the leaf node (the part that changed)
101103
fillCacheWithNewSubTreeDataButOnlyLoading(
104+
navigatedAt,
102105
newCache,
103106
currentCache,
104107
normalizedFlightData
@@ -146,6 +149,7 @@ function hasLoadingComponentInSeedData(seedData: CacheNodeSeedData | null) {
146149
}
147150

148151
function fillNewTreeWithOnlyLoadingSegments(
152+
navigatedAt: number,
149153
newCache: CacheNode,
150154
existingCache: CacheNode,
151155
routerState: FlightRouterState,
@@ -180,6 +184,7 @@ function fillNewTreeWithOnlyLoadingSegments(
180184
prefetchHead: null,
181185
parallelRoutes: new Map(),
182186
loading,
187+
navigatedAt,
183188
}
184189
} else {
185190
// No data available for this node. This will trigger a lazy fetch
@@ -192,6 +197,7 @@ function fillNewTreeWithOnlyLoadingSegments(
192197
prefetchHead: null,
193198
parallelRoutes: new Map(),
194199
loading: null,
200+
navigatedAt: -1,
195201
}
196202
}
197203

@@ -203,6 +209,7 @@ function fillNewTreeWithOnlyLoadingSegments(
203209
}
204210

205211
fillNewTreeWithOnlyLoadingSegments(
212+
navigatedAt,
206213
newCacheNode,
207214
existingCache,
208215
parallelRouteState,

‎packages/next/src/client/components/router-reducer/apply-flight-data.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { PrefetchCacheEntry } from './router-reducer-types'
55
import type { NormalizedFlightData } from '../../flight-data-helpers'
66

77
export function applyFlightData(
8+
navigatedAt: number,
89
existingCache: CacheNode,
910
cache: CacheNode,
1011
flightData: NormalizedFlightData,
@@ -30,6 +31,7 @@ export function applyFlightData(
3031
// old behavior — no PPR value.
3132
cache.prefetchRsc = null
3233
fillLazyItemsTillLeafWithHead(
34+
navigatedAt,
3335
cache,
3436
existingCache,
3537
treePatch,
@@ -47,7 +49,13 @@ export function applyFlightData(
4749
cache.parallelRoutes = new Map(existingCache.parallelRoutes)
4850
cache.loading = existingCache.loading
4951
// Create a copy of the existing cache with the rsc applied.
50-
fillCacheWithNewSubTreeData(cache, existingCache, flightData, prefetchEntry)
52+
fillCacheWithNewSubTreeData(
53+
navigatedAt,
54+
cache,
55+
existingCache,
56+
flightData,
57+
prefetchEntry
58+
)
5159
}
5260

5361
return true

‎packages/next/src/client/components/router-reducer/clear-cache-node-data-for-segment-path.test.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React from 'react'
22
import { clearCacheNodeDataForSegmentPath } from './clear-cache-node-data-for-segment-path'
33
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
44

5+
const navigatedAt = -1
6+
57
describe('clearCacheNodeDataForSegmentPath', () => {
68
it('should clear the data property', () => {
79
const pathname = '/dashboard/settings'
@@ -13,6 +15,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
1315
.flat()
1416

1517
const cache: CacheNode = {
18+
navigatedAt,
1619
lazyData: null,
1720
rsc: null,
1821
prefetchRsc: null,
@@ -22,6 +25,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
2225
loading: null,
2326
}
2427
const existingCache: CacheNode = {
28+
navigatedAt,
2529
lazyData: null,
2630
rsc: <>Root layout</>,
2731
prefetchRsc: null,
@@ -35,6 +39,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
3539
[
3640
'linking',
3741
{
42+
navigatedAt,
3843
lazyData: null,
3944
rsc: <>Linking</>,
4045
prefetchRsc: null,
@@ -48,6 +53,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
4853
[
4954
'',
5055
{
56+
navigatedAt,
5157
lazyData: null,
5258
rsc: <>Page</>,
5359
prefetchRsc: null,
@@ -74,18 +80,21 @@ describe('clearCacheNodeDataForSegmentPath', () => {
7480
"head": null,
7581
"lazyData": null,
7682
"loading": null,
83+
"navigatedAt": -1,
7784
"parallelRoutes": Map {
7885
"children" => Map {
7986
"linking" => {
8087
"head": null,
8188
"lazyData": null,
8289
"loading": null,
90+
"navigatedAt": -1,
8391
"parallelRoutes": Map {
8492
"children" => Map {
8593
"" => {
8694
"head": null,
8795
"lazyData": null,
8896
"loading": null,
97+
"navigatedAt": -1,
8998
"parallelRoutes": Map {},
9099
"prefetchHead": null,
91100
"prefetchRsc": null,
@@ -105,6 +114,7 @@ describe('clearCacheNodeDataForSegmentPath', () => {
105114
"head": null,
106115
"lazyData": null,
107116
"loading": null,
117+
"navigatedAt": -1,
108118
"parallelRoutes": Map {},
109119
"prefetchHead": null,
110120
"prefetchRsc": null,

‎packages/next/src/client/components/router-reducer/clear-cache-node-data-for-segment-path.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function clearCacheNodeDataForSegmentPath(
4444
prefetchHead: null,
4545
parallelRoutes: new Map(),
4646
loading: null,
47+
navigatedAt: -1,
4748
})
4849
}
4950
return
@@ -60,6 +61,7 @@ export function clearCacheNodeDataForSegmentPath(
6061
prefetchHead: null,
6162
parallelRoutes: new Map(),
6263
loading: null,
64+
navigatedAt: -1,
6365
})
6466
}
6567
return

‎packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const getInitialRouterStateTree = (): FlightRouterState => [
1919
true,
2020
]
2121

22+
const navigatedAt = Date.now()
23+
2224
describe('createInitialRouterState', () => {
2325
it('should return the correct initial router state', () => {
2426
const initialTree = getInitialRouterStateTree()
@@ -32,6 +34,7 @@ describe('createInitialRouterState', () => {
3234
const initialParallelRoutes: CacheNode['parallelRoutes'] = new Map()
3335

3436
const state = createInitialRouterState({
37+
navigatedAt,
3538
initialFlightData: [[initialTree, ['', children, {}, null]]],
3639
initialCanonicalUrlParts: initialCanonicalUrl.split('/'),
3740
initialParallelRoutes,
@@ -42,6 +45,7 @@ describe('createInitialRouterState', () => {
4245
})
4346

4447
const state2 = createInitialRouterState({
48+
navigatedAt,
4549
initialFlightData: [[initialTree, ['', children, {}, null]]],
4650
initialCanonicalUrlParts: initialCanonicalUrl.split('/'),
4751
initialParallelRoutes,
@@ -52,6 +56,7 @@ describe('createInitialRouterState', () => {
5256
})
5357

5458
const expectedCache: CacheNode = {
59+
navigatedAt,
5560
lazyData: null,
5661
rsc: children,
5762
prefetchRsc: null,
@@ -65,13 +70,15 @@ describe('createInitialRouterState', () => {
6570
[
6671
'linking',
6772
{
73+
navigatedAt,
6874
parallelRoutes: new Map([
6975
[
7076
'children',
7177
new Map([
7278
[
7379
'',
7480
{
81+
navigatedAt,
7582
lazyData: null,
7683
rsc: null,
7784
prefetchRsc: null,

‎packages/next/src/client/components/router-reducer/create-initial-router-state.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { addRefreshMarkerToActiveParallelSegments } from './refetch-inactive-par
1010
import { getFlightDataPartsFromPath } from '../../flight-data-helpers'
1111

1212
export interface InitialRouterStateParameters {
13+
navigatedAt: number
1314
initialCanonicalUrlParts: string[]
1415
initialParallelRoutes: CacheNode['parallelRoutes']
1516
initialFlightData: FlightDataPath[]
@@ -20,6 +21,7 @@ export interface InitialRouterStateParameters {
2021
}
2122

2223
export function createInitialRouterState({
24+
navigatedAt,
2325
initialFlightData,
2426
initialCanonicalUrlParts,
2527
initialParallelRoutes,
@@ -52,6 +54,7 @@ export function createInitialRouterState({
5254
// The cache gets seeded during the first render. `initialParallelRoutes` ensures the cache from the first render is there during the second render.
5355
parallelRoutes: initialParallelRoutes,
5456
loading,
57+
navigatedAt,
5558
}
5659

5760
const canonicalUrl =
@@ -69,6 +72,7 @@ export function createInitialRouterState({
6972
// When the cache hasn't been seeded yet we fill the cache with the head.
7073
if (initialParallelRoutes === null || initialParallelRoutes.size === 0) {
7174
fillLazyItemsTillLeafWithHead(
75+
navigatedAt,
7276
cache,
7377
undefined,
7478
initialTree,

‎packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx

+15-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const getFlightData = (): NormalizedFlightData[] => {
2020
describe('fillCacheWithNewSubtreeData', () => {
2121
it('should apply rsc and head property', () => {
2222
const cache: CacheNode = {
23+
navigatedAt: -1,
2324
lazyData: null,
2425
rsc: null,
2526
prefetchRsc: null,
@@ -29,6 +30,7 @@ describe('fillCacheWithNewSubtreeData', () => {
2930
parallelRoutes: new Map(),
3031
}
3132
const existingCache: CacheNode = {
33+
navigatedAt: -1,
3234
lazyData: null,
3335
rsc: <>Root layout</>,
3436
prefetchRsc: null,
@@ -42,6 +44,7 @@ describe('fillCacheWithNewSubtreeData', () => {
4244
[
4345
'linking',
4446
{
47+
navigatedAt: -1,
4548
lazyData: null,
4649
rsc: <>Linking</>,
4750
prefetchRsc: null,
@@ -55,6 +58,7 @@ describe('fillCacheWithNewSubtreeData', () => {
5558
[
5659
'',
5760
{
61+
navigatedAt: -1,
5862
lazyData: null,
5963
rsc: <>Page</>,
6064
prefetchRsc: null,
@@ -83,9 +87,16 @@ describe('fillCacheWithNewSubtreeData', () => {
8387
// Mirrors the way router-reducer values are passed in.
8488
const normalizedFlightData = flightData[0]
8589

86-
fillCacheWithNewSubTreeData(cache, existingCache, normalizedFlightData)
90+
const navigatedAt = -1
91+
fillCacheWithNewSubTreeData(
92+
navigatedAt,
93+
cache,
94+
existingCache,
95+
normalizedFlightData
96+
)
8797

8898
const expectedCache: CacheNode = {
99+
navigatedAt: -1,
89100
lazyData: null,
90101
rsc: null,
91102
prefetchRsc: null,
@@ -99,6 +110,7 @@ describe('fillCacheWithNewSubtreeData', () => {
99110
[
100111
'linking',
101112
{
113+
navigatedAt: -1,
102114
lazyData: null,
103115
rsc: <>Linking</>,
104116
prefetchRsc: null,
@@ -113,6 +125,7 @@ describe('fillCacheWithNewSubtreeData', () => {
113125
[
114126
'',
115127
{
128+
navigatedAt: -1,
116129
lazyData: null,
117130
rsc: <>Page</>,
118131
prefetchRsc: null,
@@ -125,6 +138,7 @@ describe('fillCacheWithNewSubtreeData', () => {
125138
[
126139
'about',
127140
{
141+
navigatedAt: -1,
128142
lazyData: null,
129143
head: null,
130144
prefetchHead: null,

0 commit comments

Comments
 (0)
Please sign in to comment.