Skip to content
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

Merged
merged 1 commit into from
Dec 13, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Member

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 effort

Copy link
Contributor Author

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

? 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]) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be safe to remove the postponed handling in the non-PPR case, but I don't mind it being a separate PR if that's the plan 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Expand Down