Skip to content

Commit c80bef1

Browse files
committed
Re-implement Server Action reducer
WIP
1 parent c42d0e3 commit c80bef1

File tree

10 files changed

+501
-230
lines changed

10 files changed

+501
-230
lines changed

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,5 +943,6 @@
943943
"942": "Unexpected stream chunk while in Before stage",
944944
"943": "getFlightStream should always receive a ReadableStream when using the edge runtime",
945945
"944": "nodeStreamFromReadableStream cannot be used in the edge runtime",
946-
"945": "createNodeStreamFromChunks cannot be used in the edge runtime"
946+
"945": "createNodeStreamFromChunks cannot be used in the edge runtime",
947+
"946": "Internal Next.js error: Missing page segment."
947948
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@ export function createEmptyCacheNode(): CacheNode {
117117
}
118118
}
119119

120+
export function clearDynamicDataFromCacheNodeTree(
121+
existingCache: CacheNode
122+
): CacheNode {
123+
// Copy all the fields from the root cache node, but none of the children.
124+
const clone = createEmptyCacheNode()
125+
clone.lazyData = existingCache.lazyData
126+
clone.rsc = existingCache.rsc
127+
clone.prefetchRsc = existingCache.prefetchRsc
128+
clone.head = existingCache.head
129+
clone.prefetchHead = existingCache.prefetchHead
130+
clone.loading = existingCache.loading
131+
clone.navigatedAt = existingCache.navigatedAt
132+
return clone
133+
}
134+
120135
function copyNextJsInternalHistoryState(data: any) {
121136
if (data == null) data = {}
122137
const currentState = window.history.state

packages/next/src/client/components/router-reducer/ppr-navigations.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type {
1515
} from '../../../shared/lib/app-router-types'
1616
import { DEFAULT_SEGMENT_KEY } from '../../../shared/lib/segment'
1717
import { matchSegment } from '../match-segments'
18-
import { revalidateEntireCache } from '../segment-cache/cache'
1918
import { createHrefFromUrl } from './create-href-from-url'
2019
import { createRouterCacheKey } from './create-router-cache-key'
2120
import {
@@ -111,6 +110,8 @@ export function startPPRNavigation(
111110
oldCacheNode: CacheNode,
112111
oldRouterState: FlightRouterState,
113112
newRouterState: FlightRouterState,
113+
seedData: CacheNodeSeedData | null,
114+
seedHead: HeadData | null,
114115
prefetchData: CacheNodeSeedData | null,
115116
prefetchHead: HeadData | null,
116117
isPrefetchHeadPartial: boolean,
@@ -125,6 +126,8 @@ export function startPPRNavigation(
125126
oldRouterState,
126127
newRouterState,
127128
false,
129+
seedData,
130+
seedHead,
128131
prefetchData,
129132
prefetchHead,
130133
isPrefetchHeadPartial,
@@ -137,18 +140,14 @@ export function startPPRNavigation(
137140
export function startPPRRefresh(
138141
navigatedAt: number,
139142
currentRouterState: FlightRouterState,
140-
currentNextUrl: string | null,
143+
seedData: CacheNodeSeedData | null,
144+
seedHead: HeadData | null,
141145
accumulation: NavigationRequestAccumulation
142146
): Task | null {
143147
// A refresh is a special case of a navigation where all the dynamic data in
144148
// the page is re-fetched. There is no "shared layout" to consider because
145149
// the route hasn't changed.
146150

147-
// TODO: Currently, all refreshes purge the prefetch cache. In the future,
148-
// only client-side refreshes will have this behavior; the server-side
149-
// `refresh` should send new data without purging the prefetch cache.
150-
revalidateEntireCache(currentNextUrl, currentRouterState)
151-
152151
// TODO: Currently refreshes do not read from the prefetch cache, as in the
153152
// pre-Segment Cache implementation. This will be added in a subsequent PR.
154153
const prefetchData = null
@@ -167,6 +166,8 @@ export function startPPRRefresh(
167166
navigatedAt,
168167
currentRouterState,
169168
existingCacheNode,
169+
seedData,
170+
seedHead,
170171
prefetchData,
171172
prefetchHead,
172173
isPrefetchHeadPartial,
@@ -182,6 +183,8 @@ function updateCacheNodeOnNavigation(
182183
oldRouterState: FlightRouterState,
183184
newRouterState: FlightRouterState,
184185
didFindRootLayout: boolean,
186+
seedData: CacheNodeSeedData | null,
187+
seedHead: HeadData | null,
185188
prefetchData: CacheNodeSeedData | null,
186189
prefetchHead: HeadData | null,
187190
isPrefetchHeadPartial: boolean,
@@ -192,6 +195,7 @@ function updateCacheNodeOnNavigation(
192195
// Diff the old and new trees to reuse the shared layouts.
193196
const oldRouterStateChildren = oldRouterState[1]
194197
const newRouterStateChildren = newRouterState[1]
198+
const seedDataChildren = seedData !== null ? seedData[1] : null
195199
const prefetchDataChildren = prefetchData !== null ? prefetchData[1] : null
196200

197201
if (!didFindRootLayout) {
@@ -257,9 +261,13 @@ function updateCacheNodeOnNavigation(
257261
const oldRouterStateChild: FlightRouterState | void =
258262
oldRouterStateChildren[parallelRouteKey]
259263
const oldSegmentMapChild = oldParallelRoutes.get(parallelRouteKey)
264+
const seedDataChild: CacheNodeSeedData | void | null =
265+
seedDataChildren !== null
266+
? (seedDataChildren[parallelRouteKey] ?? null)
267+
: null
260268
const prefetchDataChild: CacheNodeSeedData | void | null =
261269
prefetchDataChildren !== null
262-
? prefetchDataChildren[parallelRouteKey]
270+
? (prefetchDataChildren[parallelRouteKey] ?? null)
263271
: null
264272

265273
const newSegmentChild = newRouterStateChild[0]
@@ -299,7 +307,9 @@ function updateCacheNodeOnNavigation(
299307
newRouterStateChild,
300308
oldCacheNodeChild,
301309
didFindRootLayout,
302-
prefetchDataChild !== undefined ? prefetchDataChild : null,
310+
seedDataChild,
311+
seedHead,
312+
prefetchDataChild,
303313
prefetchHead,
304314
isPrefetchHeadPartial,
305315
newSegmentPathChild,
@@ -340,7 +350,9 @@ function updateCacheNodeOnNavigation(
340350
newRouterStateChild,
341351
oldCacheNodeChild,
342352
didFindRootLayout,
343-
prefetchDataChild !== undefined ? prefetchDataChild : null,
353+
seedDataChild,
354+
seedHead,
355+
prefetchDataChild,
344356
prefetchHead,
345357
isPrefetchHeadPartial,
346358
newSegmentPathChild,
@@ -364,6 +376,8 @@ function updateCacheNodeOnNavigation(
364376
oldRouterStateChild,
365377
newRouterStateChild,
366378
didFindRootLayout,
379+
seedDataChild,
380+
seedHead,
367381
prefetchDataChild,
368382
prefetchHead,
369383
isPrefetchHeadPartial,
@@ -380,7 +394,9 @@ function updateCacheNodeOnNavigation(
380394
newRouterStateChild,
381395
oldCacheNodeChild,
382396
didFindRootLayout,
383-
prefetchDataChild !== undefined ? prefetchDataChild : null,
397+
seedDataChild,
398+
seedHead,
399+
prefetchDataChild,
384400
prefetchHead,
385401
isPrefetchHeadPartial,
386402
newSegmentPathChild,
@@ -395,7 +411,9 @@ function updateCacheNodeOnNavigation(
395411
newRouterStateChild,
396412
oldCacheNodeChild,
397413
didFindRootLayout,
398-
prefetchDataChild !== undefined ? prefetchDataChild : null,
414+
seedDataChild,
415+
seedHead,
416+
prefetchDataChild,
399417
prefetchHead,
400418
isPrefetchHeadPartial,
401419
newSegmentPathChild,
@@ -495,6 +513,8 @@ function beginRenderingNewRouteTree(
495513
newRouterState: FlightRouterState,
496514
existingCacheNode: CacheNode | void,
497515
didFindRootLayout: boolean,
516+
seedData: CacheNodeSeedData | null,
517+
seedHead: HeadData | null,
498518
prefetchData: CacheNodeSeedData | null,
499519
possiblyPartialPrefetchHead: HeadData | null,
500520
isPrefetchHeadPartial: boolean,
@@ -537,6 +557,8 @@ function beginRenderingNewRouteTree(
537557
navigatedAt,
538558
newRouterState,
539559
existingCacheNode,
560+
seedData,
561+
seedHead,
540562
prefetchData,
541563
possiblyPartialPrefetchHead,
542564
isPrefetchHeadPartial,
@@ -551,6 +573,8 @@ function createCacheNodeOnNavigation(
551573
navigatedAt: number,
552574
routerState: FlightRouterState,
553575
existingCacheNode: CacheNode | void,
576+
seedData: CacheNodeSeedData | null,
577+
seedHead: HeadData | null,
554578
prefetchData: CacheNodeSeedData | null,
555579
possiblyPartialPrefetchHead: HeadData | null,
556580
isPrefetchHeadPartial: boolean,
@@ -597,7 +621,22 @@ function createCacheNodeOnNavigation(
597621
let loading: LoadingModuleData | Promise<LoadingModuleData>
598622
let head: HeadData | null
599623
let cacheNodeNavigatedAt: number
600-
if (
624+
if (seedData !== null) {
625+
// When navigating in response to a Server Action, the server may send back
626+
// RSC data alongside the action body response. If so, we can use this data
627+
// directly without consulting the client cache and without triggering a new
628+
// request from the server.
629+
// TODO: Since Server Action responses are streaming, we may not be able to
630+
// render the UI immediately, even though we already have a reference to it
631+
// here. It's worth checking the prefetch cache to see if there's a partial
632+
// state we can show in the meantime. We'll implement this once this module
633+
// is updated to access the prefetch cache directly, instead of passing it
634+
// down via the CacheNodeSeedData type.
635+
rsc = seedData[0]
636+
loading = seedData[2]
637+
head = isLeafSegment ? seedHead : null
638+
cacheNodeNavigatedAt = navigatedAt
639+
} else if (
601640
existingCacheNode !== undefined &&
602641
// DYNAMIC_STALETIME_MS defaults to 0, but it can be increased using
603642
// the experimental.staleTimes.dynamic config. When set, we'll avoid
@@ -673,6 +712,7 @@ function createCacheNodeOnNavigation(
673712
// new one from the server. Keep traversing down the tree until we reach
674713
// something that requires a dynamic request.
675714
const prefetchDataChildren = prefetchData !== null ? prefetchData[1] : null
715+
const seedDataChildren = seedData !== null ? seedData[1] : null
676716
const taskChildren = new Map()
677717
const existingCacheNodeChildren =
678718
existingCacheNode !== undefined ? existingCacheNode.parallelRoutes : null
@@ -693,9 +733,13 @@ function createCacheNodeOnNavigation(
693733
for (let parallelRouteKey in routerStateChildren) {
694734
const routerStateChild: FlightRouterState =
695735
routerStateChildren[parallelRouteKey]
696-
const prefetchDataChild: CacheNodeSeedData | void | null =
736+
const seedDataChild: CacheNodeSeedData | null =
737+
seedDataChildren !== null
738+
? (seedDataChildren[parallelRouteKey] ?? null)
739+
: null
740+
const prefetchDataChild: CacheNodeSeedData | null =
697741
prefetchDataChildren !== null
698-
? prefetchDataChildren[parallelRouteKey]
742+
? (prefetchDataChildren[parallelRouteKey] ?? null)
699743
: null
700744
const existingSegmentMapChild =
701745
existingCacheNodeChildren !== null
@@ -719,6 +763,8 @@ function createCacheNodeOnNavigation(
719763
navigatedAt,
720764
routerStateChild,
721765
existingCacheNodeChild,
766+
seedDataChild,
767+
seedHead,
722768
prefetchDataChild,
723769
possiblyPartialPrefetchHead,
724770
isPrefetchHeadPartial,

packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,24 @@ export function navigateReducer(
164164
return handleExternalUrl(state, mutable, href, pendingPush)
165165
}
166166

167-
// Temporary glue code between the router reducer and the new navigation
168-
// implementation. Eventually we'll rewrite the router reducer to a
169-
// state machine.
170167
const currentUrl = new URL(state.canonicalUrl, location.origin)
168+
const seedRoute = null
169+
const seedData = null
170+
const seedHead = null
171171
const result = navigateUsingSegmentCache(
172172
url,
173173
currentUrl,
174174
state.cache,
175175
state.tree,
176176
state.nextUrl,
177+
seedRoute,
178+
seedData,
179+
seedHead,
177180
shouldScroll,
178181
mutable
179182
)
183+
// Temporary glue code between the router reducer and the new navigation
184+
// implementation. Eventually we'll rewrite the router reducer to a
185+
// state machine.
180186
return handleNavigationResult(url, state, mutable, pendingPush, result)
181187
}

packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,30 @@ import type {
66
} from '../router-reducer-types'
77
import { handleNavigationResult } from './navigate-reducer'
88
import { refresh as refreshUsingSegmentCache } from '../../segment-cache/navigation'
9+
import { revalidateEntireCache } from '../../segment-cache/cache'
910

1011
export function refreshReducer(
1112
state: ReadonlyReducerState,
1213
action: RefreshAction
1314
): ReducerState {
15+
// Client-initiated refreshes (router.refresh()) purge the entire
16+
// prefetch cache. This is unlike the Server Action-based refresh(), which
17+
// only purges the dynamic data cache.
18+
const currentNextUrl = state.nextUrl
19+
const currentRouterState = state.tree
20+
revalidateEntireCache(currentNextUrl, currentRouterState)
21+
1422
const currentUrl = new URL(state.canonicalUrl, action.origin)
23+
const seedData = null
24+
const seedHead = null
1525
const result = refreshUsingSegmentCache(
1626
currentUrl,
17-
state.tree,
18-
state.nextUrl,
27+
currentRouterState,
28+
currentNextUrl,
1929
state.renderedSearch,
20-
state.canonicalUrl
30+
state.canonicalUrl,
31+
seedData,
32+
seedHead
2133
)
2234

2335
const mutable: Mutable = {}

0 commit comments

Comments
 (0)