File size: 17,855 Bytes
fea495a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 | "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, route, 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 was no matching route tree in the cache. Let's see if we can
// construct an "optimistic" route tree.
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 prefetchHead = optimisticRoute.head;
const isPrefetchHeadPartial = optimisticRoute.isHeadPartial;
const newCanonicalUrl = optimisticRoute.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)(new URL(canonicalUrl, url.origin), {
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, 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, route, tree.cacheKey);
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;
}
}
// 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)));
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 |