1import type {
2 CacheNodeSeedData,
3 FlightRouterState,
4 FlightSegmentPath,
5 Segment,
6} from '../../../shared/lib/app-router-types'
7import type { CacheNode } from '../../../shared/lib/app-router-types'
8import type { HeadData } from '../../../shared/lib/app-router-types'
9import { PrefetchHint } from '../../../shared/lib/app-router-types'
10import {
11 PAGE_SEGMENT_KEY,
12 DEFAULT_SEGMENT_KEY,
13 NOT_FOUND_SEGMENT_KEY,
14} from '../../../shared/lib/segment'
15import { matchSegment } from '../match-segments'
16import { createHrefFromUrl } from './create-href-from-url'
17import { fetchServerResponse } from './fetch-server-response'
18import { dispatchAppRouterAction } from '../use-action-queue'
19import {
20 ACTION_SERVER_PATCH,
21 type ServerPatchAction,
22} from './router-reducer-types'
23import { isNavigatingToNewRootLayout } from './is-navigating-to-new-root-layout'
24import { getLastCommittedTree } from './reducers/committed-state'
25import {
26 convertServerPatchToFullTree,
27 type NavigationSeed,
28} from '../segment-cache/navigation'
29import {
30 type RouteTree,
31 type RefreshState,
32 type FulfilledRouteCacheEntry,
33 convertReusedFlightRouterStateToRouteTree,
34 readSegmentCacheEntry,
35 waitForSegmentCacheEntry,
36 markRouteEntryAsDynamicRewrite,
37 invalidateRouteCacheEntries,
38 getStaleAt,
39 writeStaticStageResponseIntoCache,
40 processRuntimePrefetchStream,
41 writeDynamicRenderResponseIntoCache,
42 EntryStatus,
43} from '../segment-cache/cache'
44import { FetchStrategy } from '../segment-cache/types'
45import { discoverKnownRoute } from '../segment-cache/optimistic-routes'
46import { NEXT_NAV_DEPLOYMENT_ID_HEADER } from '../../../lib/constants'
47import type { NormalizedSearch } from '../segment-cache/cache-key'
48import {
49 getRenderedSearchFromVaryPath,
50 type PageVaryPath,
51} from '../segment-cache/vary-path'
52import {
53 readFromBFCache,
54 readFromBFCacheDuringRegularNavigation,
55 writeToBFCache,
56 writeHeadToBFCache,
57} from '../segment-cache/bfcache'
58import { DYNAMIC_STALETIME_MS } from './reducers/navigate-reducer'
59
60// This is yet another tree type that is used to track pending promises that
61// need to be fulfilled once the dynamic data is received. The terminal nodes of
62// this tree represent the new Cache Node trees that were created during this
63// request. We can't use the Cache Node tree or Route State tree directly
64// because those include reused nodes, too. This tree is discarded as soon as
65// the navigation response is received.
66export type NavigationTask = {
67 status: NavigationTaskStatus
68 // The router state that corresponds to the tree that this Task represents.
69 route: FlightRouterState
70 // The CacheNode that corresponds to the tree that this Task represents.
71 node: CacheNode
72 // The tree sent to the server during the dynamic request. If all the segments
73 // are static, then this will be null, and no server request is required.
74 // Otherwise, this is the same as `route`, except with the `refetch` marker
75 // set on the top-most segment that needs to be fetched.
76 dynamicRequestTree: FlightRouterState | null
77 // The URL that should be used to fetch the dynamic data. This is only set
78 // when the segment cannot be refetched from the current route, because it's
79 // part of a "default" parallel slot that was reused during a navigation.
80 refreshState: RefreshState | null
81 children: Map<string, NavigationTask> | null
82}
83
84export const enum FreshnessPolicy {
85 Default,
86 Hydration,
87 HistoryTraversal,
88 RefreshAll,
89 HMRRefresh,
90 Gesture,
91}
92
93const enum NavigationTaskStatus {
94 Pending,
95 Fulfilled,
96 Rejected,
97}
98
99/**
100 * When a NavigationTask finishes, there may or may not be data still missing,
101 * necessitating a retry.
102 */
103const enum NavigationTaskExitStatus {
104 /**
105 * No additional navigation is required.
106 */
107 Done = 0,
108 /**
109 * Some data failed to load, presumably due to a route tree mismatch. Perform
110 * a soft retry to reload the entire tree.
111 */
112 SoftRetry = 1,
113 /**
114 * Some data failed to load in an unrecoverable way, e.g. in an inactive
115 * parallel route. Fall back to a hard (MPA-style) retry.
116 */
117 HardRetry = 2,
118}
119
120export type NavigationRequestAccumulation = {
121 scrollableSegments: Array<FlightSegmentPath> | null
122 separateRefreshUrls: Set<string> | null
123}
124
125const noop = () => {}
126
127export function createInitialCacheNodeForHydration(
128 navigatedAt: number,
129 initialTree: RouteTree,
130 seedData: CacheNodeSeedData | null,
131 seedHead: HeadData
132): NavigationTask {
133 // Create the initial cache node tree, using the data embedded into the
134 // HTML document.
135 const accumulation: NavigationRequestAccumulation = {
136 scrollableSegments: null,
137 separateRefreshUrls: null,
138 }
139 const task = createCacheNodeOnNavigation(
140 navigatedAt,
141 initialTree,
142 null,
143 FreshnessPolicy.Hydration,
144 seedData,
145 seedHead,
146 null,
147 null,
148 false,
149 accumulation
150 )
151 return task
152}
153
154// Creates a new Cache Node tree (i.e. copy-on-write) that represents the
155// optimistic result of a navigation, using both the current Cache Node tree and
156// data that was prefetched prior to navigation.
157//
158// At the moment we call this function, we haven't yet received the navigation
159// response from the server. It could send back something completely different
160// from the tree that was prefetched — due to rewrites, default routes, parallel
161// routes, etc.
162//
163// But in most cases, it will return the same tree that we prefetched, just with
164// the dynamic holes filled in. So we optimistically assume this will happen,
165// and accept that the real result could be arbitrarily different.
166//
167// We'll reuse anything that was already in the previous tree, since that's what
168// the server does.
169//
170// New segments (ones that don't appear in the old tree) are assigned an
171// unresolved promise. The data for these promises will be fulfilled later, when
172// the navigation response is received.
173//
174// The tree can be rendered immediately after it is created (that's why this is
175// a synchronous function). Any new trees that do not have prefetch data will
176// suspend during rendering, until the dynamic data streams in.
177//
178// Returns a Task object, which contains both the updated Cache Node and a path
179// to the pending subtrees that need to be resolved by the navigation response.
180//
181// A return value of `null` means there were no changes, and the previous tree
182// can be reused without initiating a server request.
183export function startPPRNavigation(
184 navigatedAt: number,
185 oldUrl: URL,
186 oldRenderedSearch: string,
187 oldCacheNode: CacheNode | null,
188 oldRouterState: FlightRouterState,
189 newRouteTree: RouteTree,
190 newMetadataVaryPath: PageVaryPath | null,
191 freshness: FreshnessPolicy,
192 seedData: CacheNodeSeedData | null,
193 seedHead: HeadData | null,
194 isSamePageNavigation: boolean,
195 accumulation: NavigationRequestAccumulation
196): NavigationTask | null {
197 const didFindRootLayout = false
198 const parentNeedsDynamicRequest = false
199 const parentRefreshState = null
200 const oldRootRefreshState: RefreshState = {
201 canonicalUrl: createHrefFromUrl(oldUrl),
202 renderedSearch: oldRenderedSearch as NormalizedSearch,
203 }
204 return updateCacheNodeOnNavigation(
205 navigatedAt,
206 oldUrl,
207 oldCacheNode !== null ? oldCacheNode : undefined,
208 oldRouterState,
209 newRouteTree,
210 newMetadataVaryPath,
211 freshness,
212 didFindRootLayout,
213 seedData,
214 seedHead,
215 isSamePageNavigation,
216 null,
217 null,
218 parentNeedsDynamicRequest,
219 oldRootRefreshState,
220 parentRefreshState,
221 accumulation
222 )
223}
224
225function updateCacheNodeOnNavigation(
226 navigatedAt: number,
227 oldUrl: URL,
228 oldCacheNode: CacheNode | void,
229 oldRouterState: FlightRouterState,
230 newRouteTree: RouteTree,
231 newMetadataVaryPath: PageVaryPath | null,
232 freshness: FreshnessPolicy,
233 didFindRootLayout: boolean,
234 seedData: CacheNodeSeedData | null,
235 seedHead: HeadData | null,
236 isSamePageNavigation: boolean,
237 parentSegmentPath: FlightSegmentPath | null,
238 parentParallelRouteKey: string | null,
239 parentNeedsDynamicRequest: boolean,
240 oldRootRefreshState: RefreshState,
241 parentRefreshState: RefreshState | null,
242 accumulation: NavigationRequestAccumulation
243): NavigationTask | null {
244 // Check if this segment matches the one in the previous route.
245 const oldSegment = oldRouterState[0]
246 const newSegment = createSegmentFromRouteTree(newRouteTree)
247 if (!matchSegment(newSegment, oldSegment)) {
248 // This segment does not match the previous route. We're now entering the
249 // new part of the target route. Switch to the "create" path.
250 if (
251 // Check if the route tree changed before we reached a layout. (The
252 // highest-level layout in a route tree is referred to as the "root"
253 // layout.) This could mean that we're navigating between two different
254 // root layouts. When this happens, we perform a full-page (MPA-style)
255 // navigation.
256 //
257 // However, the algorithm for deciding where to start rendering a route
258 // (i.e. the one performed in order to reach this function) is stricter
259 // than the one used to detect a change in the root layout. So just
260 // because we're re-rendering a segment outside of the root layout does
261 // not mean we should trigger a full-page navigation.
262 //
263 // Specifically, we handle dynamic parameters differently: two segments
264 // are considered the same even if their parameter values are different.
265 //
266 // Refer to isNavigatingToNewRootLayout for details.
267 //
268 // Note that we only have to perform this extra traversal if we didn't
269 // already discover a root layout in the part of the tree that is
270 // unchanged. We also only need to compare the subtree that is not
271 // shared. In the common case, this branch is skipped completely.
272 (!didFindRootLayout &&
273 isNavigatingToNewRootLayout(oldRouterState, newRouteTree)) ||
274 // The global Not Found route (app/global-not-found.tsx) is a special
275 // case, because it acts like a root layout, but in the router tree, it
276 // is rendered in the same position as app/layout.tsx.
277 //
278 // Any navigation to the global Not Found route should trigger a
279 // full-page navigation.
280 //
281 // TODO: We should probably model this by changing the key of the root
282 // segment when this happens. Then the root layout check would work
283 // as expected, without a special case.
284 newSegment === NOT_FOUND_SEGMENT_KEY
285 ) {
286 return null
287 }
288 if (parentSegmentPath === null || parentParallelRouteKey === null) {
289 // The root should never mismatch. If it does, it suggests an internal
290 // Next.js error, or a malformed server response. Trigger a full-
291 // page navigation.
292 return null
293 }
294 return createCacheNodeOnNavigation(
295 navigatedAt,
296 newRouteTree,
297 newMetadataVaryPath,
298 freshness,
299 seedData,
300 seedHead,
301 parentSegmentPath,
302 parentParallelRouteKey,
303 parentNeedsDynamicRequest,
304 accumulation
305 )
306 }
307
308 // TODO: The segment paths are tracked so that LayoutRouter knows which
309 // segments to scroll to after a navigation. But we should just mark this
310 // information on the CacheNode directly. It used to be necessary to do this
311 // separately because CacheNodes were created lazily during render, not when
312 // rather than when creating the route tree.
313 const segmentPath =
314 parentParallelRouteKey !== null && parentSegmentPath !== null
315 ? parentSegmentPath.concat([parentParallelRouteKey, newSegment])
316 : // NOTE: The root segment is intentionally omitted from the segment path
317 []
318
319 const newSlots = newRouteTree.slots
320 const oldRouterStateChildren = oldRouterState[1]
321 const seedDataChildren = seedData !== null ? seedData[1] : null
322
323 // We're currently traversing the part of the tree that was also part of
324 // the previous route. If we discover a root layout, then we don't need to
325 // trigger an MPA navigation.
326 const childDidFindRootLayout =
327 didFindRootLayout ||
328 (newRouteTree.prefetchHints & PrefetchHint.IsRootLayout) !== 0
329
330 let shouldRefreshDynamicData: boolean = false
331 switch (freshness) {
332 case FreshnessPolicy.Default:
333 case FreshnessPolicy.HistoryTraversal:
334 case FreshnessPolicy.Hydration: // <- shouldn't happen during client nav
335 case FreshnessPolicy.Gesture:
336 // We should never drop dynamic data in shared layouts, except during
337 // a refresh.
338 shouldRefreshDynamicData = false
339 break
340 case FreshnessPolicy.RefreshAll:
341 case FreshnessPolicy.HMRRefresh:
342 shouldRefreshDynamicData = true
343 break
344 default:
345 freshness satisfies never
346 break
347 }
348
349 // TODO: We're not consistent about how we do this check. Some places
350 // check if the segment starts with PAGE_SEGMENT_KEY, but most seem to
351 // check if there any any children, which is why I'm doing it here. We
352 // should probably encode an empty children set as `null` though. Either
353 // way, we should update all the checks to be consistent.
354 const isLeafSegment = newSlots === null
355
356 // Get the data for this segment. Since it was part of the previous route,
357 // usually we just clone the data from the old CacheNode. However, during a
358 // refresh or a revalidation, there won't be any existing CacheNode. So we
359 // may need to consult the prefetch cache, like we would for a new segment.
360 let newCacheNode: CacheNode
361 let needsDynamicRequest: boolean
362 if (
363 oldCacheNode !== undefined &&
364 !shouldRefreshDynamicData &&
365 // During a same-page navigation, we always refetch the page segments
366 !(isLeafSegment && isSamePageNavigation)
367 ) {
368 // Reuse the existing CacheNode
369 const dropPrefetchRsc = false
370 newCacheNode = reuseSharedCacheNode(dropPrefetchRsc, oldCacheNode)
371 needsDynamicRequest = false
372 } else {
373 // If this is part of a refresh, ignore the existing CacheNode and create a
374 // new one.
375 const seedRsc = seedData !== null ? seedData[0] : null
376 const result = createCacheNodeForSegment(
377 navigatedAt,
378 newRouteTree,
379 seedRsc,
380 newMetadataVaryPath,
381 seedHead,
382 freshness
383 )
384 newCacheNode = result.cacheNode
385 needsDynamicRequest = result.needsDynamicRequest
386 }
387
388 // During a refresh navigation, there's a special case that happens when
389 // entering a "default" slot. The default slot may not be part of the
390 // current route; it may have been reused from an older route. If so,
391 // we need to fetch its data from the old route's URL rather than current
392 // route's URL. Keep track of this as we traverse the tree.
393 const maybeRefreshState = newRouteTree.refreshState
394 const refreshState =
395 maybeRefreshState !== undefined && maybeRefreshState !== null
396 ? // This segment is not present in the current route. Track its
397 // refresh URL as we continue traversing the tree.
398 maybeRefreshState
399 : // Inherit the refresh URL from the parent.
400 parentRefreshState
401
402 // If this segment itself needs to fetch new data from the server, then by
403 // definition it is being refreshed. Track its refresh URL so we know which
404 // URL to request the data from.
405 if (needsDynamicRequest && refreshState !== null) {
406 accumulateRefreshUrl(accumulation, refreshState)
407 }
408
409 // As we diff the trees, we may sometimes modify (copy-on-write, not mutate)
410 // the Route Tree that was returned by the server — for example, in the case
411 // of default parallel routes, we preserve the currently active segment. To
412 // avoid mutating the original tree, we clone the router state children along
413 // the return path.
414 let patchedRouterStateChildren: {
415 [parallelRouteKey: string]: FlightRouterState
416 } = {}
417 let taskChildren = null
418
419 // Most navigations require a request to fetch additional data from the
420 // server, either because the data was not already prefetched, or because the
421 // target route contains dynamic data that cannot be prefetched.
422 //
423 // However, if the target route is fully static, and it's already completely
424 // loaded into the segment cache, then we can skip the server request.
425 //
426 // This starts off as `false`, and is set to `true` if any of the child
427 // routes requires a dynamic request.
428 let childNeedsDynamicRequest = false
429 // As we traverse the children, we'll construct a FlightRouterState that can
430 // be sent to the server to request the dynamic data. If it turns out that
431 // nothing in the subtree is dynamic (i.e. childNeedsDynamicRequest is false
432 // at the end), then this will be discarded.
433 // TODO: We can probably optimize the format of this data structure to only
434 // include paths that are dynamic. Instead of reusing the
435 // FlightRouterState type.
436 let dynamicRequestTreeChildren: {
437 [parallelRouteKey: string]: FlightRouterState
438 } = {}
439
440 let newCacheNodeSlots: Record<string, CacheNode> | null = null
441 if (newSlots !== null) {
442 const oldCacheNodeSlots =
443 oldCacheNode !== undefined ? oldCacheNode.slots : null
444
445 newCacheNode.slots = newCacheNodeSlots = {}
446 taskChildren = new Map()
447 for (let parallelRouteKey in newSlots) {
448 let newRouteTreeChild: RouteTree = newSlots[parallelRouteKey]
449 const oldRouterStateChild: FlightRouterState | void =
450 oldRouterStateChildren[parallelRouteKey]
451 if (oldRouterStateChild === undefined) {
452 // This should never happen, but if it does, it suggests a malformed
453 // server response. Trigger a full-page navigation.
454 return null
455 }
456
457 let seedDataChild: CacheNodeSeedData | void | null =
458 seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null
459
460 const oldSegmentChild = oldRouterStateChild[0]
461 let newSegmentChild = createSegmentFromRouteTree(newRouteTreeChild)
462 let seedHeadChild = seedHead
463 if (
464 // Skip this branch during a history traversal. We restore the tree that
465 // was stashed in the history entry as-is.
466 freshness !== FreshnessPolicy.HistoryTraversal &&
467 newSegmentChild === DEFAULT_SEGMENT_KEY &&
468 oldSegmentChild !== DEFAULT_SEGMENT_KEY
469 ) {
470 // This is a "default" segment. These are never sent by the server during
471 // a soft navigation; instead, the client reuses whatever segment was
472 // already active in that slot on the previous route.
473 newRouteTreeChild = reuseActiveSegmentInDefaultSlot(
474 newRouteTree,
475 parallelRouteKey,
476 oldRootRefreshState,
477 oldRouterStateChild
478 )
479 newSegmentChild = createSegmentFromRouteTree(newRouteTreeChild)
480
481 // Since we're switching to a different route tree, these are no
482 // longer valid, because they correspond to the outer tree.
483 seedDataChild = null
484 seedHeadChild = null
485 }
486
487 const oldCacheNodeChild =
488 oldCacheNodeSlots !== null
489 ? oldCacheNodeSlots[parallelRouteKey]
490 : undefined
491
492 const taskChild = updateCacheNodeOnNavigation(
493 navigatedAt,
494 oldUrl,
495 oldCacheNodeChild,
496 oldRouterStateChild,
497 newRouteTreeChild,
498 newMetadataVaryPath,
499 freshness,
500 childDidFindRootLayout,
501 seedDataChild ?? null,
502 seedHeadChild,
503 isSamePageNavigation,
504 segmentPath,
505 parallelRouteKey,
506 parentNeedsDynamicRequest || needsDynamicRequest,
507 oldRootRefreshState,
508 refreshState,
509 accumulation
510 )
511
512 if (taskChild === null) {
513 // One of the child tasks discovered a change to the root layout.
514 // Immediately unwind from this recursive traversal. This will trigger a
515 // full-page navigation.
516 return null
517 }
518
519 // Recursively propagate up the child tasks.
520 taskChildren.set(parallelRouteKey, taskChild)
521 newCacheNodeSlots[parallelRouteKey] = taskChild.node
522
523 // The child tree's route state may be different from the prefetched
524 // route sent by the server. We need to clone it as we traverse back up
525 // the tree.
526 const taskChildRoute = taskChild.route
527 patchedRouterStateChildren[parallelRouteKey] = taskChildRoute
528
529 const dynamicRequestTreeChild = taskChild.dynamicRequestTree
530 if (dynamicRequestTreeChild !== null) {
531 // Something in the child tree is dynamic.
532 childNeedsDynamicRequest = true
533 dynamicRequestTreeChildren[parallelRouteKey] = dynamicRequestTreeChild
534 } else {
535 dynamicRequestTreeChildren[parallelRouteKey] = taskChildRoute
536 }
537 }
538 }
539
540 const newFlightRouterState: FlightRouterState = [
541 createSegmentFromRouteTree(newRouteTree),
542 patchedRouterStateChildren,
543 refreshState !== null
544 ? [refreshState.canonicalUrl, refreshState.renderedSearch]
545 : null,
546 null,
547 newRouteTree.prefetchHints,
548 ]
549
550 return {
551 status: needsDynamicRequest
552 ? NavigationTaskStatus.Pending
553 : NavigationTaskStatus.Fulfilled,
554 route: newFlightRouterState,
555 node: newCacheNode,
556 dynamicRequestTree: createDynamicRequestTree(
557 newFlightRouterState,
558 dynamicRequestTreeChildren,
559 needsDynamicRequest,
560 childNeedsDynamicRequest,
561 parentNeedsDynamicRequest
562 ),
563 refreshState,
564 children: taskChildren,
565 }
566}
567
568function createCacheNodeOnNavigation(
569 navigatedAt: number,
570 newRouteTree: RouteTree,
571 newMetadataVaryPath: PageVaryPath | null,
572 freshness: FreshnessPolicy,
573 seedData: CacheNodeSeedData | null,
574 seedHead: HeadData | null,
575 parentSegmentPath: FlightSegmentPath | null,
576 parentParallelRouteKey: string | null,
577 parentNeedsDynamicRequest: boolean,
578 accumulation: NavigationRequestAccumulation
579): NavigationTask {
580 // Same traversal as updateCacheNodeNavigation, but simpler. We switch to this
581 // path once we reach the part of the tree that was not in the previous route.
582 // We don't need to diff against the old tree, we just need to create a new
583 // one. We also don't need to worry about any refresh-related logic.
584 //
585 // For the most part, this is a subset of updateCacheNodeOnNavigation, so any
586 // change that happens in this function likely needs to be applied to that
587 // one, too. However there are some places where the behavior intentionally
588 // diverges, which is why we keep them separate.
589
590 const newSegment = createSegmentFromRouteTree(newRouteTree)
591 const segmentPath =
592 parentParallelRouteKey !== null && parentSegmentPath !== null
593 ? parentSegmentPath.concat([parentParallelRouteKey, newSegment])
594 : // NOTE: The root segment is intentionally omitted from the segment path
595 []
596
597 const newSlots = newRouteTree.slots
598 const seedDataChildren = seedData !== null ? seedData[1] : null
599
600 const isLeafSegment = newSlots === null
601
602 if (isLeafSegment) {
603 // The segment path of every leaf segment (i.e. page) is collected into
604 // a result array. This is used by the LayoutRouter to scroll to ensure that
605 // new pages are visible after a navigation.
606 //
607 // This only happens for new pages, not for refreshed pages.
608 //
609 // TODO: We should use a string to represent the segment path instead of
610 // an array. We already use a string representation for the path when
611 // accessing the Segment Cache, so we can use the same one.
612 if (accumulation.scrollableSegments === null) {
613 accumulation.scrollableSegments = []
614 }
615 accumulation.scrollableSegments.push(segmentPath)
616 }
617
618 const seedRsc = seedData !== null ? seedData[0] : null
619 const result = createCacheNodeForSegment(
620 navigatedAt,
621 newRouteTree,
622 seedRsc,
623 newMetadataVaryPath,
624 seedHead,
625 freshness
626 )
627 const newCacheNode = result.cacheNode
628 const needsDynamicRequest = result.needsDynamicRequest
629
630 let patchedRouterStateChildren: {
631 [parallelRouteKey: string]: FlightRouterState
632 } = {}
633 let taskChildren = null
634
635 let childNeedsDynamicRequest = false
636 let dynamicRequestTreeChildren: {
637 [parallelRouteKey: string]: FlightRouterState
638 } = {}
639
640 let newCacheNodeSlots: Record<string, CacheNode> | null = null
641 if (newSlots !== null) {
642 newCacheNode.slots = newCacheNodeSlots = {}
643 taskChildren = new Map()
644 for (let parallelRouteKey in newSlots) {
645 const newRouteTreeChild: RouteTree = newSlots[parallelRouteKey]
646 const seedDataChild: CacheNodeSeedData | void | null =
647 seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null
648
649 const taskChild = createCacheNodeOnNavigation(
650 navigatedAt,
651 newRouteTreeChild,
652 newMetadataVaryPath,
653 freshness,
654 seedDataChild ?? null,
655 seedHead,
656 segmentPath,
657 parallelRouteKey,
658 parentNeedsDynamicRequest || needsDynamicRequest,
659 accumulation
660 )
661
662 taskChildren.set(parallelRouteKey, taskChild)
663 newCacheNodeSlots[parallelRouteKey] = taskChild.node
664
665 const taskChildRoute = taskChild.route
666 patchedRouterStateChildren[parallelRouteKey] = taskChildRoute
667
668 const dynamicRequestTreeChild = taskChild.dynamicRequestTree
669 if (dynamicRequestTreeChild !== null) {
670 childNeedsDynamicRequest = true
671 dynamicRequestTreeChildren[parallelRouteKey] = dynamicRequestTreeChild
672 } else {
673 dynamicRequestTreeChildren[parallelRouteKey] = taskChildRoute
674 }
675 }
676 }
677
678 const newFlightRouterState: FlightRouterState = [
679 newSegment,
680 patchedRouterStateChildren,
681 null,
682 null,
683 newRouteTree.prefetchHints,
684 ]
685
686 return {
687 status: needsDynamicRequest
688 ? NavigationTaskStatus.Pending
689 : NavigationTaskStatus.Fulfilled,
690 route: newFlightRouterState,
691 node: newCacheNode,
692 dynamicRequestTree: createDynamicRequestTree(
693 newFlightRouterState,
694 dynamicRequestTreeChildren,
695 needsDynamicRequest,
696 childNeedsDynamicRequest,
697 parentNeedsDynamicRequest
698 ),
699 // This route is not part of the current tree, so there's no reason to
700 // track the refresh URL.
701 refreshState: null,
702 children: taskChildren,
703 }
704}
705
706function createSegmentFromRouteTree(newRouteTree: RouteTree): Segment {
707 if (newRouteTree.isPage) {
708 // In a dynamic server response, the server embeds the search params into
709 // the segment key, but in a static one it's omitted. The client handles
710 // this inconsistency by adding the search params back right at the end.
711 //
712 // TODO: The only thing this is used for is to create a cache key for
713 // ChildSegmentMap. But we already track the `renderedSearch` everywhere as
714 // part of the varyPath. The plan is get rid of ChildSegmentMap and
715 // store the page data in a CacheMap using the varyPath, like we do
716 // for prefetches. Then we can remove it from the segment key.
717 //
718 // As an incremental step, we can grab the search params from the varyPath.
719 const renderedSearch = getRenderedSearchFromVaryPath(newRouteTree.varyPath)
720 if (renderedSearch === null) {
721 return PAGE_SEGMENT_KEY
722 }
723 // This is based on equivalent logic in addSearchParamsIfPageSegment, used
724 // on the server.
725 const stringifiedQuery = JSON.stringify(
726 Object.fromEntries(new URLSearchParams(renderedSearch))
727 )
728 return stringifiedQuery !== '{}'
729 ? PAGE_SEGMENT_KEY + '?' + stringifiedQuery
730 : PAGE_SEGMENT_KEY
731 }
732 return newRouteTree.segment
733}
734
735function patchRouterStateWithNewChildren(
736 baseRouterState: FlightRouterState,
737 newChildren: { [parallelRouteKey: string]: FlightRouterState }
738): FlightRouterState {
739 const clone: FlightRouterState = [baseRouterState[0], newChildren]
740 // Based on equivalent logic in apply-router-state-patch-to-tree, but should
741 // confirm whether we need to copy all of these fields. Not sure the server
742 // ever sends, e.g. the refetch marker.
743 if (2 in baseRouterState) {
744 clone[2] = baseRouterState[2]
745 }
746 if (3 in baseRouterState) {
747 clone[3] = baseRouterState[3]
748 }
749 if (4 in baseRouterState) {
750 clone[4] = baseRouterState[4]
751 }
752 return clone
753}
754
755function createDynamicRequestTree(
756 newRouterState: FlightRouterState,
757 dynamicRequestTreeChildren: Record<string, FlightRouterState>,
758 needsDynamicRequest: boolean,
759 childNeedsDynamicRequest: boolean,
760 parentNeedsDynamicRequest: boolean
761): FlightRouterState | null {
762 // Create a FlightRouterState that instructs the server how to render the
763 // requested segment.
764 //
765 // Or, if neither this segment nor any of the children require a new data,
766 // then we return `null` to skip the request.
767 let dynamicRequestTree: FlightRouterState | null = null
768 if (needsDynamicRequest) {
769 dynamicRequestTree = patchRouterStateWithNewChildren(
770 newRouterState,
771 dynamicRequestTreeChildren
772 )
773 // The "refetch" marker is set on the top-most segment that requires new
774 // data. We can omit it if a parent was already marked.
775 if (!parentNeedsDynamicRequest) {
776 dynamicRequestTree[3] = 'refetch'
777 }
778 } else if (childNeedsDynamicRequest) {
779 // This segment does not request new data, but at least one of its
780 // children does.
781 dynamicRequestTree = patchRouterStateWithNewChildren(
782 newRouterState,
783 dynamicRequestTreeChildren
784 )
785 } else {
786 dynamicRequestTree = null
787 }
788 return dynamicRequestTree
789}
790
791function accumulateRefreshUrl(
792 accumulation: NavigationRequestAccumulation,
793 refreshState: RefreshState
794) {
795 // This is a refresh navigation, and we're inside a "default" slot that's
796 // not part of the current route; it was reused from an older route. In
797 // order to get fresh data for this reused route, we need to issue a
798 // separate request using the old route's URL.
799 //
800 // Track these extra URLs in the accumulated result. Later, we'll construct
801 // an appropriate request for each unique URL in the final set. The reason
802 // we don't do it immediately here is so we can deduplicate multiple
803 // instances of the same URL into a single request. See
804 // listenForDynamicRequest for more details.
805 const refreshUrl = refreshState.canonicalUrl
806 const separateRefreshUrls = accumulation.separateRefreshUrls
807 if (separateRefreshUrls === null) {
808 accumulation.separateRefreshUrls = new Set([refreshUrl])
809 } else {
810 separateRefreshUrls.add(refreshUrl)
811 }
812}
813
814function reuseActiveSegmentInDefaultSlot(
815 parentRouteTree: RouteTree,
816 parallelRouteKey: string,
817 oldRootRefreshState: RefreshState,
818 oldRouterState: FlightRouterState
819): RouteTree {
820 // This is a "default" segment. These are never sent by the server during a
821 // soft navigation; instead, the client reuses whatever segment was already
822 // active in that slot on the previous route. This means if we later need to
823 // refresh the segment, it will have to be refetched from the previous route's
824 // URL. We store it in the Flight Router State.
825
826 let reusedUrl: string
827 let reusedRenderedSearch: NormalizedSearch
828 const oldRefreshState = oldRouterState[2]
829 if (oldRefreshState !== undefined && oldRefreshState !== null) {
830 // This segment was already reused from an even older route. Keep its
831 // existing URL and refresh state.
832 reusedUrl = oldRefreshState[0]
833 reusedRenderedSearch = oldRefreshState[1] as NormalizedSearch
834 } else {
835 // Since this route didn't already have a refresh state, it must have been
836 // reachable from the root of the old route. So we use the refresh state
837 // that represents the old route.
838 reusedUrl = oldRootRefreshState.canonicalUrl
839 reusedRenderedSearch = oldRootRefreshState.renderedSearch
840 }
841
842 const acc = { metadataVaryPath: null }
843 const reusedRouteTree = convertReusedFlightRouterStateToRouteTree(
844 parentRouteTree,
845 parallelRouteKey,
846 oldRouterState,
847 reusedRenderedSearch,
848 acc
849 )
850 reusedRouteTree.refreshState = {
851 canonicalUrl: reusedUrl,
852 renderedSearch: reusedRenderedSearch,
853 }
854 return reusedRouteTree
855}
856
857function reuseSharedCacheNode(
858 dropPrefetchRsc: boolean,
859 existingCacheNode: CacheNode
860): CacheNode {
861 // Clone the CacheNode that was already present in the previous tree
862 return createCacheNode(
863 existingCacheNode.rsc,
864 dropPrefetchRsc ? null : existingCacheNode.prefetchRsc,
865 existingCacheNode.head,
866 dropPrefetchRsc ? null : existingCacheNode.prefetchHead
867 )
868}
869
870function createCacheNodeForSegment(
871 now: number,
872 tree: RouteTree,
873 seedRsc: React.ReactNode | null,
874 metadataVaryPath: PageVaryPath | null,
875 seedHead: HeadData | null,
876 freshness: FreshnessPolicy
877): { cacheNode: CacheNode; needsDynamicRequest: boolean } {
878 // Construct a new CacheNode using data from the BFCache, the client's
879 // Segment Cache, or seeded from a server response.
880 //
881 // If there's a cache miss, or if we only have a partial hit, we'll render
882 // the partial state immediately, and spawn a request to the server to fill
883 // in the missing data.
884 //
885 // If the segment is fully cached on the client already, we can omit this
886 // segment from the server request.
887 //
888 // If we already have a dynamic data response associated with this navigation,
889 // as in the case of a Server Action-initiated redirect or refresh, we may
890 // also be able to use that data without spawning a new request. (This is
891 // referred to as the "seed" data.)
892
893 const isPage = tree.isPage
894
895 // During certain kinds of navigations, we may be able to render from
896 // the BFCache.
897 switch (freshness) {
898 case FreshnessPolicy.Default: {
899 // When experimental.staleTimes.dynamic config is set, we read from the
900 // BFCache even during regular navigations. (This is not a recommended API
901 // with Cache Components, but it's supported for backwards compatibility.
902 // Use cacheLife instead.)
903
904 // This outer check isn't semantically necessary; even if the configured
905 // stale time is 0, the bfcache will return null, because any entry would
906 // have immediately expired. Just an optimization.
907 if (DYNAMIC_STALETIME_MS > 0) {
908 const bfcacheEntry = readFromBFCacheDuringRegularNavigation(
909 now,
910 tree.varyPath
911 )
912 if (bfcacheEntry !== null) {
913 return {
914 cacheNode: createCacheNode(
915 bfcacheEntry.rsc,
916 bfcacheEntry.prefetchRsc,
917 bfcacheEntry.head,
918 bfcacheEntry.prefetchHead
919 ),
920 needsDynamicRequest: false,
921 }
922 }
923 }
924 break
925 }
926 case FreshnessPolicy.Hydration: {
927 // This is not related to the BFCache but it is a special case.
928 //
929 // We should never spawn network requests during hydration. We must treat
930 // the initial payload as authoritative, because the initial page load is
931 // used as a last-ditch mechanism for recovering the app.
932 //
933 // This is also an important safety check because if this leaks into the
934 // server rendering path (which theoretically it never should because the
935 // server payload should be consistent), the server would hang because these
936 // promises would never resolve.
937 //
938 // TODO: There is an existing case where the global "not found" boundary
939 // triggers this path. But it does render correctly despite that. That's an
940 // unusual render path so it's not surprising, but we should look into
941 // modeling it in a more consistent way. See also the /_notFound special
942 // case in updateCacheNodeOnNavigation.
943 const rsc = seedRsc
944 const prefetchRsc = null
945 const head = isPage ? seedHead : null
946 const prefetchHead = null
947 writeToBFCache(now, tree.varyPath, rsc, prefetchRsc, head, prefetchHead)
948 if (isPage && metadataVaryPath !== null) {
949 writeHeadToBFCache(now, metadataVaryPath, head, prefetchHead)
950 }
951 return {
952 cacheNode: createCacheNode(rsc, prefetchRsc, head, prefetchHead),
953 needsDynamicRequest: false,
954 }
955 }
956 case FreshnessPolicy.HistoryTraversal:
957 const bfcacheEntry = readFromBFCache(tree.varyPath)
958 if (bfcacheEntry !== null) {
959 // Only show prefetched data if the dynamic data is still pending. This
960 // avoids a flash back to the prefetch state in a case where it's highly
961 // likely to have already streamed in.
962 //
963 // Tehnically, what we're actually checking is whether the dynamic
964 // network response was received. But since it's a streaming response,
965 // this does not mean that all the dynamic data has fully streamed in.
966 // It just means that _some_ of the dynamic data was received. But as a
967 // heuristic, we assume that the rest dynamic data will stream in
968 // quickly, so it's still better to skip the prefetch state.
969 const oldRsc = bfcacheEntry.rsc
970 const oldRscDidResolve =
971 !isDeferredRsc(oldRsc) || oldRsc.status !== 'pending'
972 const dropPrefetchRsc = oldRscDidResolve
973 return {
974 cacheNode: createCacheNode(
975 bfcacheEntry.rsc,
976 dropPrefetchRsc ? null : bfcacheEntry.prefetchRsc,
977 bfcacheEntry.head,
978 dropPrefetchRsc ? null : bfcacheEntry.prefetchHead
979 ),
980 needsDynamicRequest: false,
981 }
982 }
983 break
984 case FreshnessPolicy.RefreshAll:
985 case FreshnessPolicy.HMRRefresh:
986 case FreshnessPolicy.Gesture:
987 // Don't consult the BFCache.
988 break
989 default:
990 freshness satisfies never
991 break
992 }
993
994 let cachedRsc: React.ReactNode | null = null
995 let isCachedRscPartial: boolean = true
996
997 const segmentEntry = readSegmentCacheEntry(now, tree.varyPath)
998 if (segmentEntry !== null) {
999 switch (segmentEntry.status) {
1000 case EntryStatus.Fulfilled: {
1001 // Happy path: a cache hit
1002 cachedRsc = segmentEntry.rsc
1003 isCachedRscPartial = segmentEntry.isPartial
1004 break
1005 }
1006 case EntryStatus.Pending: {
1007 // We haven't received data for this segment yet, but there's already
1008 // an in-progress request. Since it's extremely likely to arrive
1009 // before the dynamic data response, we might as well use it.
1010 const promiseForFulfilledEntry = waitForSegmentCacheEntry(segmentEntry)
1011 cachedRsc = promiseForFulfilledEntry.then((entry) =>
1012 entry !== null ? entry.rsc : null
1013 )
1014 // Because the request is still pending, we typically don't know yet
1015 // whether the response will be partial. We shouldn't skip this segment
1016 // during the dynamic navigation request. Otherwise, we might need to
1017 // do yet another request to fill in the remaining data, creating
1018 // a waterfall.
1019 //
1020 // The one exception is if this segment is being fetched with via
1021 // prefetch={true} (i.e. the "force stale" or "full" strategy). If so,
1022 // we can assume the response will be full. This field is set to `false`
1023 // for such segments.
1024 isCachedRscPartial = segmentEntry.isPartial
1025 break
1026 }
1027 case EntryStatus.Empty:
1028 case EntryStatus.Rejected: {
1029 break
1030 }
1031 default: {
1032 segmentEntry satisfies never
1033 break
1034 }
1035 }
1036 }
1037
1038 // Now combine the cached data with the seed data to determine what we can
1039 // render immediately, versus what needs to stream in later.
1040
1041 // A partial state to show immediately while we wait for the final data to
1042 // arrive. If `rsc` is already a complete value (not partial), or if we
1043 // don't have any useful partial state, this will be `null`.
1044 let prefetchRsc: React.ReactNode | null
1045 // The final, resolved segment data. If the data is missing, this will be a
1046 // promise that resolves to the eventual data. A resolved value of `null`
1047 // means the data failed to load; the LayoutRouter will suspend indefinitely
1048 // until the router updates again (refer to finishNavigationTask).
1049 let rsc: React.ReactNode | null
1050 let doesSegmentNeedDynamicRequest: boolean
1051
1052 if (seedRsc !== null) {
1053 // We already have a dynamic server response for this segment.
1054 if (isCachedRscPartial) {
1055 // The seed data may still be streaming in, so it's worth showing the
1056 // partial cached state in the meantime.
1057 prefetchRsc = cachedRsc
1058 rsc = seedRsc
1059 } else {
1060 // We already have a completely cached segment. Ignore the seed data,
1061 // which may still be streaming in. This shouldn't happen in the normal
1062 // case because the client will inform the server which segments are
1063 // already fully cached, and the server will skip rendering them.
1064 prefetchRsc = null
1065 rsc = cachedRsc
1066 }
1067 doesSegmentNeedDynamicRequest = false
1068 } else {
1069 if (isCachedRscPartial) {
1070 // The cached data contains dynamic holes, or it's missing entirely. We'll
1071 // show the partial state immediately (if available), and stream in the
1072 // final data.
1073 //
1074 // Create a pending promise that we can later write to when the
1075 // data arrives from the server.
1076 prefetchRsc = cachedRsc
1077 rsc = createDeferredRsc()
1078 } else {
1079 // The data is fully cached.
1080 prefetchRsc = null
1081 rsc = cachedRsc
1082 }
1083 doesSegmentNeedDynamicRequest = isCachedRscPartial
1084 }
1085
1086 // If this is a page segment, we need to do the same for the head. This
1087 // follows analogous logic to the segment data above.
1088 // TODO: We don't need to store the head on the page segment's CacheNode; we
1089 // can lift it to the main state object. Then we can also delete
1090 // findHeadCache.
1091
1092 let prefetchHead: HeadData | null = null
1093 let head: React.ReactNode | null = null
1094 let doesHeadNeedDynamicRequest: boolean = isPage
1095
1096 if (isPage) {
1097 let cachedHead: HeadData | null = null
1098 let isCachedHeadPartial: boolean = true
1099 if (metadataVaryPath !== null) {
1100 const metadataEntry = readSegmentCacheEntry(now, metadataVaryPath)
1101 if (metadataEntry !== null) {
1102 switch (metadataEntry.status) {
1103 case EntryStatus.Fulfilled: {
1104 cachedHead = metadataEntry.rsc
1105 isCachedHeadPartial = metadataEntry.isPartial
1106 break
1107 }
1108 case EntryStatus.Pending: {
1109 cachedHead = waitForSegmentCacheEntry(metadataEntry).then(
1110 (entry) => (entry !== null ? entry.rsc : null)
1111 )
1112 isCachedHeadPartial = metadataEntry.isPartial
1113 break
1114 }
1115 case EntryStatus.Empty:
1116 case EntryStatus.Rejected: {
1117 break
1118 }
1119 default: {
1120 metadataEntry satisfies never
1121 break
1122 }
1123 }
1124 }
1125 }
1126
1127 if (process.env.__NEXT_OPTIMISTIC_ROUTING && isCachedHeadPartial) {
1128 // TODO: When optimistic routing is enabled, don't block on waiting for
1129 // the viewport to resolve. This is a temporary workaround until Vary
1130 // Params are tracked when rendering the metadata. We'll fix it before
1131 // this feature is stable. However, it's not a critical issue because 1)
1132 // it will stream in eventually anyway 2) metadata is wrapped in an
1133 // internal Suspense boundary, so is always non-blocking; this only
1134 // affects the viewport node, which is meant to blocking, however... 3)
1135 // before Segment Cache landed this wasn't always the case, anyway, so
1136 // it's unlikely that many people are relying on this behavior. Still,
1137 // will be fixed before stable. It's the very next step in the sequence of
1138 // work on this project.
1139 //
1140 // This line of code works because the App Router treats `null` as
1141 // "no renderable head available", rather than an empty head. React treats
1142 // an empty string as empty.
1143 cachedHead = ''
1144 }
1145
1146 if (seedHead !== null) {
1147 if (isCachedHeadPartial) {
1148 prefetchHead = cachedHead
1149 head = seedHead
1150 } else {
1151 prefetchHead = null
1152 head = cachedHead
1153 }
1154 doesHeadNeedDynamicRequest = false
1155 } else {
1156 if (isCachedHeadPartial) {
1157 prefetchHead = cachedHead
1158 head = createDeferredRsc()
1159 } else {
1160 prefetchHead = null
1161 head = cachedHead
1162 }
1163 doesHeadNeedDynamicRequest = isCachedHeadPartial
1164 }
1165 }
1166
1167 // Now that we're creating a new segment, write its data to the BFCache. A
1168 // subsequent back/forward navigation will reuse this same data, until or
1169 // unless it's cleared by a refresh/revalidation.
1170 //
1171 // Skip BFCache writes for optimistic navigations since they are transient
1172 // and will be replaced by the canonical navigation.
1173 if (freshness !== FreshnessPolicy.Gesture) {
1174 writeToBFCache(now, tree.varyPath, rsc, prefetchRsc, head, prefetchHead)
1175 if (isPage && metadataVaryPath !== null) {
1176 writeHeadToBFCache(now, metadataVaryPath, head, prefetchHead)
1177 }
1178 }
1179
1180 return {
1181 cacheNode: createCacheNode(rsc, prefetchRsc, head, prefetchHead),
1182 // TODO: We should store this field on the CacheNode itself. I think we can
1183 // probably unify NavigationTask, CacheNode, and DeferredRsc into a
1184 // single type. Or at least CacheNode and DeferredRsc.
1185 needsDynamicRequest:
1186 doesSegmentNeedDynamicRequest || doesHeadNeedDynamicRequest,
1187 }
1188}
1189
1190function createCacheNode(
1191 rsc: React.ReactNode | null,
1192 prefetchRsc: React.ReactNode | null,
1193 head: React.ReactNode | null,
1194 prefetchHead: HeadData | null
1195): CacheNode {
1196 return {
1197 rsc,
1198 prefetchRsc,
1199 head,
1200 prefetchHead,
1201 slots: null,
1202 }
1203}
1204
1205// Represents whether the previuos navigation resulted in a route tree mismatch.
1206// A mismatch results in a refresh of the page. If there are two successive
1207// mismatches, we will fall back to an MPA navigation, to prevent a retry loop.
1208let previousNavigationDidMismatch = false
1209
1210// Writes a dynamic server response into the tree created by
1211// updateCacheNodeOnNavigation. All pending promises that were spawned by the
1212// navigation will be resolved, either with dynamic data from the server, or
1213// `null` to indicate that the data is missing.
1214//
1215// A `null` value will trigger a lazy fetch during render, which will then patch
1216// up the tree using the same mechanism as the non-PPR implementation
1217// (serverPatchReducer).
1218//
1219// Usually, the server will respond with exactly the subset of data that we're
1220// waiting for — everything below the nearest shared layout. But technically,
1221// the server can return anything it wants.
1222//
1223// This does _not_ create a new tree; it modifies the existing one in place.
1224// Which means it must follow the Suspense rules of cache safety.
1225export function spawnDynamicRequests(
1226 task: NavigationTask,
1227 primaryUrl: URL,
1228 nextUrl: string | null,
1229 freshnessPolicy: FreshnessPolicy,
1230 accumulation: NavigationRequestAccumulation,
1231 // The route cache entry used for this navigation, if it came from route
1232 // prediction. Passed through so it can be marked as having a dynamic rewrite
1233 // if the server returns a different pathname than expected (indicating
1234 // dynamic rewrite behavior that varies by param value).
1235 routeCacheEntry: FulfilledRouteCacheEntry | null,
1236 // The original navigation's push/replace intent. Threaded through to the
1237 // server-patch retry logic so it can inherit the intent if the original
1238 // transition hasn't committed yet.
1239 navigateType: 'push' | 'replace'
1240): void {
1241 const dynamicRequestTree = task.dynamicRequestTree
1242 if (dynamicRequestTree === null) {
1243 // This navigation was fully cached. There are no dynamic requests to spawn.
1244 previousNavigationDidMismatch = false
1245 return
1246 }
1247
1248 // This is intentionally not an async function to discourage the caller from
1249 // awaiting the result. Any subsequent async operations spawned by this
1250 // function should result in a separate navigation task, rather than
1251 // block the original one.
1252 //
1253 // In this function we spawn (but do not await) all the network requests that
1254 // block the navigation, and collect the promises. The next function,
1255 // `finishNavigationTask`, can await the promises in any order without
1256 // accidentally introducing a network waterfall.
1257 const primaryRequestPromise = fetchMissingDynamicData(
1258 task,
1259 dynamicRequestTree,
1260 primaryUrl,
1261 nextUrl,
1262 freshnessPolicy,
1263 routeCacheEntry
1264 )
1265
1266 const separateRefreshUrls = accumulation.separateRefreshUrls
1267 let refreshRequestPromises: Array<
1268 ReturnType<typeof fetchMissingDynamicData>
1269 > | null = null
1270 if (separateRefreshUrls !== null) {
1271 // There are multiple URLs that we need to request the data from. This
1272 // happens when a "default" parallel route slot is present in the tree, and
1273 // its data cannot be fetched from the current route. We need to split the
1274 // combined dynamic request tree into separate requests per URL.
1275
1276 // TODO: Create a scoped dynamic request tree that omits anything that
1277 // is not relevant to the given URL. Without doing this, the server may
1278 // sometimes render more data than necessary; this is not a regression
1279 // compared to the pre-Segment Cache implementation, though, just an
1280 // optimization we can make in the future.
1281
1282 // Construct a request tree for each additional refresh URL. This will
1283 // prune away everything except the parts of the tree that match the
1284 // given refresh URL.
1285 refreshRequestPromises = []
1286 const canonicalUrl = createHrefFromUrl(primaryUrl)
1287 for (const refreshUrl of separateRefreshUrls) {
1288 if (refreshUrl === canonicalUrl) {
1289 // We already initiated a request for the this URL, above. Skip it.
1290 // TODO: This only happens because the main URL is not tracked as
1291 // part of the separateRefreshURLs set. There's probably a better way
1292 // to structure this so this case doesn't happen.
1293 continue
1294 }
1295 // TODO: Create a scoped dynamic request tree that omits anything that
1296 // is not relevant to the given URL. Without doing this, the server may
1297 // sometimes render more data than necessary; this is not a regression
1298 // compared to the pre-Segment Cache implementation, though, just an
1299 // optimization we can make in the future.
1300 // const scopedDynamicRequestTree = splitTaskByURL(task, refreshUrl)
1301 const scopedDynamicRequestTree = dynamicRequestTree
1302 if (scopedDynamicRequestTree !== null) {
1303 refreshRequestPromises.push(
1304 fetchMissingDynamicData(
1305 task,
1306 scopedDynamicRequestTree,
1307 new URL(refreshUrl, location.origin),
1308 // TODO: Just noticed that this should actually the Next-Url at the
1309 // time the refresh URL was set, not the current Next-Url. Need to
1310 // start tracking this alongside the refresh URL. In the meantime,
1311 // if a refresh fails due to a mismatch, it will trigger a
1312 // hard refresh.
1313 nextUrl,
1314 freshnessPolicy,
1315 routeCacheEntry
1316 )
1317 )
1318 }
1319 }
1320 }
1321
1322 // Further async operations are moved into this separate function to
1323 // discourage sequential network requests.
1324 const voidPromise = finishNavigationTask(
1325 task,
1326 nextUrl,
1327 primaryRequestPromise,
1328 refreshRequestPromises,
1329 routeCacheEntry,
1330 navigateType
1331 )
1332 // `finishNavigationTask` is responsible for error handling, so we can attach
1333 // noop callbacks to this promise.
1334 voidPromise.then(noop, noop)
1335}
1336
1337async function finishNavigationTask(
1338 task: NavigationTask,
1339 nextUrl: string | null,
1340 primaryRequestPromise: ReturnType<typeof fetchMissingDynamicData>,
1341 refreshRequestPromises: Array<
1342 ReturnType<typeof fetchMissingDynamicData>
1343 > | null,
1344 routeCacheEntry: FulfilledRouteCacheEntry | null,
1345 navigateType: 'push' | 'replace'
1346): Promise<void> {
1347 // Wait for all the requests to finish, or for the first one to fail.
1348 let exitStatus = await waitForRequestsToFinish(
1349 primaryRequestPromise,
1350 refreshRequestPromises
1351 )
1352
1353 // Once the all the requests have finished, check the tree for any remaining
1354 // pending tasks. If anything is still pending, it means the server response
1355 // does not match the client, and we must refresh to get back to a consistent
1356 // state. We can skip this step if we already detected a mismatch during the
1357 // first phase; it doesn't matter in that case because we're going to refresh
1358 // the whole tree regardless.
1359 if (exitStatus === NavigationTaskExitStatus.Done) {
1360 exitStatus = abortRemainingPendingTasks(task, null, null)
1361 }
1362
1363 switch (exitStatus) {
1364 case NavigationTaskExitStatus.Done: {
1365 // The task has completely finished. There's no missing data. Exit.
1366 previousNavigationDidMismatch = false
1367 return
1368 }
1369 case NavigationTaskExitStatus.SoftRetry: {
1370 // Some data failed to finish loading. Trigger a soft retry.
1371 // TODO: As an extra precaution against soft retry loops, consider
1372 // tracking whether a navigation was itself triggered by a retry. If two
1373 // happen in a row, fall back to a hard retry.
1374 const isHardRetry = false
1375 const primaryRequestResult = await primaryRequestPromise
1376 dispatchRetryDueToTreeMismatch(
1377 isHardRetry,
1378 primaryRequestResult.url,
1379 nextUrl,
1380 primaryRequestResult.seed,
1381 task.route,
1382 routeCacheEntry,
1383 navigateType
1384 )
1385 return
1386 }
1387 case NavigationTaskExitStatus.HardRetry: {
1388 // Some data failed to finish loading in a non-recoverable way, such as a
1389 // network error. Trigger an MPA navigation.
1390 //
1391 // Hard navigating/refreshing is how we prevent an infinite retry loop
1392 // caused by a network error — when the network fails, we fall back to the
1393 // browser behavior for offline navigations. In the future, Next.js may
1394 // introduce its own custom handling of offline navigations, but that
1395 // doesn't exist yet.
1396 const isHardRetry = true
1397 const primaryRequestResult = await primaryRequestPromise
1398 dispatchRetryDueToTreeMismatch(
1399 isHardRetry,
1400 primaryRequestResult.url,
1401 nextUrl,
1402 primaryRequestResult.seed,
1403 task.route,
1404 routeCacheEntry,
1405 navigateType
1406 )
1407 return
1408 }
1409 default: {
1410 return exitStatus satisfies never
1411 }
1412 }
1413}
1414
1415function waitForRequestsToFinish(
1416 primaryRequestPromise: ReturnType<typeof fetchMissingDynamicData>,
1417 refreshRequestPromises: Array<
1418 ReturnType<typeof fetchMissingDynamicData>
1419 > | null
1420) {
1421 // Custom async combinator logic. This could be replaced by Promise.any but
1422 // we don't assume that's available.
1423 //
1424 // Each promise resolves once the server responsds and the data is written
1425 // into the CacheNode tree. Resolve the combined promise once all the
1426 // requests finish.
1427 //
1428 // Or, resolve as soon as one of the requests fails, without waiting for the
1429 // others to finish.
1430 return new Promise<NavigationTaskExitStatus>((resolve) => {
1431 const onFulfill = (result: { exitStatus: NavigationTaskExitStatus }) => {
1432 if (result.exitStatus === NavigationTaskExitStatus.Done) {
1433 remainingCount--
1434 if (remainingCount === 0) {
1435 // All the requests finished successfully.
1436 resolve(NavigationTaskExitStatus.Done)
1437 }
1438 } else {
1439 // One of the requests failed. Exit with a failing status.
1440 // NOTE: It's possible for one of the requests to fail with SoftRetry
1441 // and a later one to fail with HardRetry. In this case, we choose to
1442 // retry immediately, rather than delay the retry until all the requests
1443 // finish. If it fails again, we will hard retry on the next
1444 // attempt, anyway.
1445 resolve(result.exitStatus)
1446 }
1447 }
1448 // onReject shouldn't ever be called because fetchMissingDynamicData's
1449 // entire body is wrapped in a try/catch. This is just defensive.
1450 const onReject = () => resolve(NavigationTaskExitStatus.HardRetry)
1451
1452 // Attach the listeners to the promises.
1453 let remainingCount = 1
1454 primaryRequestPromise.then(onFulfill, onReject)
1455 if (refreshRequestPromises !== null) {
1456 remainingCount += refreshRequestPromises.length
1457 refreshRequestPromises.forEach((refreshRequestPromise) =>
1458 refreshRequestPromise.then(onFulfill, onReject)
1459 )
1460 }
1461 })
1462}
1463
1464function dispatchRetryDueToTreeMismatch(
1465 isHardRetry: boolean,
1466 retryUrl: URL,
1467 retryNextUrl: string | null,
1468 seed: NavigationSeed | null,
1469 baseTree: FlightRouterState,
1470 // The route cache entry used for this navigation, if it came from route
1471 // prediction. If the navigation results in a mismatch, we mark it as having
1472 // a dynamic rewrite so future predictions bail out.
1473 routeCacheEntry: FulfilledRouteCacheEntry | null,
1474 // The original navigation's push/replace intent.
1475 originalNavigateType: 'push' | 'replace'
1476) {
1477 // If the navigation used a route prediction, mark it as having a dynamic
1478 // rewrite since it resulted in a mismatch.
1479 if (routeCacheEntry !== null) {
1480 markRouteEntryAsDynamicRewrite(routeCacheEntry)
1481 } else if (seed !== null) {
1482 // Even without a direct reference to the route cache entry, we can still
1483 // mark the route as having a dynamic rewrite by traversing the known route
1484 // tree. This handles cases where the navigation didn't originate from a
1485 // route prediction, but still needs to mark the pattern.
1486 const metadataVaryPath = seed.metadataVaryPath
1487 if (metadataVaryPath !== null) {
1488 const now = Date.now()
1489 discoverKnownRoute(
1490 now,
1491 retryUrl.pathname,
1492 retryNextUrl,
1493 null,
1494 seed.routeTree,
1495 metadataVaryPath,
1496 false, // couldBeIntercepted - doesn't matter, we're just marking hasDynamicRewrite
1497 createHrefFromUrl(retryUrl),
1498 false, // supportsPerSegmentPrefetching - doesn't matter, we're just marking hasDynamicRewrite
1499 true // hasDynamicRewrite
1500 )
1501 }
1502 }
1503
1504 // Invalidate all route cache entries. Other entries may have been derived
1505 // from the template before we knew it had a dynamic rewrite. This also
1506 // triggers re-prefetching of visible links.
1507 invalidateRouteCacheEntries(retryNextUrl, baseTree)
1508
1509 // If this is the second time in a row that a navigation resulted in a
1510 // mismatch, fall back to a hard (MPA) refresh.
1511 isHardRetry = isHardRetry || previousNavigationDidMismatch
1512 previousNavigationDidMismatch = true
1513
1514 // If the original navigation hasn't committed to the browser history yet
1515 // (the transition suspended before React committed), inherit its push/replace
1516 // intent. Otherwise, the pushState already ran, so use 'replace' to avoid
1517 // creating a duplicate history entry.
1518 //
1519 // This works because React entangles the retry's state update with the
1520 // original pending transition — they commit together as a single batch,
1521 // so the navigate type from the retry is what HistoryUpdater ultimately sees.
1522 //
1523 // TODO: Ideally this check would happen right before we schedule the React
1524 // update (i.e., closer to where the action is dispatched into the queue),
1525 // not here where the action is constructed. But the current action queue
1526 // doesn't provide a natural place for that. Revisit when we refactor the
1527 // action queue into a more reactive navigation model.
1528 const lastCommitted = getLastCommittedTree()
1529 const retryNavigateType: 'push' | 'replace' =
1530 lastCommitted !== null && baseTree !== lastCommitted
1531 ? originalNavigateType
1532 : 'replace'
1533
1534 const retryAction: ServerPatchAction = {
1535 type: ACTION_SERVER_PATCH,
1536 previousTree: baseTree,
1537 url: retryUrl,
1538 nextUrl: retryNextUrl,
1539 seed,
1540 mpa: isHardRetry,
1541 navigateType: retryNavigateType,
1542 }
1543 dispatchAppRouterAction(retryAction)
1544}
1545
1546async function fetchMissingDynamicData(
1547 task: NavigationTask,
1548 dynamicRequestTree: FlightRouterState,
1549 url: URL,
1550 nextUrl: string | null,
1551 freshnessPolicy: FreshnessPolicy,
1552 routeCacheEntry: FulfilledRouteCacheEntry | null
1553): Promise<{
1554 exitStatus: NavigationTaskExitStatus
1555 url: URL
1556 seed: NavigationSeed | null
1557}> {
1558 try {
1559 const result = await fetchServerResponse(url, {
1560 flightRouterState: dynamicRequestTree,
1561 nextUrl,
1562 isHmrRefresh: freshnessPolicy === FreshnessPolicy.HMRRefresh,
1563 })
1564 if (typeof result === 'string') {
1565 // fetchServerResponse will return an href to indicate that the SPA
1566 // navigation failed. For example, if the server triggered a hard
1567 // redirect, or the fetch request errored. Initiate an MPA navigation
1568 // to the given href.
1569 return {
1570 exitStatus: NavigationTaskExitStatus.HardRetry,
1571 url: new URL(result, location.origin),
1572 seed: null,
1573 }
1574 }
1575 const seed = convertServerPatchToFullTree(
1576 task.route,
1577 result.flightData,
1578 result.renderedSearch
1579 )
1580
1581 // If the navigation lock is active, wait for it to be released before
1582 // writing the dynamic data. This allows tests to assert on the prefetched
1583 // UI state.
1584 if (process.env.__NEXT_EXPOSE_TESTING_API) {
1585 await waitForNavigationLock()
1586 }
1587
1588 const now = Date.now()
1589
1590 if (routeCacheEntry !== null && result.staticStageData !== null) {
1591 const { response: staticStageResponse, isResponsePartial } =
1592 result.staticStageData
1593
1594 getStaleAt(now, staticStageResponse.s)
1595 .then((staleAt) => {
1596 const buildId =
1597 result.responseHeaders.get(NEXT_NAV_DEPLOYMENT_ID_HEADER) ??
1598 staticStageResponse.b
1599
1600 writeStaticStageResponseIntoCache(
1601 now,
1602 staticStageResponse.f,
1603 buildId,
1604 staticStageResponse.h,
1605 staleAt,
1606 dynamicRequestTree,
1607 result.renderedSearch,
1608 isResponsePartial
1609 )
1610 })
1611 .catch(() => {
1612 // The static stage processing failed. Not fatal — the navigation
1613 // completed normally, we just won't write into the cache.
1614 })
1615 }
1616
1617 if (routeCacheEntry !== null && result.runtimePrefetchStream !== null) {
1618 processRuntimePrefetchStream(
1619 now,
1620 result.runtimePrefetchStream,
1621 dynamicRequestTree,
1622 result.renderedSearch
1623 )
1624 .then((processed) => {
1625 if (processed !== null) {
1626 writeDynamicRenderResponseIntoCache(
1627 now,
1628 FetchStrategy.PPRRuntime,
1629 processed.flightDatas,
1630 processed.buildId,
1631 processed.isResponsePartial,
1632 processed.headVaryParams,
1633 processed.staleAt,
1634 processed.navigationSeed,
1635 null
1636 )
1637 }
1638 })
1639 .catch(() => {
1640 // The runtime prefetch cache write failed. Not fatal — the
1641 // navigation completed normally, we just won't cache runtime data.
1642 })
1643 }
1644
1645 const didReceiveUnknownParallelRoute = writeDynamicDataIntoNavigationTask(
1646 task,
1647 seed.routeTree,
1648 seed.data,
1649 seed.head,
1650 result.debugInfo
1651 )
1652 return {
1653 exitStatus: didReceiveUnknownParallelRoute
1654 ? NavigationTaskExitStatus.SoftRetry
1655 : NavigationTaskExitStatus.Done,
1656 url: new URL(result.canonicalUrl, location.origin),
1657 seed,
1658 }
1659 } catch {
1660 // This shouldn't happen because fetchServerResponse's entire body is
1661 // wrapped in a try/catch. If it does, though, it implies the server failed
1662 // to respond with any tree at all. So we must fall back to a hard retry.
1663 return {
1664 exitStatus: NavigationTaskExitStatus.HardRetry,
1665 url: url,
1666 seed: null,
1667 }
1668 }
1669}
1670
1671function writeDynamicDataIntoNavigationTask(
1672 task: NavigationTask,
1673 serverRouteTree: RouteTree,
1674 dynamicData: CacheNodeSeedData | null,
1675 dynamicHead: HeadData,
1676 debugInfo: Array<any> | null
1677): boolean {
1678 if (task.status === NavigationTaskStatus.Pending && dynamicData !== null) {
1679 task.status = NavigationTaskStatus.Fulfilled
1680 finishPendingCacheNode(task.node, dynamicData, dynamicHead, debugInfo)
1681 }
1682
1683 const taskChildren = task.children
1684 const serverChildren = serverRouteTree.slots
1685 const dynamicDataChildren = dynamicData !== null ? dynamicData[1] : null
1686
1687 // Detect whether the server sends a parallel route slot that the client
1688 // doesn't know about.
1689 let didReceiveUnknownParallelRoute = false
1690
1691 if (taskChildren !== null) {
1692 if (serverChildren !== null) {
1693 for (const parallelRouteKey in serverChildren) {
1694 const serverRouteTreeChild: RouteTree = serverChildren[parallelRouteKey]
1695 const dynamicDataChild: CacheNodeSeedData | null | void =
1696 dynamicDataChildren !== null
1697 ? dynamicDataChildren[parallelRouteKey]
1698 : null
1699
1700 const taskChild = taskChildren.get(parallelRouteKey)
1701 if (taskChild === undefined) {
1702 // The server sent a child segment that the client doesn't know about.
1703 //
1704 // When we receive an unknown parallel route, we must consider it a
1705 // mismatch. This is unlike the case where the segment itself
1706 // mismatches, because multiple routes can be active simultaneously.
1707 // But a given layout should never have a mismatching set of
1708 // child slots.
1709 //
1710 // Theoretically, this should only happen in development during an HMR
1711 // refresh, because the set of parallel routes for a layout does not
1712 // change over the lifetime of a build/deployment. In production, we
1713 // should have already mismatched on either the build id or the segment
1714 // path. But as an extra precaution, we validate in prod, too.
1715 didReceiveUnknownParallelRoute = true
1716 } else {
1717 const taskSegment = taskChild.route[0]
1718 const serverSegment = createSegmentFromRouteTree(serverRouteTreeChild)
1719 if (
1720 matchSegment(serverSegment, taskSegment) &&
1721 dynamicDataChild !== null &&
1722 dynamicDataChild !== undefined
1723 ) {
1724 // Found a match for this task. Keep traversing down the task tree.
1725 const childDidReceiveUnknownParallelRoute =
1726 writeDynamicDataIntoNavigationTask(
1727 taskChild,
1728 serverRouteTreeChild,
1729 dynamicDataChild,
1730 dynamicHead,
1731 debugInfo
1732 )
1733 if (childDidReceiveUnknownParallelRoute) {
1734 didReceiveUnknownParallelRoute = true
1735 }
1736 }
1737 }
1738 }
1739 } else {
1740 if (serverChildren !== null) {
1741 // The server sent a child segment that the client doesn't know about.
1742 didReceiveUnknownParallelRoute = true
1743 }
1744 }
1745 }
1746
1747 return didReceiveUnknownParallelRoute
1748}
1749
1750function finishPendingCacheNode(
1751 cacheNode: CacheNode,
1752 dynamicData: CacheNodeSeedData,
1753 dynamicHead: HeadData,
1754 debugInfo: Array<any> | null
1755): void {
1756 // Writes a dynamic response into an existing Cache Node tree. This does _not_
1757 // create a new tree, it updates the existing tree in-place. So it must follow
1758 // the Suspense rules of cache safety — it can resolve pending promises, but
1759 // it cannot overwrite existing data. It can add segments to the tree (because
1760 // a missing segment will cause the layout router to suspend).
1761 // but it cannot delete them.
1762 //
1763 // We must resolve every promise in the tree, or else it will suspend
1764 // indefinitely. If we did not receive data for a segment, we will resolve its
1765 // data promise to `null` to trigger a lazy fetch during render.
1766
1767 // Use the dynamic data from the server to fulfill the deferred RSC promise
1768 // on the Cache Node.
1769 const rsc = cacheNode.rsc
1770 const dynamicSegmentData = dynamicData[0]
1771
1772 if (dynamicSegmentData === null) {
1773 // This is an empty CacheNode; this particular server request did not
1774 // render this segment. There may be a separate pending request that will,
1775 // though, so we won't abort the task until all pending requests finish.
1776 return
1777 }
1778
1779 if (rsc === null) {
1780 // This is a lazy cache node. We can overwrite it. This is only safe
1781 // because we know that the LayoutRouter suspends if `rsc` is `null`.
1782 cacheNode.rsc = dynamicSegmentData
1783 } else if (isDeferredRsc(rsc)) {
1784 // This is a deferred RSC promise. We can fulfill it with the data we just
1785 // received from the server. If it was already resolved by a different
1786 // navigation, then this does nothing because we can't overwrite data.
1787 rsc.resolve(dynamicSegmentData, debugInfo)
1788 } else {
1789 // This is not a deferred RSC promise, nor is it empty, so it must have
1790 // been populated by a different navigation. We must not overwrite it.
1791 }
1792
1793 // Check if this is a leaf segment. If so, it will have a `head` property with
1794 // a pending promise that needs to be resolved with the dynamic head from
1795 // the server.
1796 const head = cacheNode.head
1797 if (isDeferredRsc(head)) {
1798 head.resolve(dynamicHead, debugInfo)
1799 }
1800}
1801
1802function abortRemainingPendingTasks(
1803 task: NavigationTask,
1804 error: any,
1805 debugInfo: Array<any> | null
1806): NavigationTaskExitStatus {
1807 let exitStatus
1808 if (task.status === NavigationTaskStatus.Pending) {
1809 // The data for this segment is still missing.
1810 task.status = NavigationTaskStatus.Rejected
1811 abortPendingCacheNode(task.node, error, debugInfo)
1812
1813 // If the server failed to fulfill the data for this segment, it implies
1814 // that the route tree received from the server mismatched the tree that
1815 // was previously prefetched.
1816 //
1817 // In an app with fully static routes and no proxy-driven redirects or
1818 // rewrites, this should never happen, because the route for a URL would
1819 // always be the same across multiple requests. So, this implies that some
1820 // runtime routing condition changed, likely in a proxy, without being
1821 // pushed to the client.
1822 //
1823 // When this happens, we treat this the same as a refresh(). The entire
1824 // tree will be re-rendered from the root.
1825 if (task.refreshState === null) {
1826 // Trigger a "soft" refresh. Essentially the same as calling `refresh()`
1827 // in a Server Action.
1828 exitStatus = NavigationTaskExitStatus.SoftRetry
1829 } else {
1830 // The mismatch was discovered inside an inactive parallel route. This
1831 // implies the inactive parallel route is no longer reachable at the URL
1832 // that originally rendered it. Fall back to an MPA refresh.
1833 // TODO: An alternative could be to trigger a soft refresh but to _not_
1834 // re-use the inactive parallel routes this time. Similar to what would
1835 // happen if were to do a hard refrehs, but without the HTML page.
1836 exitStatus = NavigationTaskExitStatus.HardRetry
1837 }
1838 } else {
1839 // This segment finished. (An error here is treated as Done because they are
1840 // surfaced to the application during render.)
1841 exitStatus = NavigationTaskExitStatus.Done
1842 }
1843
1844 const taskChildren = task.children
1845 if (taskChildren !== null) {
1846 for (const [, taskChild] of taskChildren) {
1847 const childExitStatus = abortRemainingPendingTasks(
1848 taskChild,
1849 error,
1850 debugInfo
1851 )
1852 // Propagate the exit status up the tree. The statuses are ordered by
1853 // their precedence.
1854 if (childExitStatus > exitStatus) {
1855 exitStatus = childExitStatus
1856 }
1857 }
1858 }
1859
1860 return exitStatus
1861}
1862
1863function abortPendingCacheNode(
1864 cacheNode: CacheNode,
1865 error: any,
1866 debugInfo: Array<any> | null
1867): void {
1868 const rsc = cacheNode.rsc
1869 if (isDeferredRsc(rsc)) {
1870 if (error === null) {
1871 // This will trigger a lazy fetch during render.
1872 rsc.resolve(null, debugInfo)
1873 } else {
1874 // This will trigger an error during rendering.
1875 rsc.reject(error, debugInfo)
1876 }
1877 }
1878
1879 // Check if this is a leaf segment. If so, it will have a `head` property with
1880 // a pending promise that needs to be resolved. If an error was provided, we
1881 // will not resolve it with an error, since this is rendered at the root of
1882 // the app. We want the segment to error, not the entire app.
1883 const head = cacheNode.head
1884 if (isDeferredRsc(head)) {
1885 head.resolve(null, debugInfo)
1886 }
1887}
1888
1889const DEFERRED = Symbol()
1890
1891type PendingDeferredRsc<T> = Promise<T> & {
1892 status: 'pending'
1893 resolve: (value: T, debugInfo: Array<any> | null) => void
1894 reject: (error: any, debugInfo: Array<any> | null) => void
1895 tag: Symbol
1896 _debugInfo: Array<any>
1897}
1898
1899type FulfilledDeferredRsc<T> = Promise<T> & {
1900 status: 'fulfilled'
1901 value: T
1902 resolve: (value: T, debugInfo: Array<any> | null) => void
1903 reject: (error: any, debugInfo: Array<any> | null) => void
1904 tag: Symbol
1905 _debugInfo: Array<any>
1906}
1907
1908type RejectedDeferredRsc<T> = Promise<T> & {
1909 status: 'rejected'
1910 reason: any
1911 resolve: (value: T, debugInfo: Array<any> | null) => void
1912 reject: (error: any, debugInfo: Array<any> | null) => void
1913 tag: Symbol
1914 _debugInfo: Array<any>
1915}
1916
1917type DeferredRsc<T extends React.ReactNode = React.ReactNode> =
1918 | PendingDeferredRsc<T>
1919 | FulfilledDeferredRsc<T>
1920 | RejectedDeferredRsc<T>
1921
1922// This type exists to distinguish a DeferredRsc from a Flight promise. It's a
1923// compromise to avoid adding an extra field on every Cache Node, which would be
1924// awkward because the pre-PPR parts of codebase would need to account for it,
1925// too. We can remove it once type Cache Node type is more settled.
1926export function isDeferredRsc(value: any): value is DeferredRsc {
1927 return value && typeof value === 'object' && value.tag === DEFERRED
1928}
1929
1930function createDeferredRsc<
1931 T extends React.ReactNode = React.ReactNode,
1932>(): PendingDeferredRsc<T> {
1933 // Create an unresolved promise that represents data derived from a Flight
1934 // response. The promise will be resolved later as soon as we start receiving
1935 // data from the server, i.e. as soon as the Flight client decodes and returns
1936 // the top-level response object.
1937
1938 // The `_debugInfo` field contains profiling information. Promises that are
1939 // created by Flight already have this info added by React; for any derived
1940 // promise created by the router, we need to transfer the Flight debug info
1941 // onto the derived promise.
1942 //
1943 // The debug info represents the latency between the start of the navigation
1944 // and the start of rendering. (It does not represent the time it takes for
1945 // whole stream to finish.)
1946 const debugInfo: Array<any> = []
1947
1948 let resolve: any
1949 let reject: any
1950 const pendingRsc = new Promise<T>((res, rej) => {
1951 resolve = res
1952 reject = rej
1953 }) as PendingDeferredRsc<T>
1954 pendingRsc.status = 'pending'
1955 pendingRsc.resolve = (value: T, responseDebugInfo: Array<any> | null) => {
1956 if (pendingRsc.status === 'pending') {
1957 const fulfilledRsc: FulfilledDeferredRsc<T> = pendingRsc as any
1958 fulfilledRsc.status = 'fulfilled'
1959 fulfilledRsc.value = value
1960 if (responseDebugInfo !== null) {
1961 // Transfer the debug info to the derived promise.
1962 debugInfo.push.apply(debugInfo, responseDebugInfo)
1963 }
1964 resolve(value)
1965 }
1966 }
1967 pendingRsc.reject = (error: any, responseDebugInfo: Array<any> | null) => {
1968 if (pendingRsc.status === 'pending') {
1969 const rejectedRsc: RejectedDeferredRsc<T> = pendingRsc as any
1970 rejectedRsc.status = 'rejected'
1971 rejectedRsc.reason = error
1972 if (responseDebugInfo !== null) {
1973 // Transfer the debug info to the derived promise.
1974 debugInfo.push.apply(debugInfo, responseDebugInfo)
1975 }
1976 reject(error)
1977 }
1978 }
1979 pendingRsc.tag = DEFERRED
1980 pendingRsc._debugInfo = debugInfo
1981
1982 return pendingRsc
1983}
1984
1985/**
1986 * Helper for the Instant Navigation Testing API. Waits for the navigation lock
1987 * to be released before returning. The network request has already completed by
1988 * the time this is called, so this only delays writing the dynamic data.
1989 *
1990 * Not exposed in production builds by default.
1991 */
1992async function waitForNavigationLock(): Promise<void> {
1993 if (process.env.__NEXT_EXPOSE_TESTING_API) {
1994 const { waitForNavigationLockIfActive } =
1995 require('../segment-cache/navigation-testing-lock') as typeof import('../segment-cache/navigation-testing-lock')
1996 await waitForNavigationLockIfActive()
1997 }
1998}
1999