UNPKG

next

Version:

The React Framework

275 lines (274 loc) 13.1 kB
import { fetchServerResponse } from '../router-reducer/fetch-server-response'; import { updateCacheNodeOnNavigation, listenForDynamicRequest } from '../router-reducer/ppr-navigations'; import { createHrefFromUrl as createCanonicalUrl } from '../router-reducer/create-href-from-url'; import { EntryStatus, readRouteCacheEntry, readSegmentCacheEntry, waitForSegmentCacheEntry } from './cache'; import { createCacheKey } from './cache-key'; export var NavigationResultTag = /*#__PURE__*/ function(NavigationResultTag) { NavigationResultTag[NavigationResultTag["MPA"] = 0] = "MPA"; NavigationResultTag[NavigationResultTag["Success"] = 1] = "Success"; NavigationResultTag[NavigationResultTag["NoOp"] = 2] = "NoOp"; NavigationResultTag[NavigationResultTag["Async"] = 3] = "Async"; return NavigationResultTag; }({}); const noOpNavigationResult = { tag: 2, data: null }; /** * Navigate to a new URL, using the Segment Cache to construct a response. * * To allow for synchronous navigations whenever possible, this is not an async * function. It returns a promise only if there's no matching prefetch in * the cache. Otherwise it returns an immediate result and uses Suspense/RSC to * stream in any missing data. */ export function navigate(url, currentCacheNode, currentFlightRouterState, nextUrl) { const now = Date.now(); const cacheKey = createCacheKey(url.href, nextUrl); const route = readRouteCacheEntry(now, cacheKey); if (route !== null && route.status === EntryStatus.Fulfilled) { // We have a matching prefetch. const snapshot = readRenderSnapshotFromCache(now, route.tree); const prefetchFlightRouterState = snapshot.flightRouterState; const prefetchSeedData = snapshot.seedData; const prefetchHead = route.head; const isPrefetchHeadPartial = route.isHeadPartial; const canonicalUrl = route.canonicalUrl; return navigateUsingPrefetchedRouteTree(url, nextUrl, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, canonicalUrl); } // There's no matching prefetch for this route in the cache. return { tag: 3, data: navigateDynamicallyWithNoPrefetch(url, nextUrl, currentCacheNode, currentFlightRouterState) }; } function navigateUsingPrefetchedRouteTree(url, nextUrl, currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial, canonicalUrl) { // 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 task = updateCacheNodeOnNavigation(currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial); if (task !== null) { if (task.needsDynamicRequest) { const promiseForDynamicServerResponse = fetchServerResponse(url, { flightRouterState: currentFlightRouterState, nextUrl }); 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); } // The server sent back an empty tree patch. There's nothing to update. return noOpNavigationResult; } function navigationTaskToResult(task, currentCacheNode, canonicalUrl) { const newCacheNode = task.node; return { tag: 1, data: { flightRouterState: task.route, cacheNode: newCacheNode !== null ? newCacheNode : currentCacheNode, canonicalUrl } }; } function readRenderSnapshotFromCache(now, 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, childTree); childRouterStates[parallelRouteKey] = childResult.flightRouterState; childSeedDatas[parallelRouteKey] = childResult.seedData; } } let rsc = null; let loading = null; let isPartial = true; const segmentEntry = readSegmentCacheEntry(now, tree.path); if (segmentEntry !== null) { switch(segmentEntry.status){ case EntryStatus.Fulfilled: { // Happy path: a cache hit rsc = segmentEntry.rsc; loading = segmentEntry.loading; isPartial = segmentEntry.isPartial; break; } case 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 = 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 EntryStatus.Rejected: break; default: { const _exhaustiveCheck = segmentEntry; break; } } } const extra = tree.extra; const flightRouterStateSegment = extra[0]; const isRootLayout = extra[1]; return { flightRouterState: [ flightRouterStateSegment, childRouterStates, null, null, isRootLayout ], seedData: [ flightRouterStateSegment, rsc, childSeedDatas, loading, isPartial ] }; } async function navigateDynamicallyWithNoPrefetch(url, nextUrl, currentCacheNode, currentFlightRouterState) { // 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 = fetchServerResponse(url, { flightRouterState: currentFlightRouterState, nextUrl }); const { flightData, canonicalUrl: canonicalUrlOverride } = await promiseForDynamicServerResponse; // TODO: Detect if the only thing that changed was the hash, like we do in // in navigateReducer if (typeof flightData === 'string') { // This is an MPA navigation. const newUrl = flightData; return { tag: 0, 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 = createCanonicalUrl(canonicalUrlOverride ? canonicalUrlOverride : url); // Now we proceed exactly as we would for normal navigation. const task = updateCacheNodeOnNavigation(currentCacheNode, currentFlightRouterState, prefetchFlightRouterState, prefetchSeedData, prefetchHead, isPrefetchHeadPartial); if (task !== null) { if (task.needsDynamicRequest) { 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); } // The server sent back an empty tree patch. There's nothing to update. return noOpNavigationResult; } 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; } //# sourceMappingURL=navigation.js.map