UNPKG

next

Version:

The React Framework

341 lines (339 loc) 16.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "navigate", { enumerable: true, get: function() { return navigate; } }); const _fetchserverresponse = require("../router-reducer/fetch-server-response"); const _pprnavigations = require("../router-reducer/ppr-navigations"); const _createhreffromurl = require("../router-reducer/create-href-from-url"); const _cache = require("./cache"); const _cachekey = require("./cache-key"); const _segment = require("../../../shared/lib/segment"); const _segmentcache = require("../segment-cache"); function navigate(url, currentCacheNode, currentFlightRouterState, nextUrl, shouldScroll) { const now = Date.now(); const href = url.href; // We special case navigations to the exact same URL as the current location. // It's a common UI pattern for apps to refresh when you click a link to the // current page. So when this happens, we refresh the dynamic data in the page // segments. // // Note that this does not apply if the any part of the hash or search query // has changed. This might feel a bit weird but it makes more sense when you // consider that the way to trigger this behavior is to click the same link // multiple times. // // TODO: We should probably refresh the *entire* route when this case occurs, // not just the page segments. Essentially treating it the same as a refresh() // triggered by an action, which is the more explicit way of modeling the UI // pattern described above. // // Also note that this only refreshes the dynamic data, not static/ cached // data. If the page segment is fully static and prefetched, the request is // skipped. (This is also how refresh() works.) const isSamePageNavigation = // TODO: This is not the only place we read from the location, but we should // consider storing the current URL in the router state instead of reading // from the location object. In practice I don't think this matters much // since we keep them in sync anyway, but having two sources of truth can // lead to subtle bugs and race conditions. href === window.location.href; const cacheKey = (0, _cachekey.createCacheKey)(href, nextUrl); const route = (0, _cache.readRouteCacheEntry)(now, cacheKey); if (route !== null && route.status === _cache.EntryStatus.Fulfilled) { // We have a matching prefetch. const snapshot = readRenderSnapshotFromCache(now, cacheKey, route.tree); const prefetchFlightRouterState = snapshot.flightRouterState; const prefetchSeedData = snapshot.seedData; const prefetchHead = route.head; const isPrefetchHeadPartial = route.isHeadPartial; const newCanonicalUrl = route.canonicalUrl; return navigateUsingPrefetchedRouteTree(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, shouldScroll, url.hash); } // There's no matching prefetch for this route in the cache. return { tag: _segmentcache.NavigationResultTag.Async, data: navigateDynamicallyWithNoPrefetch(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, shouldScroll, url.hash) }; } function navigateUsingPrefetchedRouteTree(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, canonicalUrl, shouldScroll, hash) { // Recursively construct a prefetch tree by reading from the Segment Cache. To // maintain compatibility, we output the same data structures as the old // prefetching implementation: FlightRouterState and CacheNodeSeedData. // TODO: Eventually updateCacheNodeOnNavigation (or the equivalent) should // read from the Segment Cache directly. It's only structured this way for now // so we can share code with the old prefetching implementation. const scrollableSegments = []; const task = (0, _pprnavigations.startPPRNavigation)(now, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, scrollableSegments); if (task !== null) { const dynamicRequestTree = task.dynamicRequestTree; if (dynamicRequestTree !== null) { const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(url, { flightRouterState: dynamicRequestTree, nextUrl }); (0, _pprnavigations.listenForDynamicRequest)(task, promiseForDynamicServerResponse); } else { // The prefetched tree does not contain dynamic holes — it's // fully static. We can skip the dynamic request. } return navigationTaskToResult(task, currentCacheNode, canonicalUrl, scrollableSegments, shouldScroll, hash); } // The server sent back an empty tree patch. There's nothing to update, except // possibly the URL. return { tag: _segmentcache.NavigationResultTag.NoOp, data: { canonicalUrl, shouldScroll } }; } function navigationTaskToResult(task, currentCacheNode, canonicalUrl, scrollableSegments, shouldScroll, hash) { const flightRouterState = task.route; if (flightRouterState === null) { // When no router state is provided, it signals that we should perform an // MPA navigation. return { tag: _segmentcache.NavigationResultTag.MPA, data: canonicalUrl }; } const newCacheNode = task.node; return { tag: _segmentcache.NavigationResultTag.Success, data: { flightRouterState, cacheNode: newCacheNode !== null ? newCacheNode : currentCacheNode, canonicalUrl, scrollableSegments, shouldScroll, hash } }; } function readRenderSnapshotFromCache(now, routeCacheKey, tree) { let childRouterStates = {}; let childSeedDatas = {}; const slots = tree.slots; if (slots !== null) { for(const parallelRouteKey in slots){ const childTree = slots[parallelRouteKey]; const childResult = readRenderSnapshotFromCache(now, routeCacheKey, childTree); childRouterStates[parallelRouteKey] = childResult.flightRouterState; childSeedDatas[parallelRouteKey] = childResult.seedData; } } let rsc = null; let loading = null; let isPartial = true; const segmentEntry = (0, _cache.readSegmentCacheEntry)(now, routeCacheKey, tree.key); if (segmentEntry !== null) { switch(segmentEntry.status){ case _cache.EntryStatus.Fulfilled: { // Happy path: a cache hit rsc = segmentEntry.rsc; loading = segmentEntry.loading; isPartial = segmentEntry.isPartial; break; } case _cache.EntryStatus.Pending: { // We haven't received data for this segment yet, but there's already // an in-progress request. Since it's extremely likely to arrive // before the dynamic data response, we might as well use it. const promiseForFulfilledEntry = (0, _cache.waitForSegmentCacheEntry)(segmentEntry); rsc = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.rsc : null); loading = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.loading : null); // Since we don't know yet whether the segment is partial or fully // static, we must assume it's partial; we can't skip the // dynamic request. isPartial = true; break; } case _cache.EntryStatus.Empty: case _cache.EntryStatus.Rejected: break; default: segmentEntry; } } const segment = tree.segment === _segment.PAGE_SEGMENT_KEY && routeCacheKey.search ? // included in the segment. However, the Segment Cache tracks search // params separately from the rest of the segment key. So we need to // add them back here. // // See corresponding comment in convertFlightRouterStateToTree. // // TODO: What we should do instead is update the navigation diffing // logic to compare search params explicitly. This is a temporary // solution until more of the Segment Cache implementation has settled. (0, _segment.addSearchParamsIfPageSegment)(tree.segment, Object.fromEntries(new URLSearchParams(routeCacheKey.search))) : tree.segment; return { flightRouterState: [ segment, childRouterStates, null, null, tree.isRootLayout ], seedData: [ segment, rsc, childSeedDatas, loading, isPartial ] }; } async function navigateDynamicallyWithNoPrefetch(now, url, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, shouldScroll, hash) { // Runs when a navigation happens but there's no cached prefetch we can use. // Don't bother to wait for a prefetch response; go straight to a full // navigation that contains both static and dynamic data in a single stream. // (This is unlike the old navigation implementation, which instead blocks // the dynamic request until a prefetch request is received.) // // To avoid duplication of logic, we're going to pretend that the tree // returned by the dynamic request is, in fact, a prefetch tree. Then we can // use the same server response to write the actual data into the CacheNode // tree. So it's the same flow as the "happy path" (prefetch, then // navigation), except we use a single server response for both stages. const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(url, { flightRouterState: currentFlightRouterState, nextUrl }); const { flightData, canonicalUrl: canonicalUrlOverride } = await promiseForDynamicServerResponse; if (typeof flightData === 'string') { // This is an MPA navigation. const newUrl = flightData; return { tag: _segmentcache.NavigationResultTag.MPA, data: newUrl }; } // Since the response format of dynamic requests and prefetches is slightly // different, we'll need to massage the data a bit. Create FlightRouterState // tree that simulates what we'd receive as the result of a prefetch. const prefetchFlightRouterState = simulatePrefetchTreeUsingDynamicTreePatch(currentFlightRouterState, flightData); // In our simulated prefetch payload, we pretend that there's no seed data // nor a prefetch head. const prefetchSeedData = null; const prefetchHead = null; const isPrefetchHeadPartial = true; const canonicalUrl = (0, _createhreffromurl.createHrefFromUrl)(canonicalUrlOverride ? canonicalUrlOverride : url); // Now we proceed exactly as we would for normal navigation. const scrollableSegments = []; const task = (0, _pprnavigations.startPPRNavigation)(now, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, scrollableSegments); if (task !== null) { // In this case, we've already sent the dynamic request, so we don't // actually use the request tree created by `startPPRNavigation`, // except to check if it contains dynamic holes. // // This is almost always true, but it could be false if all the segment data // was present in the cache, but the route tree was not. E.g. navigating // to a URL that was not prefetched but rewrites to a different URL // that was. const hasDynamicHoles = task.dynamicRequestTree !== null; if (hasDynamicHoles) { (0, _pprnavigations.listenForDynamicRequest)(task, promiseForDynamicServerResponse); } else { // The prefetched tree does not contain dynamic holes — it's // fully static. We don't need to process the server response further. } return navigationTaskToResult(task, currentCacheNode, canonicalUrl, scrollableSegments, shouldScroll, hash); } // The server sent back an empty tree patch. There's nothing to update, except // possibly the URL. return { tag: _segmentcache.NavigationResultTag.NoOp, data: { canonicalUrl, shouldScroll } }; } function simulatePrefetchTreeUsingDynamicTreePatch(currentTree, flightData) { // Takes the current FlightRouterState and applies the router state patch // received from the server, to create a full FlightRouterState tree that we // can pretend was returned by a prefetch. // // (It sounds similar to what applyRouterStatePatch does, but it doesn't need // to handle stuff like interception routes or diffing since that will be // handled later.) let baseTree = currentTree; for (const { segmentPath, tree: treePatch } of flightData){ // If the server sends us multiple tree patches, we only need to clone the // base tree when applying the first patch. After the first patch, we can // apply the remaining patches in place without copying. const canMutateInPlace = baseTree !== currentTree; baseTree = simulatePrefetchTreeUsingDynamicTreePatchImpl(baseTree, treePatch, segmentPath, canMutateInPlace, 0); } return baseTree; } function simulatePrefetchTreeUsingDynamicTreePatchImpl(baseRouterState, patch, segmentPath, canMutateInPlace, index) { if (index === segmentPath.length) { // We reached the part of the tree that we need to patch. return patch; } // segmentPath represents the parent path of subtree. It's a repeating // pattern of parallel route key and segment: // // [string, Segment, string, Segment, string, Segment, ...] // // This path tells us which part of the base tree to apply the tree patch. // // NOTE: In the case of a fully dynamic request with no prefetch, we receive // the FlightRouterState patch in the same request as the dynamic data. // Therefore we don't need to worry about diffing the segment values; we can // assume the server sent us a correct result. const updatedParallelRouteKey = segmentPath[index]; // const segment: Segment = segmentPath[index + 1] <-- Not used, see note above const baseChildren = baseRouterState[1]; const newChildren = {}; for(const parallelRouteKey in baseChildren){ if (parallelRouteKey === updatedParallelRouteKey) { const childBaseRouterState = baseChildren[parallelRouteKey]; newChildren[parallelRouteKey] = simulatePrefetchTreeUsingDynamicTreePatchImpl(childBaseRouterState, patch, segmentPath, canMutateInPlace, // Advance the index by two and keep cloning until we reach // the end of the segment path. index + 2); } else { // This child is not being patched. Copy it over as-is. newChildren[parallelRouteKey] = baseChildren[parallelRouteKey]; } } if (canMutateInPlace) { // We can mutate the base tree in place, because the base tree is already // a clone. baseRouterState[1] = newChildren; return baseRouterState; } // Clone all the fields except the children. // // Based on equivalent logic in apply-router-state-patch-to-tree, but should // confirm whether we need to copy all of these fields. Not sure the server // ever sends, e.g. the refetch marker. const clone = [ baseRouterState[0], newChildren ]; if (2 in baseRouterState) { clone[2] = baseRouterState[2]; } if (3 in baseRouterState) { clone[3] = baseRouterState[3]; } if (4 in baseRouterState) { clone[4] = baseRouterState[4]; } return clone; } 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=navigation.js.map