390 lines
21 KiB
JavaScript
390 lines
21 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
0 && (module.exports = {
|
|
handleExternalUrl: null,
|
|
navigateReducer: null
|
|
});
|
|
function _export(target, all) {
|
|
for(var name in all)Object.defineProperty(target, name, {
|
|
enumerable: true,
|
|
get: all[name]
|
|
});
|
|
}
|
|
_export(exports, {
|
|
handleExternalUrl: function() {
|
|
return handleExternalUrl;
|
|
},
|
|
navigateReducer: function() {
|
|
return navigateReducer;
|
|
}
|
|
});
|
|
const _fetchserverresponse = require("../fetch-server-response");
|
|
const _createhreffromurl = require("../create-href-from-url");
|
|
const _invalidatecachebelowflightsegmentpath = require("../invalidate-cache-below-flight-segmentpath");
|
|
const _applyrouterstatepatchtotree = require("../apply-router-state-patch-to-tree");
|
|
const _shouldhardnavigate = require("../should-hard-navigate");
|
|
const _isnavigatingtonewrootlayout = require("../is-navigating-to-new-root-layout");
|
|
const _routerreducertypes = require("../router-reducer-types");
|
|
const _handlemutable = require("../handle-mutable");
|
|
const _applyflightdata = require("../apply-flight-data");
|
|
const _prefetchreducer = require("./prefetch-reducer");
|
|
const _approuter = require("../../app-router");
|
|
const _segment = require("../../../../shared/lib/segment");
|
|
const _pprnavigations = require("../ppr-navigations");
|
|
const _prefetchcacheutils = require("../prefetch-cache-utils");
|
|
const _clearcachenodedataforsegmentpath = require("../clear-cache-node-data-for-segment-path");
|
|
function handleExternalUrl(state, mutable, url, pendingPush) {
|
|
mutable.mpaNavigation = true;
|
|
mutable.canonicalUrl = url;
|
|
mutable.pendingPush = pendingPush;
|
|
mutable.scrollableSegments = undefined;
|
|
return (0, _handlemutable.handleMutable)(state, mutable);
|
|
}
|
|
function generateSegmentsFromPatch(flightRouterPatch) {
|
|
const segments = [];
|
|
const [segment, parallelRoutes] = flightRouterPatch;
|
|
if (Object.keys(parallelRoutes).length === 0) {
|
|
return [
|
|
[
|
|
segment
|
|
]
|
|
];
|
|
}
|
|
for (const [parallelRouteKey, parallelRoute] of Object.entries(parallelRoutes)){
|
|
for (const childSegment of generateSegmentsFromPatch(parallelRoute)){
|
|
// If the segment is empty, it means we are at the root of the tree
|
|
if (segment === "") {
|
|
segments.push([
|
|
parallelRouteKey,
|
|
...childSegment
|
|
]);
|
|
} else {
|
|
segments.push([
|
|
segment,
|
|
parallelRouteKey,
|
|
...childSegment
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
return segments;
|
|
}
|
|
function triggerLazyFetchForLeafSegments(newCache, currentCache, flightSegmentPath, treePatch) {
|
|
let appliedPatch = false;
|
|
newCache.rsc = currentCache.rsc;
|
|
newCache.prefetchRsc = currentCache.prefetchRsc;
|
|
newCache.loading = currentCache.loading;
|
|
newCache.parallelRoutes = new Map(currentCache.parallelRoutes);
|
|
const segmentPathsToFill = generateSegmentsFromPatch(treePatch).map((segment)=>[
|
|
...flightSegmentPath,
|
|
...segment
|
|
]);
|
|
for (const segmentPaths of segmentPathsToFill){
|
|
(0, _clearcachenodedataforsegmentpath.clearCacheNodeDataForSegmentPath)(newCache, currentCache, segmentPaths);
|
|
appliedPatch = true;
|
|
}
|
|
return appliedPatch;
|
|
}
|
|
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, action) {
|
|
const { url, isExternalUrl, navigateType, shouldScroll } = action;
|
|
const mutable = {};
|
|
const { hash } = url;
|
|
const href = (0, _createhreffromurl.createHrefFromUrl)(url);
|
|
const pendingPush = navigateType === "push";
|
|
// we want to prune the prefetch cache on every navigation to avoid it growing too large
|
|
(0, _prefetchcacheutils.prunePrefetchCache)(state.prefetchCache);
|
|
mutable.preserveCustomHistoryState = false;
|
|
if (isExternalUrl) {
|
|
return handleExternalUrl(state, mutable, url.toString(), pendingPush);
|
|
}
|
|
const prefetchValues = (0, _prefetchcacheutils.getOrCreatePrefetchCacheEntry)({
|
|
url,
|
|
nextUrl: state.nextUrl,
|
|
tree: state.tree,
|
|
buildId: state.buildId,
|
|
prefetchCache: state.prefetchCache
|
|
});
|
|
const { treeAtTimeOfPrefetch, data } = prefetchValues;
|
|
_prefetchreducer.prefetchQueue.bump(data);
|
|
return data.then((param)=>{
|
|
let [flightData, canonicalUrlOverride] = param;
|
|
let isFirstRead = false;
|
|
// we only want to mark this once
|
|
if (!prefetchValues.lastUsedTime) {
|
|
// important: we should only mark the cache node as dirty after we unsuspend from the call above
|
|
prefetchValues.lastUsedTime = Date.now();
|
|
isFirstRead = true;
|
|
}
|
|
// Handle case when navigating to page in `pages` from `app`
|
|
if (typeof flightData === "string") {
|
|
return handleExternalUrl(state, mutable, flightData, pendingPush);
|
|
}
|
|
// Handles case where `<meta http-equiv="refresh">` tag is present,
|
|
// which will trigger an MPA navigation.
|
|
if (document.getElementById("__next-page-redirect")) {
|
|
return handleExternalUrl(state, mutable, href, pendingPush);
|
|
}
|
|
let currentTree = state.tree;
|
|
let currentCache = state.cache;
|
|
let scrollableSegments = [];
|
|
for (const flightDataPath of flightData){
|
|
const flightSegmentPath = flightDataPath.slice(0, -4);
|
|
// The one before last item is the router state tree patch
|
|
const treePatch = flightDataPath.slice(-3)[0];
|
|
// TODO-APP: remove ''
|
|
const flightSegmentPathWithLeadingEmpty = [
|
|
"",
|
|
...flightSegmentPath
|
|
];
|
|
// Create new tree based on the flightSegmentPath and router state patch
|
|
let newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
|
|
flightSegmentPathWithLeadingEmpty, currentTree, treePatch, href);
|
|
// 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 = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
|
|
flightSegmentPathWithLeadingEmpty, treeAtTimeOfPrefetch, treePatch, href);
|
|
}
|
|
if (newTree !== null) {
|
|
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
|
|
return handleExternalUrl(state, mutable, href, pendingPush);
|
|
}
|
|
const cache = (0, _approuter.createEmptyCacheNode)();
|
|
let applied = false;
|
|
if (prefetchValues.status === _routerreducertypes.PrefetchCacheEntryStatus.stale && !isFirstRead) {
|
|
// When we have a stale prefetch entry, we only want to re-use the loading state of the route we're navigating to, to support instant loading navigations
|
|
// this will trigger a lazy fetch for the actual page data by nulling the `rsc` and `prefetchRsc` values for page data,
|
|
// while copying over the `loading` for the segment that contains the page data.
|
|
// We only do this on subsequent reads, as otherwise there'd be no loading data to re-use.
|
|
applied = triggerLazyFetchForLeafSegments(cache, currentCache, flightSegmentPath, treePatch);
|
|
// since we re-used the stale cache's loading state & refreshed the data,
|
|
// update the `lastUsedTime` so that it can continue to be re-used for the next 30s
|
|
prefetchValues.lastUsedTime = Date.now();
|
|
} else {
|
|
applied = (0, _applyflightdata.applyFlightData)(currentCache, cache, flightDataPath, prefetchValues);
|
|
}
|
|
const hardNavigate = (0, _shouldhardnavigate.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;
|
|
(0, _invalidatecachebelowflightsegmentpath.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;
|
|
// If we applied the cache, we update the "current cache" value so any other
|
|
// segments in the FlightDataPath will be able to reference the updated 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] !== _segment.DEFAULT_SEGMENT_KEY) {
|
|
scrollableSegments.push(scrollableSegmentPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mutable.patchedTree = currentTree;
|
|
mutable.canonicalUrl = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : href;
|
|
mutable.pendingPush = pendingPush;
|
|
mutable.scrollableSegments = scrollableSegments;
|
|
mutable.hashFragment = hash;
|
|
mutable.shouldScroll = shouldScroll;
|
|
return (0, _handlemutable.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, action) {
|
|
const { url, isExternalUrl, navigateType, shouldScroll } = action;
|
|
const mutable = {};
|
|
const { hash } = url;
|
|
const href = (0, _createhreffromurl.createHrefFromUrl)(url);
|
|
const pendingPush = navigateType === "push";
|
|
// we want to prune the prefetch cache on every navigation to avoid it growing too large
|
|
(0, _prefetchcacheutils.prunePrefetchCache)(state.prefetchCache);
|
|
mutable.preserveCustomHistoryState = false;
|
|
if (isExternalUrl) {
|
|
return handleExternalUrl(state, mutable, url.toString(), pendingPush);
|
|
}
|
|
const prefetchValues = (0, _prefetchcacheutils.getOrCreatePrefetchCacheEntry)({
|
|
url,
|
|
nextUrl: state.nextUrl,
|
|
tree: state.tree,
|
|
buildId: state.buildId,
|
|
prefetchCache: state.prefetchCache
|
|
});
|
|
const { treeAtTimeOfPrefetch, data } = prefetchValues;
|
|
_prefetchreducer.prefetchQueue.bump(data);
|
|
return data.then((param)=>{
|
|
let [flightData, canonicalUrlOverride, _postponed] = param;
|
|
let isFirstRead = false;
|
|
// we only want to mark this once
|
|
if (!prefetchValues.lastUsedTime) {
|
|
// important: we should only mark the cache node as dirty after we unsuspend from the call above
|
|
prefetchValues.lastUsedTime = Date.now();
|
|
isFirstRead = true;
|
|
}
|
|
// Handle case when navigating to page in `pages` from `app`
|
|
if (typeof flightData === "string") {
|
|
return handleExternalUrl(state, mutable, flightData, pendingPush);
|
|
}
|
|
// Handles case where `<meta http-equiv="refresh">` tag is present,
|
|
// which will trigger an MPA navigation.
|
|
if (document.getElementById("__next-page-redirect")) {
|
|
return handleExternalUrl(state, mutable, href, pendingPush);
|
|
}
|
|
let currentTree = state.tree;
|
|
let currentCache = state.cache;
|
|
let scrollableSegments = [];
|
|
// TODO: In practice, this is always a single item array. We probably
|
|
// aren't going to every send multiple segments, at least not in this
|
|
// format. So we could remove the extra wrapper for now until
|
|
// that settles.
|
|
for (const flightDataPath of flightData){
|
|
const flightSegmentPath = flightDataPath.slice(0, -4);
|
|
// The one before last item is the router state tree patch
|
|
const treePatch = flightDataPath.slice(-3)[0];
|
|
// TODO-APP: remove ''
|
|
const flightSegmentPathWithLeadingEmpty = [
|
|
"",
|
|
...flightSegmentPath
|
|
];
|
|
// Create new tree based on the flightSegmentPath and router state patch
|
|
let newTree = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
|
|
flightSegmentPathWithLeadingEmpty, currentTree, treePatch, href);
|
|
// 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 = (0, _applyrouterstatepatchtotree.applyRouterStatePatchToTree)(// TODO-APP: remove ''
|
|
flightSegmentPathWithLeadingEmpty, treeAtTimeOfPrefetch, treePatch, href);
|
|
}
|
|
if (newTree !== null) {
|
|
if ((0, _isnavigatingtonewrootlayout.isNavigatingToNewRootLayout)(currentTree, newTree)) {
|
|
return handleExternalUrl(state, mutable, href, pendingPush);
|
|
}
|
|
if (// This is just a paranoid check. When PPR is enabled, the server
|
|
// will always send back a static response that's rendered from
|
|
// the root. If for some reason it doesn't, we fall back to the
|
|
// non-PPR implementation.
|
|
// TODO: We should get rid of the else branch and do all navigations
|
|
// via updateCacheNodeOnNavigation. The current structure is just
|
|
// an incremental step.
|
|
flightDataPath.length === 3) {
|
|
const prefetchedTree = flightDataPath[0];
|
|
const seedData = flightDataPath[1];
|
|
const head = flightDataPath[2];
|
|
const task = (0, _pprnavigations.updateCacheNodeOnNavigation)(currentCache, currentTree, prefetchedTree, seedData, head);
|
|
if (task !== null && task.node !== null) {
|
|
// We've created a new Cache Node tree that contains a prefetched
|
|
// version of the next page. This can be rendered instantly.
|
|
// Use the tree computed by updateCacheNodeOnNavigation instead
|
|
// of the one computed by applyRouterStatePatchToTree.
|
|
// TODO: We should remove applyRouterStatePatchToTree
|
|
// from the PPR path entirely.
|
|
const patchedRouterState = task.route;
|
|
newTree = patchedRouterState;
|
|
const newCache = task.node;
|
|
// The prefetched tree has dynamic holes in it. We initiate a
|
|
// dynamic request to fill them in.
|
|
//
|
|
// Do not block on the result. We'll immediately render the Cache
|
|
// Node tree and suspend on the dynamic parts. When the request
|
|
// comes in, we'll fill in missing data and ping React to
|
|
// re-render. Unlike the lazy fetching model in the non-PPR
|
|
// implementation, this is modeled as a single React update +
|
|
// streaming, rather than multiple top-level updates. (However,
|
|
// even in the new model, we'll still need to sometimes update the
|
|
// root multiple times per navigation, like if the server sends us
|
|
// a different response than we expected. For now, we revert back
|
|
// to the lazy fetching mechanism in that case.)
|
|
(0, _pprnavigations.listenForDynamicRequest)(task, (0, _fetchserverresponse.fetchServerResponse)(url, currentTree, state.nextUrl, state.buildId));
|
|
mutable.cache = newCache;
|
|
} else {
|
|
// Nothing changed, so reuse the old cache.
|
|
// TODO: What if the head changed but not any of the segment data?
|
|
// Is that possible? If so, we should clone the whole tree and
|
|
// update the head.
|
|
newTree = prefetchedTree;
|
|
}
|
|
} else {
|
|
// The static response does not include any dynamic holes, so
|
|
// there's no need to do a second request.
|
|
// TODO: As an incremental step this just reverts back to the
|
|
// non-PPR implementation. We can simplify this branch further,
|
|
// given that PPR prefetches are always static and return the whole
|
|
// tree. Or in the meantime we could factor it out into a
|
|
// separate function.
|
|
const cache = (0, _approuter.createEmptyCacheNode)();
|
|
let applied = false;
|
|
if (prefetchValues.status === _routerreducertypes.PrefetchCacheEntryStatus.stale && !isFirstRead) {
|
|
// When we have a stale prefetch entry, we only want to re-use the loading state of the route we're navigating to, to support instant loading navigations
|
|
// this will trigger a lazy fetch for the actual page data by nulling the `rsc` and `prefetchRsc` values for page data,
|
|
// while copying over the `loading` for the segment that contains the page data.
|
|
// We only do this on subsequent reads, as otherwise there'd be no loading data to re-use.
|
|
applied = triggerLazyFetchForLeafSegments(cache, currentCache, flightSegmentPath, treePatch);
|
|
// since we re-used the stale cache's loading state & refreshed the data,
|
|
// update the `lastUsedTime` so that it can continue to be re-used for the next 30s
|
|
prefetchValues.lastUsedTime = Date.now();
|
|
} else {
|
|
applied = (0, _applyflightdata.applyFlightData)(currentCache, cache, flightDataPath, prefetchValues);
|
|
}
|
|
const hardNavigate = (0, _shouldhardnavigate.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;
|
|
(0, _invalidatecachebelowflightsegmentpath.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;
|
|
// If we applied the cache, we update the "current cache" value so any other
|
|
// segments in the FlightDataPath will be able to reference the updated 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] !== _segment.DEFAULT_SEGMENT_KEY) {
|
|
scrollableSegments.push(scrollableSegmentPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mutable.patchedTree = currentTree;
|
|
mutable.canonicalUrl = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : href;
|
|
mutable.pendingPush = pendingPush;
|
|
mutable.scrollableSegments = scrollableSegments;
|
|
mutable.hashFragment = hash;
|
|
mutable.shouldScroll = shouldScroll;
|
|
return (0, _handlemutable.handleMutable)(state, mutable);
|
|
}, ()=>state);
|
|
}
|
|
|
|
if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
|
|
Object.defineProperty(exports.default, '__esModule', { value: true });
|
|
Object.assign(exports.default, exports);
|
|
module.exports = exports.default;
|
|
}
|
|
|
|
//# sourceMappingURL=navigate-reducer.js.map
|