Skip to content

Commit

Permalink
Fork navigateReducer into PPR and non-PPR versions (#59538)
Browse files Browse the repository at this point in the history
The PPR implementation of navigateReducer is expected to diverge
significantly from the existing, non-PPR implementation. So this forks
them into two separate functions. This will be easier to maintain than
two different implementations inside the same function, especially
considering we don't expect any more changes to the non-PPR
implementation.

This also reduces the chances we'll introduce an accidental regression
into the non-PPR version, which is the stable one that all users (except
for the ones dogfooding PPR) are currently using.

For now, the two implementations are identical. I'll start making
changes in subsequent PRs.

Only one implementation will be included in the final build; the other
one will be dead code eliminated because the feature check is statically
inlined at build time:

```js
export const navigateReducer = process.env.__NEXT_PPR
  ? navigateReducer_PPR
  : navigateReducer_noPPR
```

Closes NEXT-1856
  • Loading branch information
acdlite committed Dec 13, 2023
1 parent 55645ff commit 9dfeced
Showing 1 changed file with 205 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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]) => {
// 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 {
Expand Down

0 comments on commit 9dfeced

Please sign in to comment.