| 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 |
| } |
|
|