UNPKG

next

Version:

The React Framework

459 lines (457 loc) • 22.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { convertServerPatchToFullTree: null, navigate: null, navigateToSeededRoute: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { convertServerPatchToFullTree: function() { return convertServerPatchToFullTree; }, navigate: function() { return navigate; }, navigateToSeededRoute: function() { return navigateToSeededRoute; } }); 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 _types = require("./types"); function navigate(url, currentUrl, currentCacheNode, currentFlightRouterState, nextUrl, freshnessPolicy, shouldScroll, accumulation) { 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 = href === currentUrl.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, route, route.tree); const prefetchFlightRouterState = snapshot.flightRouterState; const prefetchSeedData = snapshot.seedData; const headSnapshot = readHeadSnapshotFromCache(now, route); const prefetchHead = headSnapshot.rsc; const isPrefetchHeadPartial = headSnapshot.isPartial; // TODO: The "canonicalUrl" stored in the cache doesn't include the hash, // because hash entries do not vary by hash fragment. However, the one // we set in the router state *does* include the hash, and it's used to // sync with the actual browser location. To make this less of a refactor // hazard, we should always track the hash separately from the rest of // the URL. const newCanonicalUrl = route.canonicalUrl + url.hash; const renderedSearch = route.renderedSearch; return navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, renderedSearch, freshnessPolicy, shouldScroll); } // There was no matching route tree in the cache. Let's see if we can // construct an "optimistic" route tree. // // Do not construct an optimistic route tree if there was a cache hit, but // the entry has a rejected status, since it may have been rejected due to a // rewrite or redirect based on the search params. // // TODO: There are multiple reasons a prefetch might be rejected; we should // track them explicitly and choose what to do here based on that. if (route === null || route.status !== _cache.EntryStatus.Rejected) { const optimisticRoute = (0, _cache.requestOptimisticRouteCacheEntry)(now, url, nextUrl); if (optimisticRoute !== null) { // We have an optimistic route tree. Proceed with the normal flow. const snapshot = readRenderSnapshotFromCache(now, optimisticRoute, optimisticRoute.tree); const prefetchFlightRouterState = snapshot.flightRouterState; const prefetchSeedData = snapshot.seedData; const headSnapshot = readHeadSnapshotFromCache(now, optimisticRoute); const prefetchHead = headSnapshot.rsc; const isPrefetchHeadPartial = headSnapshot.isPartial; const newCanonicalUrl = optimisticRoute.canonicalUrl + url.hash; const newRenderedSearch = optimisticRoute.renderedSearch; return navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, newCanonicalUrl, newRenderedSearch, freshnessPolicy, shouldScroll); } } // There's no matching prefetch for this route in the cache. let collectedDebugInfo = accumulation.collectedDebugInfo ?? []; if (accumulation.collectedDebugInfo === undefined) { collectedDebugInfo = accumulation.collectedDebugInfo = []; } return { tag: _types.NavigationResultTag.Async, data: navigateDynamicallyWithNoPrefetch(now, url, currentUrl, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, shouldScroll, collectedDebugInfo) }; } function navigateToSeededRoute(now, url, canonicalUrl, navigationSeed, currentUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, shouldScroll) { // A version of navigate() that accepts the target route tree as an argument // rather than reading it from the prefetch cache. const accumulation = { scrollableSegments: null, separateRefreshUrls: null }; const isSamePageNavigation = url.href === currentUrl.href; const task = (0, _pprnavigations.startPPRNavigation)(now, currentUrl, currentCacheNode, currentFlightRouterState, navigationSeed.tree, freshnessPolicy, navigationSeed.data, navigationSeed.head, null, null, false, isSamePageNavigation, accumulation); if (task !== null) { (0, _pprnavigations.spawnDynamicRequests)(task, url, nextUrl, freshnessPolicy, accumulation); return navigationTaskToResult(task, canonicalUrl, navigationSeed.renderedSearch, accumulation.scrollableSegments, shouldScroll, url.hash); } // Could not perform a SPA navigation. Revert to a full-page (MPA) navigation. return { tag: _types.NavigationResultTag.MPA, data: canonicalUrl }; } function navigateUsingPrefetchedRouteTree(now, url, currentUrl, nextUrl, isSamePageNavigation, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, canonicalUrl, renderedSearch, freshnessPolicy, shouldScroll) { // 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 accumulation = { scrollableSegments: null, separateRefreshUrls: null }; const seedData = null; const seedHead = null; const task = (0, _pprnavigations.startPPRNavigation)(now, currentUrl, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, freshnessPolicy, seedData, seedHead, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, isSamePageNavigation, accumulation); if (task !== null) { (0, _pprnavigations.spawnDynamicRequests)(task, url, nextUrl, freshnessPolicy, accumulation); return navigationTaskToResult(task, canonicalUrl, renderedSearch, accumulation.scrollableSegments, shouldScroll, url.hash); } // Could not perform a SPA navigation. Revert to a full-page (MPA) navigation. return { tag: _types.NavigationResultTag.MPA, data: canonicalUrl }; } function navigationTaskToResult(task, canonicalUrl, renderedSearch, scrollableSegments, shouldScroll, hash) { return { tag: _types.NavigationResultTag.Success, data: { flightRouterState: task.route, cacheNode: task.node, canonicalUrl, renderedSearch, scrollableSegments, shouldScroll, hash } }; } function readRenderSnapshotFromCache(now, route, 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, route, childTree); childRouterStates[parallelRouteKey] = childResult.flightRouterState; childSeedDatas[parallelRouteKey] = childResult.seedData; } } let rsc = null; let loading = null; let isPartial = true; const segmentEntry = (0, _cache.readSegmentCacheEntry)(now, tree.varyPath); 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); // Because the request is still pending, we typically don't know yet // whether the response will be partial. We shouldn't skip this segment // during the dynamic navigation request. Otherwise, we might need to // do yet another request to fill in the remaining data, creating // a waterfall. // // The one exception is if this segment is being fetched with via // prefetch={true} (i.e. the "force stale" or "full" strategy). If so, // we can assume the response will be full. This field is set to `false` // for such segments. isPartial = segmentEntry.isPartial; break; } case _cache.EntryStatus.Empty: case _cache.EntryStatus.Rejected: break; default: segmentEntry; } } // The navigation implementation expects the search params to be // 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. const segment = (0, _segment.addSearchParamsIfPageSegment)(tree.segment, Object.fromEntries(new URLSearchParams(route.renderedSearch))); // We don't need this information in a render snapshot, so this can just be a placeholder. const hasRuntimePrefetch = false; return { flightRouterState: [ segment, childRouterStates, null, null, tree.isRootLayout ], seedData: [ rsc, childSeedDatas, loading, isPartial, hasRuntimePrefetch ] }; } function readHeadSnapshotFromCache(now, route) { // Same as readRenderSnapshotFromCache, but for the head let rsc = null; let isPartial = true; const segmentEntry = (0, _cache.readSegmentCacheEntry)(now, route.metadata.varyPath); if (segmentEntry !== null) { switch(segmentEntry.status){ case _cache.EntryStatus.Fulfilled: { rsc = segmentEntry.rsc; isPartial = segmentEntry.isPartial; break; } case _cache.EntryStatus.Pending: { const promiseForFulfilledEntry = (0, _cache.waitForSegmentCacheEntry)(segmentEntry); rsc = promiseForFulfilledEntry.then((entry)=>entry !== null ? entry.rsc : null); isPartial = segmentEntry.isPartial; break; } case _cache.EntryStatus.Empty: case _cache.EntryStatus.Rejected: break; default: segmentEntry; } } return { rsc, isPartial }; } // Used to request all the dynamic data for a route, rather than just a subset, // e.g. during a refresh or a revalidation. Typically this gets constructed // during the normal flow when diffing the route tree, but for an unprefetched // navigation, where we don't know the structure of the target route, we use // this instead. const DynamicRequestTreeForEntireRoute = [ '', {}, null, 'refetch' ]; async function navigateDynamicallyWithNoPrefetch(now, url, currentUrl, nextUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, shouldScroll, collectedDebugInfo) { // 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. let dynamicRequestTree; switch(freshnessPolicy){ case _pprnavigations.FreshnessPolicy.Default: case _pprnavigations.FreshnessPolicy.HistoryTraversal: dynamicRequestTree = currentFlightRouterState; break; case _pprnavigations.FreshnessPolicy.Hydration: case _pprnavigations.FreshnessPolicy.RefreshAll: case _pprnavigations.FreshnessPolicy.HMRRefresh: dynamicRequestTree = DynamicRequestTreeForEntireRoute; break; default: freshnessPolicy; dynamicRequestTree = currentFlightRouterState; break; } const promiseForDynamicServerResponse = (0, _fetchserverresponse.fetchServerResponse)(url, { flightRouterState: dynamicRequestTree, nextUrl }); const result = await promiseForDynamicServerResponse; if (typeof result === 'string') { // This is an MPA navigation. const newUrl = result; return { tag: _types.NavigationResultTag.MPA, data: newUrl }; } const { flightData, canonicalUrl, renderedSearch, debugInfo: debugInfoFromResponse } = result; if (debugInfoFromResponse !== null) { collectedDebugInfo.push(...debugInfoFromResponse); } // 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 navigationSeed = convertServerPatchToFullTree(currentFlightRouterState, flightData, renderedSearch); return navigateToSeededRoute(now, url, (0, _createhreffromurl.createHrefFromUrl)(canonicalUrl), navigationSeed, currentUrl, currentCacheNode, currentFlightRouterState, freshnessPolicy, nextUrl, shouldScroll); } function convertServerPatchToFullTree(currentTree, flightData, renderedSearch) { // During a client navigation or prefetch, the server sends back only a patch // for the parts of the tree that have changed. // // This applies the patch to the base tree to create a full representation of // the resulting tree. // // The return type includes a full FlightRouterState tree and a full // CacheNodeSeedData tree. (Conceptually these are the same tree, and should // eventually be unified, but there's still lots of existing code that // operates on FlightRouterState trees alone without the CacheNodeSeedData.) // // TODO: This similar to what apply-router-state-patch-to-tree does. It // will eventually fully replace it. We should get rid of all the remaining // places where we iterate over the server patch format. This should also // eventually replace normalizeFlightData. let baseTree = currentTree; let baseData = null; let head = null; for (const { segmentPath, tree: treePatch, seedData: dataPatch, head: headPatch } of flightData){ const result = convertServerPatchToFullTreeImpl(baseTree, baseData, treePatch, dataPatch, segmentPath, 0); baseTree = result.tree; baseData = result.data; // This is the same for all patches per response, so just pick an // arbitrary one head = headPatch; } return { tree: baseTree, data: baseData, renderedSearch, head }; } function convertServerPatchToFullTreeImpl(baseRouterState, baseData, treePatch, dataPatch, segmentPath, index) { if (index === segmentPath.length) { // We reached the part of the tree that we need to patch. return { tree: treePatch, data: dataPatch }; } // 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: We receive the FlightRouterState patch in the same request as the // seed data patch. 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 baseTreeChildren = baseRouterState[1]; const baseSeedDataChildren = baseData !== null ? baseData[1] : null; const newTreeChildren = {}; const newSeedDataChildren = {}; for(const parallelRouteKey in baseTreeChildren){ const childBaseRouterState = baseTreeChildren[parallelRouteKey]; const childBaseSeedData = baseSeedDataChildren !== null ? baseSeedDataChildren[parallelRouteKey] ?? null : null; if (parallelRouteKey === updatedParallelRouteKey) { const result = convertServerPatchToFullTreeImpl(childBaseRouterState, childBaseSeedData, treePatch, dataPatch, segmentPath, // Advance the index by two and keep cloning until we reach // the end of the segment path. index + 2); newTreeChildren[parallelRouteKey] = result.tree; newSeedDataChildren[parallelRouteKey] = result.data; } else { // This child is not being patched. Copy it over as-is. newTreeChildren[parallelRouteKey] = childBaseRouterState; newSeedDataChildren[parallelRouteKey] = childBaseSeedData; } } let clonedTree; let clonedSeedData; // Clone all the fields except the children. // Clone the FlightRouterState tree. 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. clonedTree = [ baseRouterState[0], newTreeChildren ]; if (2 in baseRouterState) { clonedTree[2] = baseRouterState[2]; } if (3 in baseRouterState) { clonedTree[3] = baseRouterState[3]; } if (4 in baseRouterState) { clonedTree[4] = baseRouterState[4]; } // Clone the CacheNodeSeedData tree. const isEmptySeedDataPartial = true; clonedSeedData = [ null, newSeedDataChildren, null, isEmptySeedDataPartial, false ]; return { tree: clonedTree, data: clonedSeedData }; } 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