| | import type { |
| | TreePrefetch, |
| | RootTreePrefetch, |
| | SegmentPrefetch, |
| | } from '../../../server/app-render/collect-segment-data' |
| | import type { LoadingModuleData } from '../../../shared/lib/app-router-types' |
| | import type { |
| | CacheNodeSeedData, |
| | Segment as FlightRouterStateSegment, |
| | } from '../../../shared/lib/app-router-types' |
| | import { HasLoadingBoundary } from '../../../shared/lib/app-router-types' |
| | import { |
| | NEXT_DID_POSTPONE_HEADER, |
| | NEXT_ROUTER_PREFETCH_HEADER, |
| | NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, |
| | NEXT_ROUTER_STALE_TIME_HEADER, |
| | NEXT_ROUTER_STATE_TREE_HEADER, |
| | NEXT_URL, |
| | RSC_CONTENT_TYPE_HEADER, |
| | RSC_HEADER, |
| | } from '../app-router-headers' |
| | import { |
| | createFetch, |
| | createFromNextReadableStream, |
| | type RSCResponse, |
| | type RequestHeaders, |
| | } from '../router-reducer/fetch-server-response' |
| | import { |
| | pingPrefetchTask, |
| | isPrefetchTaskDirty, |
| | type PrefetchTask, |
| | type PrefetchSubtaskResult, |
| | startRevalidationCooldown, |
| | } from './scheduler' |
| | import { |
| | type RouteVaryPath, |
| | type SegmentVaryPath, |
| | type PartialSegmentVaryPath, |
| | getRouteVaryPath, |
| | getFulfilledRouteVaryPath, |
| | getSegmentVaryPathForRequest, |
| | appendLayoutVaryPath, |
| | finalizeLayoutVaryPath, |
| | finalizePageVaryPath, |
| | clonePageVaryPathWithNewSearchParams, |
| | type PageVaryPath, |
| | finalizeMetadataVaryPath, |
| | } from './vary-path' |
| | import { getAppBuildId } from '../../app-build-id' |
| | import { createHrefFromUrl } from '../router-reducer/create-href-from-url' |
| | import type { NormalizedSearch, RouteCacheKey } from './cache-key' |
| | |
| | import { createCacheKey as createPrefetchRequestKey } from './cache-key' |
| | import { |
| | doesStaticSegmentAppearInURL, |
| | getCacheKeyForDynamicParam, |
| | getRenderedPathname, |
| | getRenderedSearch, |
| | parseDynamicParamFromURLPart, |
| | } from '../../route-params' |
| | import { |
| | createCacheMap, |
| | getFromCacheMap, |
| | setInCacheMap, |
| | setSizeInCacheMap, |
| | deleteFromCacheMap, |
| | isValueExpired, |
| | type CacheMap, |
| | type UnknownMapEntry, |
| | } from './cache-map' |
| | import { |
| | appendSegmentRequestKeyPart, |
| | convertSegmentPathToStaticExportFilename, |
| | createSegmentRequestKeyPart, |
| | HEAD_REQUEST_KEY, |
| | ROOT_SEGMENT_REQUEST_KEY, |
| | type SegmentRequestKey, |
| | } from '../../../shared/lib/segment-cache/segment-value-encoding' |
| | import type { |
| | FlightRouterState, |
| | NavigationFlightResponse, |
| | } from '../../../shared/lib/app-router-types' |
| | import { |
| | normalizeFlightData, |
| | prepareFlightRouterStateForRequest, |
| | } from '../../flight-data-helpers' |
| | import { STATIC_STALETIME_MS } from '../router-reducer/reducers/navigate-reducer' |
| | import { pingVisibleLinks } from '../links' |
| | import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment' |
| | import { FetchStrategy } from './types' |
| | import { createPromiseWithResolvers } from '../../../shared/lib/promise-with-resolvers' |
| |
|
| | |
| | |
| | |
| | |
| | export function getStaleTimeMs(staleTimeSeconds: number): number { |
| | return Math.max(staleTimeSeconds, 30) * 1000 |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | type RouteTreeShared = { |
| | requestKey: SegmentRequestKey |
| | |
| | |
| | segment: FlightRouterStateSegment |
| | slots: null | { |
| | [parallelRouteKey: string]: RouteTree |
| | } |
| | isRootLayout: boolean |
| |
|
| | |
| | |
| | |
| | |
| | |
| | hasLoadingBoundary: HasLoadingBoundary |
| |
|
| | |
| | |
| | |
| | |
| | hasRuntimePrefetch: boolean |
| | } |
| |
|
| | type LayoutRouteTree = RouteTreeShared & { |
| | isPage: false |
| | varyPath: SegmentVaryPath |
| | } |
| |
|
| | type PageRouteTree = RouteTreeShared & { |
| | isPage: true |
| | varyPath: PageVaryPath |
| | } |
| |
|
| | export type RouteTree = LayoutRouteTree | PageRouteTree |
| |
|
| | type RouteCacheEntryShared = { |
| | |
| | |
| | |
| | couldBeIntercepted: boolean |
| |
|
| | |
| | ref: UnknownMapEntry | null |
| | size: number |
| | staleAt: number |
| | version: number |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export const enum EntryStatus { |
| | Empty = 0, |
| | Pending = 1, |
| | Fulfilled = 2, |
| | Rejected = 3, |
| | } |
| |
|
| | type PendingRouteCacheEntry = RouteCacheEntryShared & { |
| | status: EntryStatus.Empty | EntryStatus.Pending |
| | blockedTasks: Set<PrefetchTask> | null |
| | canonicalUrl: null |
| | renderedSearch: null |
| | tree: null |
| | metadata: null |
| | isPPREnabled: false |
| | } |
| |
|
| | type RejectedRouteCacheEntry = RouteCacheEntryShared & { |
| | status: EntryStatus.Rejected |
| | blockedTasks: Set<PrefetchTask> | null |
| | canonicalUrl: null |
| | renderedSearch: null |
| | tree: null |
| | metadata: null |
| | isPPREnabled: boolean |
| | } |
| |
|
| | export type FulfilledRouteCacheEntry = RouteCacheEntryShared & { |
| | status: EntryStatus.Fulfilled |
| | blockedTasks: null |
| | canonicalUrl: string |
| | renderedSearch: NormalizedSearch |
| | tree: RouteTree |
| | metadata: RouteTree |
| | isPPREnabled: boolean |
| | } |
| |
|
| | export type RouteCacheEntry = |
| | | PendingRouteCacheEntry |
| | | FulfilledRouteCacheEntry |
| | | RejectedRouteCacheEntry |
| |
|
| | type SegmentCacheEntryShared = { |
| | fetchStrategy: FetchStrategy |
| |
|
| | |
| | ref: UnknownMapEntry | null |
| | size: number |
| | staleAt: number |
| | version: number |
| | } |
| |
|
| | export type EmptySegmentCacheEntry = SegmentCacheEntryShared & { |
| | status: EntryStatus.Empty |
| | rsc: null |
| | loading: null |
| | isPartial: true |
| | promise: null |
| | } |
| |
|
| | export type PendingSegmentCacheEntry = SegmentCacheEntryShared & { |
| | status: EntryStatus.Pending |
| | rsc: null |
| | loading: null |
| | isPartial: boolean |
| | promise: null | PromiseWithResolvers<FulfilledSegmentCacheEntry | null> |
| | } |
| |
|
| | type RejectedSegmentCacheEntry = SegmentCacheEntryShared & { |
| | status: EntryStatus.Rejected |
| | rsc: null |
| | loading: null |
| | isPartial: true |
| | promise: null |
| | } |
| |
|
| | export type FulfilledSegmentCacheEntry = SegmentCacheEntryShared & { |
| | status: EntryStatus.Fulfilled |
| | rsc: React.ReactNode | null |
| | loading: LoadingModuleData | Promise<LoadingModuleData> |
| | isPartial: boolean |
| | promise: null |
| | } |
| |
|
| | export type SegmentCacheEntry = |
| | | EmptySegmentCacheEntry |
| | | PendingSegmentCacheEntry |
| | | RejectedSegmentCacheEntry |
| | | FulfilledSegmentCacheEntry |
| |
|
| | export type NonEmptySegmentCacheEntry = Exclude< |
| | SegmentCacheEntry, |
| | EmptySegmentCacheEntry |
| | > |
| |
|
| | const isOutputExportMode = |
| | process.env.NODE_ENV === 'production' && |
| | process.env.__NEXT_CONFIG_OUTPUT === 'export' |
| |
|
| | const MetadataOnlyRequestTree: FlightRouterState = [ |
| | '', |
| | {}, |
| | null, |
| | 'metadata-only', |
| | ] |
| |
|
| | let routeCacheMap: CacheMap<RouteCacheEntry> = createCacheMap() |
| | let segmentCacheMap: CacheMap<SegmentCacheEntry> = createCacheMap() |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | let invalidationListeners: Set<PrefetchTask> | null = null |
| |
|
| | |
| | let currentCacheVersion = 0 |
| |
|
| | export function getCurrentCacheVersion(): number { |
| | return currentCacheVersion |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function revalidateEntireCache( |
| | nextUrl: string | null, |
| | tree: FlightRouterState |
| | ) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | currentCacheVersion++ |
| |
|
| | |
| | startRevalidationCooldown() |
| |
|
| | |
| | pingVisibleLinks(nextUrl, tree) |
| |
|
| | |
| | |
| | |
| | pingInvalidationListeners(nextUrl, tree) |
| | } |
| |
|
| | function attachInvalidationListener(task: PrefetchTask): void { |
| | |
| | |
| | |
| | |
| | |
| | if (task.onInvalidate !== null) { |
| | if (invalidationListeners === null) { |
| | invalidationListeners = new Set([task]) |
| | } else { |
| | invalidationListeners.add(task) |
| | } |
| | } |
| | } |
| |
|
| | function notifyInvalidationListener(task: PrefetchTask): void { |
| | const onInvalidate = task.onInvalidate |
| | if (onInvalidate !== null) { |
| | |
| | |
| | task.onInvalidate = null |
| |
|
| | |
| | try { |
| | onInvalidate() |
| | } catch (error) { |
| | if (typeof reportError === 'function') { |
| | reportError(error) |
| | } else { |
| | console.error(error) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | export function pingInvalidationListeners( |
| | nextUrl: string | null, |
| | tree: FlightRouterState |
| | ): void { |
| | |
| | |
| | |
| | |
| | if (invalidationListeners !== null) { |
| | const tasks = invalidationListeners |
| | invalidationListeners = null |
| | for (const task of tasks) { |
| | if (isPrefetchTaskDirty(task, nextUrl, tree)) { |
| | notifyInvalidationListener(task) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | export function readRouteCacheEntry( |
| | now: number, |
| | key: RouteCacheKey |
| | ): RouteCacheEntry | null { |
| | const varyPath: RouteVaryPath = getRouteVaryPath( |
| | key.pathname, |
| | key.search, |
| | key.nextUrl |
| | ) |
| | const isRevalidation = false |
| | return getFromCacheMap( |
| | now, |
| | getCurrentCacheVersion(), |
| | routeCacheMap, |
| | varyPath, |
| | isRevalidation |
| | ) |
| | } |
| |
|
| | export function readSegmentCacheEntry( |
| | now: number, |
| | varyPath: SegmentVaryPath |
| | ): SegmentCacheEntry | null { |
| | const isRevalidation = false |
| | return getFromCacheMap( |
| | now, |
| | getCurrentCacheVersion(), |
| | segmentCacheMap, |
| | varyPath, |
| | isRevalidation |
| | ) |
| | } |
| |
|
| | function readRevalidatingSegmentCacheEntry( |
| | now: number, |
| | varyPath: SegmentVaryPath |
| | ): SegmentCacheEntry | null { |
| | const isRevalidation = true |
| | return getFromCacheMap( |
| | now, |
| | getCurrentCacheVersion(), |
| | segmentCacheMap, |
| | varyPath, |
| | isRevalidation |
| | ) |
| | } |
| |
|
| | export function waitForSegmentCacheEntry( |
| | pendingEntry: PendingSegmentCacheEntry |
| | ): Promise<FulfilledSegmentCacheEntry | null> { |
| | |
| | |
| | let promiseWithResolvers = pendingEntry.promise |
| | if (promiseWithResolvers === null) { |
| | promiseWithResolvers = pendingEntry.promise = |
| | createPromiseWithResolvers<FulfilledSegmentCacheEntry | null>() |
| | } else { |
| | |
| | } |
| | return promiseWithResolvers.promise |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function readOrCreateRouteCacheEntry( |
| | now: number, |
| | task: PrefetchTask, |
| | key: RouteCacheKey |
| | ): RouteCacheEntry { |
| | attachInvalidationListener(task) |
| |
|
| | const existingEntry = readRouteCacheEntry(now, key) |
| | if (existingEntry !== null) { |
| | return existingEntry |
| | } |
| | |
| | const pendingEntry: PendingRouteCacheEntry = { |
| | canonicalUrl: null, |
| | status: EntryStatus.Empty, |
| | blockedTasks: null, |
| | tree: null, |
| | metadata: null, |
| | |
| | |
| | |
| | couldBeIntercepted: true, |
| | |
| | isPPREnabled: false, |
| | renderedSearch: null, |
| |
|
| | |
| | ref: null, |
| | size: 0, |
| | |
| | |
| | staleAt: Infinity, |
| | version: getCurrentCacheVersion(), |
| | } |
| | const varyPath: RouteVaryPath = getRouteVaryPath( |
| | key.pathname, |
| | key.search, |
| | key.nextUrl |
| | ) |
| | const isRevalidation = false |
| | setInCacheMap(routeCacheMap, varyPath, pendingEntry, isRevalidation) |
| | return pendingEntry |
| | } |
| |
|
| | export function requestOptimisticRouteCacheEntry( |
| | now: number, |
| | requestedUrl: URL, |
| | nextUrl: string | null |
| | ): FulfilledRouteCacheEntry | null { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | const requestedSearch = requestedUrl.search as NormalizedSearch |
| | if (requestedSearch === '') { |
| | |
| | |
| | return null |
| | } |
| | const urlWithoutSearchParams = new URL(requestedUrl) |
| | urlWithoutSearchParams.search = '' |
| | const routeWithNoSearchParams = readRouteCacheEntry( |
| | now, |
| | createPrefetchRequestKey(urlWithoutSearchParams.href, nextUrl) |
| | ) |
| |
|
| | if ( |
| | routeWithNoSearchParams === null || |
| | routeWithNoSearchParams.status !== EntryStatus.Fulfilled |
| | ) { |
| | |
| | |
| | return null |
| | } |
| |
|
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const canonicalUrlForRouteWithNoSearchParams = new URL( |
| | routeWithNoSearchParams.canonicalUrl, |
| | requestedUrl.origin |
| | ) |
| | const optimisticCanonicalSearch = |
| | canonicalUrlForRouteWithNoSearchParams.search !== '' |
| | ? |
| | canonicalUrlForRouteWithNoSearchParams.search |
| | : requestedSearch |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const optimisticRenderedSearch = |
| | routeWithNoSearchParams.renderedSearch !== '' |
| | ? |
| | routeWithNoSearchParams.renderedSearch |
| | : requestedSearch |
| |
|
| | const optimisticUrl = new URL( |
| | routeWithNoSearchParams.canonicalUrl, |
| | location.origin |
| | ) |
| | optimisticUrl.search = optimisticCanonicalSearch |
| | const optimisticCanonicalUrl = createHrefFromUrl(optimisticUrl) |
| |
|
| | const optimisticRouteTree = createOptimisticRouteTree( |
| | routeWithNoSearchParams.tree, |
| | optimisticRenderedSearch |
| | ) |
| | const optimisticMetadataTree = createOptimisticRouteTree( |
| | routeWithNoSearchParams.metadata, |
| | optimisticRenderedSearch |
| | ) |
| |
|
| | |
| | |
| | const optimisticEntry: FulfilledRouteCacheEntry = { |
| | canonicalUrl: optimisticCanonicalUrl, |
| |
|
| | status: EntryStatus.Fulfilled, |
| | |
| | blockedTasks: null, |
| | tree: optimisticRouteTree, |
| | metadata: optimisticMetadataTree, |
| | couldBeIntercepted: routeWithNoSearchParams.couldBeIntercepted, |
| | isPPREnabled: routeWithNoSearchParams.isPPREnabled, |
| |
|
| | |
| | renderedSearch: optimisticRenderedSearch, |
| |
|
| | |
| | ref: null, |
| | size: 0, |
| | staleAt: routeWithNoSearchParams.staleAt, |
| | version: routeWithNoSearchParams.version, |
| | } |
| |
|
| | |
| | |
| | return optimisticEntry |
| | } |
| |
|
| | function createOptimisticRouteTree( |
| | tree: RouteTree, |
| | newRenderedSearch: NormalizedSearch |
| | ): RouteTree { |
| | |
| | |
| |
|
| | let clonedSlots: Record<string, RouteTree> | null = null |
| | const originalSlots = tree.slots |
| | if (originalSlots !== null) { |
| | clonedSlots = {} |
| | for (const parallelRouteKey in originalSlots) { |
| | const childTree = originalSlots[parallelRouteKey] |
| | clonedSlots[parallelRouteKey] = createOptimisticRouteTree( |
| | childTree, |
| | newRenderedSearch |
| | ) |
| | } |
| | } |
| |
|
| | |
| | if (tree.isPage) { |
| | return { |
| | requestKey: tree.requestKey, |
| | segment: tree.segment, |
| | varyPath: clonePageVaryPathWithNewSearchParams( |
| | tree.varyPath, |
| | newRenderedSearch |
| | ), |
| | isPage: true, |
| | slots: clonedSlots, |
| | isRootLayout: tree.isRootLayout, |
| | hasLoadingBoundary: tree.hasLoadingBoundary, |
| | hasRuntimePrefetch: tree.hasRuntimePrefetch, |
| | } |
| | } |
| |
|
| | return { |
| | requestKey: tree.requestKey, |
| | segment: tree.segment, |
| | varyPath: tree.varyPath, |
| | isPage: false, |
| | slots: clonedSlots, |
| | isRootLayout: tree.isRootLayout, |
| | hasLoadingBoundary: tree.hasLoadingBoundary, |
| | hasRuntimePrefetch: tree.hasRuntimePrefetch, |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function readOrCreateSegmentCacheEntry( |
| | now: number, |
| | fetchStrategy: FetchStrategy, |
| | route: FulfilledRouteCacheEntry, |
| | tree: RouteTree |
| | ): SegmentCacheEntry { |
| | const existingEntry = readSegmentCacheEntry(now, tree.varyPath) |
| | if (existingEntry !== null) { |
| | return existingEntry |
| | } |
| | |
| | const varyPathForRequest = getSegmentVaryPathForRequest(fetchStrategy, tree) |
| | const pendingEntry = createDetachedSegmentCacheEntry(route.staleAt) |
| | const isRevalidation = false |
| | setInCacheMap( |
| | segmentCacheMap, |
| | varyPathForRequest, |
| | pendingEntry, |
| | isRevalidation |
| | ) |
| | return pendingEntry |
| | } |
| |
|
| | export function readOrCreateRevalidatingSegmentEntry( |
| | now: number, |
| | fetchStrategy: FetchStrategy, |
| | route: FulfilledRouteCacheEntry, |
| | tree: RouteTree |
| | ): SegmentCacheEntry { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const existingEntry = readRevalidatingSegmentCacheEntry(now, tree.varyPath) |
| | if (existingEntry !== null) { |
| | return existingEntry |
| | } |
| | |
| | const varyPathForRequest = getSegmentVaryPathForRequest(fetchStrategy, tree) |
| | const pendingEntry = createDetachedSegmentCacheEntry(route.staleAt) |
| | const isRevalidation = true |
| | setInCacheMap( |
| | segmentCacheMap, |
| | varyPathForRequest, |
| | pendingEntry, |
| | isRevalidation |
| | ) |
| | return pendingEntry |
| | } |
| |
|
| | export function overwriteRevalidatingSegmentCacheEntry( |
| | fetchStrategy: FetchStrategy, |
| | route: FulfilledRouteCacheEntry, |
| | tree: RouteTree |
| | ) { |
| | |
| | |
| | |
| | const varyPathForRequest = getSegmentVaryPathForRequest(fetchStrategy, tree) |
| | const pendingEntry = createDetachedSegmentCacheEntry(route.staleAt) |
| | const isRevalidation = true |
| | setInCacheMap( |
| | segmentCacheMap, |
| | varyPathForRequest, |
| | pendingEntry, |
| | isRevalidation |
| | ) |
| | return pendingEntry |
| | } |
| |
|
| | export function upsertSegmentEntry( |
| | now: number, |
| | varyPath: SegmentVaryPath, |
| | candidateEntry: SegmentCacheEntry |
| | ): SegmentCacheEntry | null { |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | if (isValueExpired(now, getCurrentCacheVersion(), candidateEntry)) { |
| | |
| | return null |
| | } |
| |
|
| | const existingEntry = readSegmentCacheEntry(now, varyPath) |
| | if (existingEntry !== null) { |
| | |
| | |
| | |
| | if ( |
| | |
| | |
| | (candidateEntry.fetchStrategy !== existingEntry.fetchStrategy && |
| | !canNewFetchStrategyProvideMoreContent( |
| | existingEntry.fetchStrategy, |
| | candidateEntry.fetchStrategy |
| | )) || |
| | |
| | |
| | (!existingEntry.isPartial && candidateEntry.isPartial) |
| | ) { |
| | |
| | |
| | |
| | |
| | |
| | const rejectedEntry: RejectedSegmentCacheEntry = candidateEntry as any |
| | rejectedEntry.status = EntryStatus.Rejected |
| | rejectedEntry.loading = null |
| | rejectedEntry.rsc = null |
| | return null |
| | } |
| |
|
| | |
| | deleteFromCacheMap(existingEntry) |
| | } |
| |
|
| | const isRevalidation = false |
| | setInCacheMap(segmentCacheMap, varyPath, candidateEntry, isRevalidation) |
| | return candidateEntry |
| | } |
| |
|
| | export function createDetachedSegmentCacheEntry( |
| | staleAt: number |
| | ): EmptySegmentCacheEntry { |
| | const emptyEntry: EmptySegmentCacheEntry = { |
| | status: EntryStatus.Empty, |
| | |
| | |
| | fetchStrategy: FetchStrategy.PPR, |
| | rsc: null, |
| | loading: null, |
| | isPartial: true, |
| | promise: null, |
| |
|
| | |
| | ref: null, |
| | size: 0, |
| | staleAt, |
| | version: 0, |
| | } |
| | return emptyEntry |
| | } |
| |
|
| | export function upgradeToPendingSegment( |
| | emptyEntry: EmptySegmentCacheEntry, |
| | fetchStrategy: FetchStrategy |
| | ): PendingSegmentCacheEntry { |
| | const pendingEntry: PendingSegmentCacheEntry = emptyEntry as any |
| | pendingEntry.status = EntryStatus.Pending |
| | pendingEntry.fetchStrategy = fetchStrategy |
| |
|
| | if (fetchStrategy === FetchStrategy.Full) { |
| | |
| | |
| | |
| | pendingEntry.isPartial = false |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | pendingEntry.version = getCurrentCacheVersion() |
| | return pendingEntry |
| | } |
| |
|
| | function pingBlockedTasks(entry: { |
| | blockedTasks: Set<PrefetchTask> | null |
| | }): void { |
| | const blockedTasks = entry.blockedTasks |
| | if (blockedTasks !== null) { |
| | for (const task of blockedTasks) { |
| | pingPrefetchTask(task) |
| | } |
| | entry.blockedTasks = null |
| | } |
| | } |
| |
|
| | function fulfillRouteCacheEntry( |
| | entry: RouteCacheEntry, |
| | tree: RouteTree, |
| | metadataVaryPath: PageVaryPath, |
| | staleAt: number, |
| | couldBeIntercepted: boolean, |
| | canonicalUrl: string, |
| | renderedSearch: NormalizedSearch, |
| | isPPREnabled: boolean |
| | ): FulfilledRouteCacheEntry { |
| | |
| | |
| | |
| | |
| | const metadata: RouteTree = { |
| | requestKey: HEAD_REQUEST_KEY, |
| | segment: HEAD_REQUEST_KEY, |
| | varyPath: metadataVaryPath, |
| | |
| | |
| | |
| | isPage: true, |
| | slots: null, |
| | isRootLayout: false, |
| | hasLoadingBoundary: HasLoadingBoundary.SubtreeHasNoLoadingBoundary, |
| | hasRuntimePrefetch: false, |
| | } |
| | const fulfilledEntry: FulfilledRouteCacheEntry = entry as any |
| | fulfilledEntry.status = EntryStatus.Fulfilled |
| | fulfilledEntry.tree = tree |
| | fulfilledEntry.metadata = metadata |
| | fulfilledEntry.staleAt = staleAt |
| | fulfilledEntry.couldBeIntercepted = couldBeIntercepted |
| | fulfilledEntry.canonicalUrl = canonicalUrl |
| | fulfilledEntry.renderedSearch = renderedSearch |
| | fulfilledEntry.isPPREnabled = isPPREnabled |
| | pingBlockedTasks(entry) |
| | return fulfilledEntry |
| | } |
| |
|
| | function fulfillSegmentCacheEntry( |
| | segmentCacheEntry: PendingSegmentCacheEntry, |
| | rsc: React.ReactNode, |
| | loading: LoadingModuleData | Promise<LoadingModuleData>, |
| | staleAt: number, |
| | isPartial: boolean |
| | ): FulfilledSegmentCacheEntry { |
| | const fulfilledEntry: FulfilledSegmentCacheEntry = segmentCacheEntry as any |
| | fulfilledEntry.status = EntryStatus.Fulfilled |
| | fulfilledEntry.rsc = rsc |
| | fulfilledEntry.loading = loading |
| | fulfilledEntry.staleAt = staleAt |
| | fulfilledEntry.isPartial = isPartial |
| | |
| | if (segmentCacheEntry.promise !== null) { |
| | segmentCacheEntry.promise.resolve(fulfilledEntry) |
| | |
| | fulfilledEntry.promise = null |
| | } |
| | return fulfilledEntry |
| | } |
| |
|
| | function rejectRouteCacheEntry( |
| | entry: PendingRouteCacheEntry, |
| | staleAt: number |
| | ): void { |
| | const rejectedEntry: RejectedRouteCacheEntry = entry as any |
| | rejectedEntry.status = EntryStatus.Rejected |
| | rejectedEntry.staleAt = staleAt |
| | pingBlockedTasks(entry) |
| | } |
| |
|
| | function rejectSegmentCacheEntry( |
| | entry: PendingSegmentCacheEntry, |
| | staleAt: number |
| | ): void { |
| | const rejectedEntry: RejectedSegmentCacheEntry = entry as any |
| | rejectedEntry.status = EntryStatus.Rejected |
| | rejectedEntry.staleAt = staleAt |
| | if (entry.promise !== null) { |
| | |
| | |
| | entry.promise.resolve(null) |
| | entry.promise = null |
| | } |
| | } |
| |
|
| | type RouteTreeAccumulator = { |
| | metadataVaryPath: PageVaryPath | null |
| | } |
| |
|
| | function convertRootTreePrefetchToRouteTree( |
| | rootTree: RootTreePrefetch, |
| | renderedPathname: string, |
| | renderedSearch: NormalizedSearch, |
| | acc: RouteTreeAccumulator |
| | ) { |
| | |
| | const pathnameParts = renderedPathname.split('/').filter((p) => p !== '') |
| | const index = 0 |
| | const rootSegment = ROOT_SEGMENT_REQUEST_KEY |
| | return convertTreePrefetchToRouteTree( |
| | rootTree.tree, |
| | rootSegment, |
| | null, |
| | ROOT_SEGMENT_REQUEST_KEY, |
| | pathnameParts, |
| | index, |
| | renderedSearch, |
| | acc |
| | ) |
| | } |
| |
|
| | function convertTreePrefetchToRouteTree( |
| | prefetch: TreePrefetch, |
| | segment: FlightRouterStateSegment, |
| | partialVaryPath: PartialSegmentVaryPath | null, |
| | requestKey: SegmentRequestKey, |
| | pathnameParts: Array<string>, |
| | pathnamePartsIndex: number, |
| | renderedSearch: NormalizedSearch, |
| | acc: RouteTreeAccumulator |
| | ): RouteTree { |
| | |
| | |
| | |
| | |
| | |
| |
|
| | let slots: { [parallelRouteKey: string]: RouteTree } | null = null |
| | let isPage: boolean |
| | let varyPath: SegmentVaryPath |
| | const prefetchSlots = prefetch.slots |
| | if (prefetchSlots !== null) { |
| | isPage = false |
| | varyPath = finalizeLayoutVaryPath(requestKey, partialVaryPath) |
| |
|
| | slots = {} |
| | for (let parallelRouteKey in prefetchSlots) { |
| | const childPrefetch = prefetchSlots[parallelRouteKey] |
| | const childParamName = childPrefetch.name |
| | const childParamType = childPrefetch.paramType |
| | const childServerSentParamKey = childPrefetch.paramKey |
| |
|
| | let childDoesAppearInURL: boolean |
| | let childSegment: FlightRouterStateSegment |
| | let childPartialVaryPath: PartialSegmentVaryPath | null |
| | if (childParamType !== null) { |
| | |
| | const childParamValue = parseDynamicParamFromURLPart( |
| | childParamType, |
| | pathnameParts, |
| | pathnamePartsIndex |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const childParamKey = |
| | |
| | |
| | childServerSentParamKey !== null |
| | ? childServerSentParamKey |
| | : |
| | getCacheKeyForDynamicParam( |
| | childParamValue, |
| | '' as NormalizedSearch |
| | ) |
| |
|
| | childPartialVaryPath = appendLayoutVaryPath( |
| | partialVaryPath, |
| | childParamKey |
| | ) |
| | childSegment = [childParamName, childParamKey, childParamType] |
| | childDoesAppearInURL = true |
| | } else { |
| | |
| | |
| | childPartialVaryPath = partialVaryPath |
| | childSegment = childParamName |
| | childDoesAppearInURL = doesStaticSegmentAppearInURL(childParamName) |
| | } |
| |
|
| | |
| | |
| | const childPathnamePartsIndex = childDoesAppearInURL |
| | ? pathnamePartsIndex + 1 |
| | : pathnamePartsIndex |
| |
|
| | const childRequestKeyPart = createSegmentRequestKeyPart(childSegment) |
| | const childRequestKey = appendSegmentRequestKeyPart( |
| | requestKey, |
| | parallelRouteKey, |
| | childRequestKeyPart |
| | ) |
| | slots[parallelRouteKey] = convertTreePrefetchToRouteTree( |
| | childPrefetch, |
| | childSegment, |
| | childPartialVaryPath, |
| | childRequestKey, |
| | pathnameParts, |
| | childPathnamePartsIndex, |
| | renderedSearch, |
| | acc |
| | ) |
| | } |
| | } else { |
| | if (requestKey.endsWith(PAGE_SEGMENT_KEY)) { |
| | |
| | isPage = true |
| | varyPath = finalizePageVaryPath( |
| | requestKey, |
| | renderedSearch, |
| | partialVaryPath |
| | ) |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (acc.metadataVaryPath === null) { |
| | acc.metadataVaryPath = finalizeMetadataVaryPath( |
| | requestKey, |
| | renderedSearch, |
| | partialVaryPath |
| | ) |
| | } |
| | } else { |
| | |
| | isPage = false |
| | varyPath = finalizeLayoutVaryPath(requestKey, partialVaryPath) |
| | } |
| | } |
| |
|
| | return { |
| | requestKey, |
| | segment, |
| | varyPath, |
| | |
| | |
| | |
| | |
| | |
| | |
| | isPage: isPage as boolean as any, |
| | slots, |
| | isRootLayout: prefetch.isRootLayout, |
| | |
| | |
| | hasLoadingBoundary: HasLoadingBoundary.SegmentHasLoadingBoundary, |
| | hasRuntimePrefetch: prefetch.hasRuntimePrefetch, |
| | } |
| | } |
| |
|
| | function convertRootFlightRouterStateToRouteTree( |
| | flightRouterState: FlightRouterState, |
| | renderedSearch: NormalizedSearch, |
| | acc: RouteTreeAccumulator |
| | ): RouteTree { |
| | return convertFlightRouterStateToRouteTree( |
| | flightRouterState, |
| | ROOT_SEGMENT_REQUEST_KEY, |
| | null, |
| | renderedSearch, |
| | acc |
| | ) |
| | } |
| |
|
| | function convertFlightRouterStateToRouteTree( |
| | flightRouterState: FlightRouterState, |
| | requestKey: SegmentRequestKey, |
| | parentPartialVaryPath: PartialSegmentVaryPath | null, |
| | renderedSearch: NormalizedSearch, |
| | acc: RouteTreeAccumulator |
| | ): RouteTree { |
| | const originalSegment = flightRouterState[0] |
| |
|
| | let segment: FlightRouterStateSegment |
| | let partialVaryPath: PartialSegmentVaryPath | null |
| | let isPage: boolean |
| | let varyPath: SegmentVaryPath |
| | if (Array.isArray(originalSegment)) { |
| | isPage = false |
| | const paramCacheKey = originalSegment[1] |
| | partialVaryPath = appendLayoutVaryPath(parentPartialVaryPath, paramCacheKey) |
| | varyPath = finalizeLayoutVaryPath(requestKey, partialVaryPath) |
| | segment = originalSegment |
| | } else { |
| | |
| | |
| | partialVaryPath = parentPartialVaryPath |
| | if (requestKey.endsWith(PAGE_SEGMENT_KEY)) { |
| | |
| | isPage = true |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | segment = PAGE_SEGMENT_KEY |
| | varyPath = finalizePageVaryPath( |
| | requestKey, |
| | renderedSearch, |
| | partialVaryPath |
| | ) |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (acc.metadataVaryPath === null) { |
| | acc.metadataVaryPath = finalizeMetadataVaryPath( |
| | requestKey, |
| | renderedSearch, |
| | partialVaryPath |
| | ) |
| | } |
| | } else { |
| | |
| | isPage = false |
| | segment = originalSegment |
| | varyPath = finalizeLayoutVaryPath(requestKey, partialVaryPath) |
| | } |
| | } |
| |
|
| | let slots: { [parallelRouteKey: string]: RouteTree } | null = null |
| |
|
| | const parallelRoutes = flightRouterState[1] |
| | for (let parallelRouteKey in parallelRoutes) { |
| | const childRouterState = parallelRoutes[parallelRouteKey] |
| | const childSegment = childRouterState[0] |
| | |
| | |
| | |
| | const childRequestKeyPart = createSegmentRequestKeyPart(childSegment) |
| | const childRequestKey = appendSegmentRequestKeyPart( |
| | requestKey, |
| | parallelRouteKey, |
| | childRequestKeyPart |
| | ) |
| | const childTree = convertFlightRouterStateToRouteTree( |
| | childRouterState, |
| | childRequestKey, |
| | partialVaryPath, |
| | renderedSearch, |
| | acc |
| | ) |
| | if (slots === null) { |
| | slots = { |
| | [parallelRouteKey]: childTree, |
| | } |
| | } else { |
| | slots[parallelRouteKey] = childTree |
| | } |
| | } |
| |
|
| | return { |
| | requestKey, |
| | segment, |
| | varyPath, |
| | |
| | |
| | |
| | |
| | |
| | |
| | isPage: isPage as boolean as any, |
| | slots, |
| | isRootLayout: flightRouterState[4] === true, |
| | hasLoadingBoundary: |
| | flightRouterState[5] !== undefined |
| | ? flightRouterState[5] |
| | : HasLoadingBoundary.SubtreeHasNoLoadingBoundary, |
| |
|
| | |
| | |
| | hasRuntimePrefetch: false, |
| | } |
| | } |
| |
|
| | export function convertRouteTreeToFlightRouterState( |
| | routeTree: RouteTree |
| | ): FlightRouterState { |
| | const parallelRoutes: Record<string, FlightRouterState> = {} |
| | if (routeTree.slots !== null) { |
| | for (const parallelRouteKey in routeTree.slots) { |
| | parallelRoutes[parallelRouteKey] = convertRouteTreeToFlightRouterState( |
| | routeTree.slots[parallelRouteKey] |
| | ) |
| | } |
| | } |
| | const flightRouterState: FlightRouterState = [ |
| | routeTree.segment, |
| | parallelRoutes, |
| | null, |
| | null, |
| | routeTree.isRootLayout, |
| | ] |
| | return flightRouterState |
| | } |
| |
|
| | export async function fetchRouteOnCacheMiss( |
| | entry: PendingRouteCacheEntry, |
| | task: PrefetchTask, |
| | key: RouteCacheKey |
| | ): Promise<PrefetchSubtaskResult<null> | null> { |
| | |
| | |
| | |
| | |
| | const pathname = key.pathname |
| | const search = key.search |
| | const nextUrl = key.nextUrl |
| | const segmentPath = '/_tree' as SegmentRequestKey |
| |
|
| | const headers: RequestHeaders = { |
| | [RSC_HEADER]: '1', |
| | [NEXT_ROUTER_PREFETCH_HEADER]: '1', |
| | [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: segmentPath, |
| | } |
| | if (nextUrl !== null) { |
| | headers[NEXT_URL] = nextUrl |
| | } |
| |
|
| | try { |
| | const url = new URL(pathname + search, location.origin) |
| | let response |
| | let urlAfterRedirects |
| | if (isOutputExportMode) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const headResponse = await fetch(url, { |
| | method: 'HEAD', |
| | }) |
| | if (headResponse.status < 200 || headResponse.status >= 400) { |
| | |
| | |
| | |
| | |
| | |
| | rejectRouteCacheEntry(entry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | urlAfterRedirects = headResponse.redirected |
| | ? new URL(headResponse.url) |
| | : url |
| |
|
| | response = await fetchPrefetchResponse( |
| | addSegmentPathToUrlInOutputExportMode(urlAfterRedirects, segmentPath), |
| | headers |
| | ) |
| | } else { |
| | |
| | |
| | |
| | |
| | response = await fetchPrefetchResponse(url, headers) |
| | urlAfterRedirects = |
| | response !== null && response.redirected ? new URL(response.url) : url |
| | } |
| |
|
| | if ( |
| | !response || |
| | !response.ok || |
| | |
| | |
| | |
| | response.status === 204 || |
| | !response.body |
| | ) { |
| | |
| | |
| | rejectRouteCacheEntry(entry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const canonicalUrl = createHrefFromUrl(urlAfterRedirects) |
| |
|
| | |
| | const varyHeader = response.headers.get('vary') |
| | const couldBeIntercepted = |
| | varyHeader !== null && varyHeader.includes(NEXT_URL) |
| |
|
| | |
| | const closed = createPromiseWithResolvers<void>() |
| |
|
| | |
| | |
| | |
| | const routeIsPPREnabled = |
| | response.headers.get(NEXT_DID_POSTPONE_HEADER) === '2' || |
| | |
| | |
| | |
| | isOutputExportMode |
| |
|
| | if (routeIsPPREnabled) { |
| | const prefetchStream = createPrefetchResponseStream( |
| | response.body, |
| | closed.resolve, |
| | function onResponseSizeUpdate(size) { |
| | setSizeInCacheMap(entry, size) |
| | } |
| | ) |
| | const serverData = await createFromNextReadableStream<RootTreePrefetch>( |
| | prefetchStream, |
| | headers |
| | ) |
| | if (serverData.buildId !== getAppBuildId()) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | rejectRouteCacheEntry(entry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | |
| | |
| | |
| | const renderedPathname = getRenderedPathname(response) |
| | const renderedSearch = getRenderedSearch(response) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const acc: RouteTreeAccumulator = { metadataVaryPath: null } |
| | const routeTree = convertRootTreePrefetchToRouteTree( |
| | serverData, |
| | renderedPathname, |
| | renderedSearch, |
| | acc |
| | ) |
| | const metadataVaryPath = acc.metadataVaryPath |
| | if (metadataVaryPath === null) { |
| | rejectRouteCacheEntry(entry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | const staleTimeMs = getStaleTimeMs(serverData.staleTime) |
| | fulfillRouteCacheEntry( |
| | entry, |
| | routeTree, |
| | metadataVaryPath, |
| | Date.now() + staleTimeMs, |
| | couldBeIntercepted, |
| | canonicalUrl, |
| | renderedSearch, |
| | routeIsPPREnabled |
| | ) |
| | } else { |
| | |
| | |
| | |
| | |
| | |
| | const prefetchStream = createPrefetchResponseStream( |
| | response.body, |
| | closed.resolve, |
| | function onResponseSizeUpdate(size) { |
| | setSizeInCacheMap(entry, size) |
| | } |
| | ) |
| | const serverData = |
| | await createFromNextReadableStream<NavigationFlightResponse>( |
| | prefetchStream, |
| | headers |
| | ) |
| | if (serverData.b !== getAppBuildId()) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | rejectRouteCacheEntry(entry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | writeDynamicTreeResponseIntoCache( |
| | Date.now(), |
| | task, |
| | |
| | |
| | FetchStrategy.LoadingBoundary, |
| | response as RSCResponse<NavigationFlightResponse>, |
| | serverData, |
| | entry, |
| | couldBeIntercepted, |
| | canonicalUrl, |
| | routeIsPPREnabled |
| | ) |
| | } |
| |
|
| | if (!couldBeIntercepted) { |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | const fulfilledVaryPath: RouteVaryPath = getFulfilledRouteVaryPath( |
| | pathname, |
| | search, |
| | nextUrl, |
| | couldBeIntercepted |
| | ) |
| | const isRevalidation = false |
| | setInCacheMap(routeCacheMap, fulfilledVaryPath, entry, isRevalidation) |
| | } |
| | |
| | |
| | return { value: null, closed: closed.promise } |
| | } catch (error) { |
| | |
| | |
| | rejectRouteCacheEntry(entry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| | } |
| |
|
| | export async function fetchSegmentOnCacheMiss( |
| | route: FulfilledRouteCacheEntry, |
| | segmentCacheEntry: PendingSegmentCacheEntry, |
| | routeKey: RouteCacheKey, |
| | tree: RouteTree |
| | ): Promise<PrefetchSubtaskResult<FulfilledSegmentCacheEntry> | null> { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | const url = new URL(route.canonicalUrl, location.origin) |
| | const nextUrl = routeKey.nextUrl |
| |
|
| | const requestKey = tree.requestKey |
| | const normalizedRequestKey = |
| | requestKey === ROOT_SEGMENT_REQUEST_KEY |
| | ? |
| | |
| | |
| | |
| | |
| | |
| | ('/_index' as SegmentRequestKey) |
| | : requestKey |
| |
|
| | const headers: RequestHeaders = { |
| | [RSC_HEADER]: '1', |
| | [NEXT_ROUTER_PREFETCH_HEADER]: '1', |
| | [NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]: normalizedRequestKey, |
| | } |
| | if (nextUrl !== null) { |
| | headers[NEXT_URL] = nextUrl |
| | } |
| |
|
| | const requestUrl = isOutputExportMode |
| | ? |
| | addSegmentPathToUrlInOutputExportMode(url, normalizedRequestKey) |
| | : url |
| | try { |
| | const response = await fetchPrefetchResponse(requestUrl, headers) |
| | if ( |
| | !response || |
| | !response.ok || |
| | response.status === 204 || |
| | |
| | |
| | |
| | |
| | |
| | (response.headers.get(NEXT_DID_POSTPONE_HEADER) !== '2' && |
| | |
| | |
| | |
| | !isOutputExportMode) || |
| | !response.body |
| | ) { |
| | |
| | |
| | rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | |
| | const closed = createPromiseWithResolvers<void>() |
| |
|
| | |
| | |
| | const prefetchStream = createPrefetchResponseStream( |
| | response.body, |
| | closed.resolve, |
| | function onResponseSizeUpdate(size) { |
| | setSizeInCacheMap(segmentCacheEntry, size) |
| | } |
| | ) |
| | const serverData = await (createFromNextReadableStream( |
| | prefetchStream, |
| | headers |
| | ) as Promise<SegmentPrefetch>) |
| | if (serverData.buildId !== getAppBuildId()) { |
| | |
| | |
| | |
| | |
| | |
| | rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| | return { |
| | value: fulfillSegmentCacheEntry( |
| | segmentCacheEntry, |
| | serverData.rsc, |
| | serverData.loading, |
| | |
| | |
| | route.staleAt, |
| | serverData.isPartial |
| | ), |
| | |
| | |
| | closed: closed.promise, |
| | } |
| | } catch (error) { |
| | |
| | |
| | rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000) |
| | return null |
| | } |
| | } |
| |
|
| | export async function fetchSegmentPrefetchesUsingDynamicRequest( |
| | task: PrefetchTask, |
| | route: FulfilledRouteCacheEntry, |
| | fetchStrategy: |
| | | FetchStrategy.LoadingBoundary |
| | | FetchStrategy.PPRRuntime |
| | | FetchStrategy.Full, |
| | dynamicRequestTree: FlightRouterState, |
| | spawnedEntries: Map<SegmentRequestKey, PendingSegmentCacheEntry> |
| | ): Promise<PrefetchSubtaskResult<null> | null> { |
| | const key = task.key |
| | const url = new URL(route.canonicalUrl, location.origin) |
| | const nextUrl = key.nextUrl |
| |
|
| | if ( |
| | spawnedEntries.size === 1 && |
| | spawnedEntries.has(route.metadata.requestKey) |
| | ) { |
| | |
| | |
| | dynamicRequestTree = MetadataOnlyRequestTree |
| | } |
| |
|
| | const headers: RequestHeaders = { |
| | [RSC_HEADER]: '1', |
| | [NEXT_ROUTER_STATE_TREE_HEADER]: |
| | prepareFlightRouterStateForRequest(dynamicRequestTree), |
| | } |
| | if (nextUrl !== null) { |
| | headers[NEXT_URL] = nextUrl |
| | } |
| | switch (fetchStrategy) { |
| | case FetchStrategy.Full: { |
| | |
| | |
| | |
| | break |
| | } |
| | case FetchStrategy.PPRRuntime: { |
| | headers[NEXT_ROUTER_PREFETCH_HEADER] = '2' |
| | break |
| | } |
| | case FetchStrategy.LoadingBoundary: { |
| | headers[NEXT_ROUTER_PREFETCH_HEADER] = '1' |
| | break |
| | } |
| | default: { |
| | fetchStrategy satisfies never |
| | } |
| | } |
| |
|
| | try { |
| | const response = await fetchPrefetchResponse(url, headers) |
| | if (!response || !response.ok || !response.body) { |
| | |
| | |
| | rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | const renderedSearch = getRenderedSearch(response) |
| | if (renderedSearch !== route.renderedSearch) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000) |
| | return null |
| | } |
| |
|
| | |
| | const closed = createPromiseWithResolvers<void>() |
| |
|
| | let fulfilledEntries: Array<FulfilledSegmentCacheEntry> | null = null |
| | const prefetchStream = createPrefetchResponseStream( |
| | response.body, |
| | closed.resolve, |
| | function onResponseSizeUpdate(totalBytesReceivedSoFar) { |
| | |
| | |
| | |
| | if (fulfilledEntries === null) { |
| | |
| | |
| | return |
| | } |
| | const averageSize = totalBytesReceivedSoFar / fulfilledEntries.length |
| | for (const entry of fulfilledEntries) { |
| | setSizeInCacheMap(entry, averageSize) |
| | } |
| | } |
| | ) |
| | const serverData = await (createFromNextReadableStream( |
| | prefetchStream, |
| | headers |
| | ) as Promise<NavigationFlightResponse>) |
| |
|
| | const isResponsePartial = |
| | fetchStrategy === FetchStrategy.PPRRuntime |
| | ? |
| | serverData.rp?.[0] === true |
| | : |
| | |
| | false |
| |
|
| | |
| | |
| | |
| | fulfilledEntries = writeDynamicRenderResponseIntoCache( |
| | Date.now(), |
| | task, |
| | fetchStrategy, |
| | response as RSCResponse<NavigationFlightResponse>, |
| | serverData, |
| | isResponsePartial, |
| | route, |
| | spawnedEntries |
| | ) |
| |
|
| | |
| | |
| | return { value: null, closed: closed.promise } |
| | } catch (error) { |
| | rejectSegmentEntriesIfStillPending(spawnedEntries, Date.now() + 10 * 1000) |
| | return null |
| | } |
| | } |
| |
|
| | function writeDynamicTreeResponseIntoCache( |
| | now: number, |
| | task: PrefetchTask, |
| | fetchStrategy: |
| | | FetchStrategy.LoadingBoundary |
| | | FetchStrategy.PPRRuntime |
| | | FetchStrategy.Full, |
| | response: RSCResponse<NavigationFlightResponse>, |
| | serverData: NavigationFlightResponse, |
| | entry: PendingRouteCacheEntry, |
| | couldBeIntercepted: boolean, |
| | canonicalUrl: string, |
| | routeIsPPREnabled: boolean |
| | ) { |
| | |
| | |
| | const renderedSearch = getRenderedSearch(response) |
| |
|
| | const normalizedFlightDataResult = normalizeFlightData(serverData.f) |
| | if ( |
| | |
| | |
| | typeof normalizedFlightDataResult === 'string' || |
| | normalizedFlightDataResult.length !== 1 |
| | ) { |
| | rejectRouteCacheEntry(entry, now + 10 * 1000) |
| | return |
| | } |
| | const flightData = normalizedFlightDataResult[0] |
| | if (!flightData.isRootRender) { |
| | |
| | rejectRouteCacheEntry(entry, now + 10 * 1000) |
| | return |
| | } |
| |
|
| | const flightRouterState = flightData.tree |
| | |
| | |
| | const staleTimeSeconds = |
| | typeof serverData.rp?.[1] === 'number' |
| | ? serverData.rp[1] |
| | : parseInt(response.headers.get(NEXT_ROUTER_STALE_TIME_HEADER) ?? '', 10) |
| | const staleTimeMs = !isNaN(staleTimeSeconds) |
| | ? getStaleTimeMs(staleTimeSeconds) |
| | : STATIC_STALETIME_MS |
| |
|
| | |
| | |
| | |
| | |
| | const isResponsePartial = |
| | response.headers.get(NEXT_DID_POSTPONE_HEADER) === '1' |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const acc: RouteTreeAccumulator = { metadataVaryPath: null } |
| | const routeTree = convertRootFlightRouterStateToRouteTree( |
| | flightRouterState, |
| | renderedSearch, |
| | acc |
| | ) |
| | const metadataVaryPath = acc.metadataVaryPath |
| | if (metadataVaryPath === null) { |
| | rejectRouteCacheEntry(entry, now + 10 * 1000) |
| | return |
| | } |
| |
|
| | const fulfilledEntry = fulfillRouteCacheEntry( |
| | entry, |
| | routeTree, |
| | metadataVaryPath, |
| | now + staleTimeMs, |
| | couldBeIntercepted, |
| | canonicalUrl, |
| | renderedSearch, |
| | routeIsPPREnabled |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | writeDynamicRenderResponseIntoCache( |
| | now, |
| | task, |
| | fetchStrategy, |
| | response, |
| | serverData, |
| | isResponsePartial, |
| | fulfilledEntry, |
| | null |
| | ) |
| | } |
| |
|
| | function rejectSegmentEntriesIfStillPending( |
| | entries: Map<SegmentRequestKey, SegmentCacheEntry>, |
| | staleAt: number |
| | ): Array<FulfilledSegmentCacheEntry> { |
| | const fulfilledEntries = [] |
| | for (const entry of entries.values()) { |
| | if (entry.status === EntryStatus.Pending) { |
| | rejectSegmentCacheEntry(entry, staleAt) |
| | } else if (entry.status === EntryStatus.Fulfilled) { |
| | fulfilledEntries.push(entry) |
| | } |
| | } |
| | return fulfilledEntries |
| | } |
| |
|
| | function writeDynamicRenderResponseIntoCache( |
| | now: number, |
| | task: PrefetchTask, |
| | fetchStrategy: |
| | | FetchStrategy.LoadingBoundary |
| | | FetchStrategy.PPRRuntime |
| | | FetchStrategy.Full, |
| | response: RSCResponse<NavigationFlightResponse>, |
| | serverData: NavigationFlightResponse, |
| | isResponsePartial: boolean, |
| | route: FulfilledRouteCacheEntry, |
| | spawnedEntries: Map<SegmentRequestKey, PendingSegmentCacheEntry> | null |
| | ): Array<FulfilledSegmentCacheEntry> | null { |
| | if (serverData.b !== getAppBuildId()) { |
| | |
| | |
| | |
| | |
| | |
| | if (spawnedEntries !== null) { |
| | rejectSegmentEntriesIfStillPending(spawnedEntries, now + 10 * 1000) |
| | } |
| | return null |
| | } |
| |
|
| | const flightDatas = normalizeFlightData(serverData.f) |
| | if (typeof flightDatas === 'string') { |
| | |
| | |
| | return null |
| | } |
| |
|
| | |
| | |
| | const staleTimeSeconds = |
| | typeof serverData.rp?.[1] === 'number' |
| | ? serverData.rp[1] |
| | : parseInt(response.headers.get(NEXT_ROUTER_STALE_TIME_HEADER) ?? '', 10) |
| | const staleTimeMs = !isNaN(staleTimeSeconds) |
| | ? getStaleTimeMs(staleTimeSeconds) |
| | : STATIC_STALETIME_MS |
| | const staleAt = now + staleTimeMs |
| |
|
| | for (const flightData of flightDatas) { |
| | const seedData = flightData.seedData |
| | if (seedData !== null) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const segmentPath = flightData.segmentPath |
| | let tree = route.tree |
| | for (let i = 0; i < segmentPath.length; i += 2) { |
| | const parallelRouteKey: string = segmentPath[i] |
| | if (tree?.slots?.[parallelRouteKey] !== undefined) { |
| | tree = tree.slots[parallelRouteKey] |
| | } else { |
| | if (spawnedEntries !== null) { |
| | rejectSegmentEntriesIfStillPending(spawnedEntries, now + 10 * 1000) |
| | } |
| | return null |
| | } |
| | } |
| |
|
| | writeSeedDataIntoCache( |
| | now, |
| | task, |
| | fetchStrategy, |
| | route, |
| | tree, |
| | staleAt, |
| | seedData, |
| | isResponsePartial, |
| | spawnedEntries |
| | ) |
| | } |
| |
|
| | const head = flightData.head |
| | if (head !== null) { |
| | fulfillEntrySpawnedByRuntimePrefetch( |
| | now, |
| | fetchStrategy, |
| | route, |
| | head, |
| | null, |
| | flightData.isHeadPartial, |
| | staleAt, |
| | route.metadata, |
| | spawnedEntries |
| | ) |
| | } |
| | } |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if (spawnedEntries !== null) { |
| | const fulfilledEntries = rejectSegmentEntriesIfStillPending( |
| | spawnedEntries, |
| | now + 10 * 1000 |
| | ) |
| | return fulfilledEntries |
| | } |
| | return null |
| | } |
| |
|
| | function writeSeedDataIntoCache( |
| | now: number, |
| | task: PrefetchTask, |
| | fetchStrategy: |
| | | FetchStrategy.LoadingBoundary |
| | | FetchStrategy.PPRRuntime |
| | | FetchStrategy.Full, |
| | route: FulfilledRouteCacheEntry, |
| | tree: RouteTree, |
| | staleAt: number, |
| | seedData: CacheNodeSeedData, |
| | isResponsePartial: boolean, |
| | entriesOwnedByCurrentTask: Map< |
| | SegmentRequestKey, |
| | PendingSegmentCacheEntry |
| | > | null |
| | ) { |
| | |
| | |
| | const rsc = seedData[0] |
| | const loading = seedData[2] |
| | const isPartial = rsc === null || isResponsePartial |
| | fulfillEntrySpawnedByRuntimePrefetch( |
| | now, |
| | fetchStrategy, |
| | route, |
| | rsc, |
| | loading, |
| | isPartial, |
| | staleAt, |
| | tree, |
| | entriesOwnedByCurrentTask |
| | ) |
| |
|
| | |
| | const slots = tree.slots |
| | if (slots !== null) { |
| | const seedDataChildren = seedData[1] |
| | for (const parallelRouteKey in slots) { |
| | const childTree = slots[parallelRouteKey] |
| | const childSeedData: CacheNodeSeedData | null | void = |
| | seedDataChildren[parallelRouteKey] |
| | if (childSeedData !== null && childSeedData !== undefined) { |
| | writeSeedDataIntoCache( |
| | now, |
| | task, |
| | fetchStrategy, |
| | route, |
| | childTree, |
| | staleAt, |
| | childSeedData, |
| | isResponsePartial, |
| | entriesOwnedByCurrentTask |
| | ) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | function fulfillEntrySpawnedByRuntimePrefetch( |
| | now: number, |
| | fetchStrategy: |
| | | FetchStrategy.LoadingBoundary |
| | | FetchStrategy.PPRRuntime |
| | | FetchStrategy.Full, |
| | route: FulfilledRouteCacheEntry, |
| | rsc: React.ReactNode, |
| | loading: LoadingModuleData | Promise<LoadingModuleData>, |
| | isPartial: boolean, |
| | staleAt: number, |
| | tree: RouteTree, |
| | entriesOwnedByCurrentTask: Map< |
| | SegmentRequestKey, |
| | PendingSegmentCacheEntry |
| | > | null |
| | ) { |
| | |
| | |
| | |
| | const ownedEntry = |
| | entriesOwnedByCurrentTask !== null |
| | ? entriesOwnedByCurrentTask.get(tree.requestKey) |
| | : undefined |
| | if (ownedEntry !== undefined) { |
| | fulfillSegmentCacheEntry(ownedEntry, rsc, loading, staleAt, isPartial) |
| | } else { |
| | |
| | const possiblyNewEntry = readOrCreateSegmentCacheEntry( |
| | now, |
| | fetchStrategy, |
| | route, |
| | tree |
| | ) |
| | if (possiblyNewEntry.status === EntryStatus.Empty) { |
| | |
| | const newEntry = possiblyNewEntry |
| | fulfillSegmentCacheEntry( |
| | upgradeToPendingSegment(newEntry, fetchStrategy), |
| | rsc, |
| | loading, |
| | staleAt, |
| | isPartial |
| | ) |
| | } else { |
| | |
| | |
| | const newEntry = fulfillSegmentCacheEntry( |
| | upgradeToPendingSegment( |
| | createDetachedSegmentCacheEntry(staleAt), |
| | fetchStrategy |
| | ), |
| | rsc, |
| | loading, |
| | staleAt, |
| | isPartial |
| | ) |
| | upsertSegmentEntry( |
| | now, |
| | getSegmentVaryPathForRequest(fetchStrategy, tree), |
| | newEntry |
| | ) |
| | } |
| | } |
| | } |
| |
|
| | async function fetchPrefetchResponse<T>( |
| | url: URL, |
| | headers: RequestHeaders |
| | ): Promise<RSCResponse<T> | null> { |
| | const fetchPriority = 'low' |
| | |
| | |
| | |
| | |
| | const shouldImmediatelyDecode = false |
| | const response = await createFetch<T>( |
| | url, |
| | headers, |
| | fetchPriority, |
| | shouldImmediatelyDecode |
| | ) |
| | if (!response.ok) { |
| | return null |
| | } |
| |
|
| | |
| | if (isOutputExportMode) { |
| | |
| | |
| | |
| | |
| | } else { |
| | const contentType = response.headers.get('content-type') |
| | const isFlightResponse = |
| | contentType && contentType.startsWith(RSC_CONTENT_TYPE_HEADER) |
| | if (!isFlightResponse) { |
| | return null |
| | } |
| | } |
| | return response |
| | } |
| |
|
| | function createPrefetchResponseStream( |
| | originalFlightStream: ReadableStream<Uint8Array>, |
| | onStreamClose: () => void, |
| | onResponseSizeUpdate: (size: number) => void |
| | ): ReadableStream<Uint8Array> { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | let totalByteLength = 0 |
| | const reader = originalFlightStream.getReader() |
| | return new ReadableStream({ |
| | async pull(controller) { |
| | while (true) { |
| | const { done, value } = await reader.read() |
| | if (!done) { |
| | |
| | |
| | controller.enqueue(value) |
| |
|
| | |
| | |
| | |
| | |
| | totalByteLength += value.byteLength |
| | onResponseSizeUpdate(totalByteLength) |
| | continue |
| | } |
| | |
| | |
| | onStreamClose() |
| | return |
| | } |
| | }, |
| | }) |
| | } |
| |
|
| | function addSegmentPathToUrlInOutputExportMode( |
| | url: URL, |
| | segmentPath: SegmentRequestKey |
| | ): URL { |
| | if (isOutputExportMode) { |
| | |
| | |
| | const staticUrl = new URL(url) |
| | const routeDir = staticUrl.pathname.endsWith('/') |
| | ? staticUrl.pathname.slice(0, -1) |
| | : staticUrl.pathname |
| | const staticExportFilename = |
| | convertSegmentPathToStaticExportFilename(segmentPath) |
| | staticUrl.pathname = `${routeDir}/${staticExportFilename}` |
| | return staticUrl |
| | } |
| | return url |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function canNewFetchStrategyProvideMoreContent( |
| | currentStrategy: FetchStrategy, |
| | newStrategy: FetchStrategy |
| | ): boolean { |
| | return currentStrategy < newStrategy |
| | } |
| |
|