UNPKG

next

Version:

The React Framework

380 lines (378 loc) 20.9 kB
"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"); const _aliasedprefetchnavigations = require("../aliased-prefetch-navigations"); const _segmentcache = require("../../segment-cache"); 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; } function handleNavigationResult(url, state, mutable, pendingPush, result) { switch(result.tag){ case _segmentcache.NavigationResultTag.MPA: { // Perform an MPA navigation. const newUrl = result.data; return handleExternalUrl(state, mutable, newUrl, pendingPush); } case _segmentcache.NavigationResultTag.NoOp: { // The server responded with no change to the current page. However, if // the URL changed, we still need to update that. const newCanonicalUrl = result.data.canonicalUrl; mutable.canonicalUrl = newCanonicalUrl; // Check if the only thing that changed was the hash fragment. const oldUrl = new URL(state.canonicalUrl, url); const onlyHashChange = // We don't need to compare the origins, because client-driven // navigations are always same-origin. url.pathname === oldUrl.pathname && url.search === oldUrl.search && url.hash !== oldUrl.hash; if (onlyHashChange) { // The only updated part of the URL is the hash. mutable.onlyHashChange = true; mutable.shouldScroll = result.data.shouldScroll; mutable.hashFragment = url.hash; // Setting this to an empty array triggers a scroll for all new and // updated segments. See `ScrollAndFocusHandler` for more details. mutable.scrollableSegments = []; } return (0, _handlemutable.handleMutable)(state, mutable); } case _segmentcache.NavigationResultTag.Success: { // Received a new result. mutable.cache = result.data.cacheNode; mutable.patchedTree = result.data.flightRouterState; mutable.canonicalUrl = result.data.canonicalUrl; mutable.scrollableSegments = result.data.scrollableSegments; mutable.shouldScroll = result.data.shouldScroll; mutable.hashFragment = result.data.hash; return (0, _handlemutable.handleMutable)(state, mutable); } case _segmentcache.NavigationResultTag.Async: { return result.data.then((asyncResult)=>handleNavigationResult(url, state, mutable, pendingPush, asyncResult), // If the navigation failed, return the current state. // TODO: This matches the current behavior but we need to do something // better here if the network fails. ()=>{ return state; }); } default: { result; return state; } } } function navigateReducer(state, action) { const { url, isExternalUrl, navigateType, shouldScroll, allowAliasing } = 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; mutable.pendingPush = pendingPush; if (isExternalUrl) { return handleExternalUrl(state, mutable, url.toString(), 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); } if (process.env.__NEXT_CLIENT_SEGMENT_CACHE) { // (Very Early Experimental Feature) Segment Cache // // Bypass the normal prefetch cache and use the new per-segment cache // implementation instead. This is only supported if PPR is enabled, too. // // Temporary glue code between the router reducer and the new navigation // implementation. Eventually we'll rewrite the router reducer to a // state machine. const result = (0, _segmentcache.navigate)(url, state.cache, state.tree, state.nextUrl, shouldScroll); return handleNavigationResult(url, state, mutable, pendingPush, result); } const prefetchValues = (0, _prefetchcacheutils.getOrCreatePrefetchCacheEntry)({ url, nextUrl: state.nextUrl, tree: state.tree, prefetchCache: state.prefetchCache, allowAliasing }); const { treeAtTimeOfPrefetch, data } = prefetchValues; _prefetchreducer.prefetchQueue.bump(data); return data.then((param)=>{ let { flightData, canonicalUrl: canonicalUrlOverride, postponed } = param; const navigatedAt = Date.now(); 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 = navigatedAt; isFirstRead = true; } if (prefetchValues.aliased) { const result = (0, _aliasedprefetchnavigations.handleAliasedPrefetchEntry)(navigatedAt, state, flightData, url, mutable); // We didn't return new router state because we didn't apply the aliased entry for some reason. // We'll re-invoke the navigation handler but ensure that we don't attempt to use the aliased entry. This // will create an on-demand prefetch entry. if (result === false) { return navigateReducer(state, { ...action, allowAliasing: false }); } return result; } // Handle case when navigating to page in `pages` from `app` if (typeof flightData === 'string') { return handleExternalUrl(state, mutable, flightData, pendingPush); } const updatedCanonicalUrl = canonicalUrlOverride ? (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride) : href; const onlyHashChange = !!hash && state.canonicalUrl.split('#', 1)[0] === updatedCanonicalUrl.split('#', 1)[0]; // If only the hash has changed, the server hasn't sent us any new data. We can just update // the mutable properties responsible for URL and scroll handling and return early. if (onlyHashChange) { mutable.onlyHashChange = true; mutable.canonicalUrl = updatedCanonicalUrl; mutable.shouldScroll = shouldScroll; mutable.hashFragment = hash; mutable.scrollableSegments = []; return (0, _handlemutable.handleMutable)(state, mutable); } let currentTree = state.tree; let currentCache = state.cache; let scrollableSegments = []; for (const normalizedFlightData of flightData){ const { pathToSegment: flightSegmentPath, seedData, head, isHeadPartial, isRootRender } = normalizedFlightData; let treePatch = normalizedFlightData.tree; // 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 (// This is just a paranoid check. When a route is PPRed, the server // will 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 startPPRNavigation. The current structure is just // an incremental step. seedData && isRootRender && postponed) { const task = (0, _pprnavigations.startPPRNavigation)(navigatedAt, currentCache, currentTree, treePatch, seedData, head, isHeadPartial, false, scrollableSegments); if (task !== null) { if (task.route === null) { // Detected a change to the root layout. Perform an full- // page navigation. return handleExternalUrl(state, mutable, href, pendingPush); } // Use the tree computed by startPPRNavigation 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; if (newCache !== null) { // We've created a new Cache Node tree that contains a prefetched // version of the next page. This can be rendered instantly. mutable.cache = newCache; } const dynamicRequestTree = task.dynamicRequestTree; if (dynamicRequestTree !== null) { // 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.) const dynamicRequest = (0, _fetchserverresponse.fetchServerResponse)(url, { flightRouterState: dynamicRequestTree, nextUrl: state.nextUrl }); (0, _pprnavigations.listenForDynamicRequest)(task, dynamicRequest); // We store the dynamic request on the `lazyData` property of the CacheNode // because we're not going to await the dynamic request here. Since we're not blocking // on the dynamic request, `layout-router` will // task.node.lazyData = dynamicRequest } else { // The prefetched tree does not contain dynamic holes — it's // fully static. We can skip the dynamic request. } } 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 = treePatch; } } 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. 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. // We skip this branch if only the hash fragment has changed, as we don't want to trigger a lazy fetch in that case 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 = navigatedAt; } else { applied = (0, _applyflightdata.applyFlightData)(navigatedAt, currentCache, cache, normalizedFlightData, 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; } 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); } } } currentTree = newTree; } } mutable.patchedTree = currentTree; mutable.canonicalUrl = updatedCanonicalUrl; 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