-
Notifications
You must be signed in to change notification settings - Fork 26.1k
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
Fork navigateReducer into PPR and non-PPR versions #59538
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -93,7 +93,211 @@ function addRefetchToLeafSegments( | |
|
||
return appliedPatch | ||
} | ||
export function navigateReducer( | ||
|
||
// These implementations are expected to diverge significantly, so I've forked | ||
// the entire function. The one that's disabled should be dead code eliminated | ||
// because the check here is statically inlined at build time. | ||
export const navigateReducer = process.env.__NEXT_PPR | ||
? navigateReducer_PPR | ||
: navigateReducer_noPPR | ||
|
||
// This is the implementation when PPR is disabled. We can assume its behavior | ||
// is relatively stable because it's been running in production for a while. | ||
function navigateReducer_noPPR( | ||
state: ReadonlyReducerState, | ||
action: NavigateAction | ||
): ReducerState { | ||
const { url, isExternalUrl, navigateType, shouldScroll } = action | ||
const mutable: Mutable = {} | ||
const { hash } = url | ||
const href = createHrefFromUrl(url) | ||
const pendingPush = navigateType === 'push' | ||
// we want to prune the prefetch cache on every navigation to avoid it growing too large | ||
prunePrefetchCache(state.prefetchCache) | ||
|
||
mutable.preserveCustomHistoryState = false | ||
|
||
if (isExternalUrl) { | ||
return handleExternalUrl(state, mutable, url.toString(), pendingPush) | ||
} | ||
|
||
let prefetchValues = state.prefetchCache.get(createHrefFromUrl(url, false)) | ||
|
||
// If we don't have a prefetch value, we need to create one | ||
if (!prefetchValues) { | ||
const data = fetchServerResponse( | ||
url, | ||
state.tree, | ||
state.nextUrl, | ||
state.buildId, | ||
// in dev, there's never gonna be a prefetch entry so we want to prefetch here | ||
// in order to simulate the behavior of the prefetch cache | ||
process.env.NODE_ENV === 'development' ? PrefetchKind.AUTO : undefined | ||
) | ||
|
||
const newPrefetchValue = { | ||
data, | ||
// this will make sure that the entry will be discarded after 30s | ||
kind: | ||
process.env.NODE_ENV === 'development' | ||
? PrefetchKind.AUTO | ||
: PrefetchKind.TEMPORARY, | ||
prefetchTime: Date.now(), | ||
treeAtTimeOfPrefetch: state.tree, | ||
lastUsedTime: null, | ||
} | ||
|
||
state.prefetchCache.set(createHrefFromUrl(url, false), newPrefetchValue) | ||
prefetchValues = newPrefetchValue | ||
} | ||
|
||
const prefetchEntryCacheStatus = getPrefetchEntryCacheStatus(prefetchValues) | ||
|
||
// The one before last item is the router state tree patch | ||
const { treeAtTimeOfPrefetch, data } = prefetchValues | ||
|
||
prefetchQueue.bump(data!) | ||
|
||
return data!.then( | ||
([flightData, canonicalUrlOverride, postponed]) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be safe to remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah I left it in the spirit of "do nothing except copy paste the entire implementation" |
||
// we only want to mark this once | ||
if (prefetchValues && !prefetchValues.lastUsedTime) { | ||
// important: we should only mark the cache node as dirty after we unsuspend from the call above | ||
prefetchValues.lastUsedTime = Date.now() | ||
} | ||
|
||
// Handle case when navigating to page in `pages` from `app` | ||
if (typeof flightData === 'string') { | ||
return handleExternalUrl(state, mutable, flightData, pendingPush) | ||
} | ||
|
||
let currentTree = state.tree | ||
let currentCache = state.cache | ||
let scrollableSegments: FlightSegmentPath[] = [] | ||
for (const flightDataPath of flightData) { | ||
const flightSegmentPath = flightDataPath.slice( | ||
0, | ||
-4 | ||
) as unknown as FlightSegmentPath | ||
// The one before last item is the router state tree patch | ||
const treePatch = flightDataPath.slice(-3)[0] as FlightRouterState | ||
|
||
// TODO-APP: remove '' | ||
const flightSegmentPathWithLeadingEmpty = ['', ...flightSegmentPath] | ||
|
||
// Create new tree based on the flightSegmentPath and router state patch | ||
let newTree = applyRouterStatePatchToTree( | ||
// TODO-APP: remove '' | ||
flightSegmentPathWithLeadingEmpty, | ||
currentTree, | ||
treePatch | ||
) | ||
|
||
// If the tree patch can't be applied to the current tree then we use the tree at time of prefetch | ||
// TODO-APP: This should instead fill in the missing pieces in `currentTree` with the data from `treeAtTimeOfPrefetch`, then apply the patch. | ||
if (newTree === null) { | ||
newTree = applyRouterStatePatchToTree( | ||
// TODO-APP: remove '' | ||
flightSegmentPathWithLeadingEmpty, | ||
treeAtTimeOfPrefetch, | ||
treePatch | ||
) | ||
} | ||
|
||
if (newTree !== null) { | ||
if (isNavigatingToNewRootLayout(currentTree, newTree)) { | ||
return handleExternalUrl(state, mutable, href, pendingPush) | ||
} | ||
|
||
const cache: CacheNode = createEmptyCacheNode() | ||
let applied = applyFlightData( | ||
currentCache, | ||
cache, | ||
flightDataPath, | ||
prefetchValues?.kind === 'auto' && | ||
prefetchEntryCacheStatus === PrefetchCacheEntryStatus.reusable | ||
) | ||
|
||
if ( | ||
(!applied && | ||
prefetchEntryCacheStatus === PrefetchCacheEntryStatus.stale) || | ||
// TODO-APP: If the prefetch was postponed, we don't want to apply it | ||
// until we land router changes to handle the postponed case. | ||
postponed | ||
) { | ||
applied = addRefetchToLeafSegments( | ||
cache, | ||
currentCache, | ||
flightSegmentPath, | ||
treePatch, | ||
// eslint-disable-next-line no-loop-func | ||
() => | ||
fetchServerResponse( | ||
url, | ||
currentTree, | ||
state.nextUrl, | ||
state.buildId | ||
) | ||
) | ||
} | ||
|
||
const hardNavigate = shouldHardNavigate( | ||
// TODO-APP: remove '' | ||
flightSegmentPathWithLeadingEmpty, | ||
currentTree | ||
) | ||
|
||
if (hardNavigate) { | ||
// Copy rsc for the root node of the cache. | ||
cache.rsc = currentCache.rsc | ||
cache.prefetchRsc = currentCache.prefetchRsc | ||
|
||
invalidateCacheBelowFlightSegmentPath( | ||
cache, | ||
currentCache, | ||
flightSegmentPath | ||
) | ||
// Ensure the existing cache value is used when the cache was not invalidated. | ||
mutable.cache = cache | ||
} else if (applied) { | ||
mutable.cache = cache | ||
} | ||
|
||
currentCache = cache | ||
currentTree = newTree | ||
|
||
for (const subSegment of generateSegmentsFromPatch(treePatch)) { | ||
const scrollableSegmentPath = [...flightSegmentPath, ...subSegment] | ||
// Filter out the __DEFAULT__ paths as they shouldn't be scrolled to in this case. | ||
if ( | ||
scrollableSegmentPath[scrollableSegmentPath.length - 1] !== | ||
'__DEFAULT__' | ||
) { | ||
scrollableSegments.push(scrollableSegmentPath) | ||
} | ||
} | ||
} | ||
} | ||
|
||
mutable.patchedTree = currentTree | ||
mutable.canonicalUrl = canonicalUrlOverride | ||
? createHrefFromUrl(canonicalUrlOverride) | ||
: href | ||
mutable.pendingPush = pendingPush | ||
mutable.scrollableSegments = scrollableSegments | ||
mutable.hashFragment = hash | ||
mutable.shouldScroll = shouldScroll | ||
|
||
return handleMutable(state, mutable) | ||
}, | ||
() => state | ||
) | ||
} | ||
|
||
// This is the experimental PPR implementation. It's closer to the behavior we | ||
// want, but is likelier to include accidental regressions because it rewrites | ||
// existing functionality. | ||
function navigateReducer_PPR( | ||
state: ReadonlyReducerState, | ||
action: NavigateAction | ||
): ReducerState { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wonder if it'd be useful to annotate the action type that we dispatch to devtools with something like
navigate (ppr)
just to sanity check which reducer is being used on the client. But I imagine it'll be pretty obvious which one is being used without that once more functionality is wired up, so maybe not worth the effortThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah you can just log in the reducer, or log process.env.__NEXT_PPR, since it's a static condition