diff --git a/packages/next/src/client/components/router-reducer/apply-flight-data.ts b/packages/next/src/client/components/router-reducer/apply-flight-data.ts index 51c33de3f19bc..e4fd66204a528 100644 --- a/packages/next/src/client/components/router-reducer/apply-flight-data.ts +++ b/packages/next/src/client/components/router-reducer/apply-flight-data.ts @@ -1,32 +1,24 @@ import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime' -import type { FlightDataPath } from '../../../server/app-render/types' import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head' import { fillCacheWithNewSubTreeData } from './fill-cache-with-new-subtree-data' import type { PrefetchCacheEntry } from './router-reducer-types' -import { - getFlightDataPartsFromPath, - isRootFlightDataPath, -} from '../../flight-data-helpers' +import type { NormalizedFlightData } from '../../flight-data-helpers' export function applyFlightData( existingCache: CacheNode, cache: CacheNode, - flightDataPath: FlightDataPath, + flightData: NormalizedFlightData, prefetchEntry?: PrefetchCacheEntry ): boolean { // The one before last item is the router state tree patch - const { - tree: treePatch, - seedData, - head, - } = getFlightDataPartsFromPath(flightDataPath) + const { tree: treePatch, seedData, head, isRootRender } = flightData // Handles case where prefetch only returns the router tree patch without rendered components. if (seedData === null) { return false } - if (isRootFlightDataPath(flightDataPath)) { + if (isRootRender) { const rsc = seedData[1] const loading = seedData[3] cache.loading = loading @@ -55,12 +47,7 @@ export function applyFlightData( cache.parallelRoutes = new Map(existingCache.parallelRoutes) cache.loading = existingCache.loading // Create a copy of the existing cache with the rsc applied. - fillCacheWithNewSubTreeData( - cache, - existingCache, - flightDataPath, - prefetchEntry - ) + fillCacheWithNewSubTreeData(cache, existingCache, flightData, prefetchEntry) } return true diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts index d6277f66f03af..465349fdffdc2 100644 --- a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts +++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts @@ -32,11 +32,12 @@ export function createInitialRouterState({ // This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it // as a URL that should be crawled. const initialCanonicalUrl = initialCanonicalUrlParts.join('/') + const normalizedFlightData = getFlightDataPartsFromPath(initialFlightData[0]) const { tree: initialTree, seedData: initialSeedData, head: initialHead, - } = getFlightDataPartsFromPath(initialFlightData[0]) + } = normalizedFlightData const isServer = !location // For the SSR render, seed data should always be available (we only send back a `null` response // in the case of a `loading` segment, pre-PPR.) @@ -114,7 +115,7 @@ export function createInitialRouterState({ createPrefetchCacheEntryForInitialLoad({ url, data: { - flightData: initialFlightData, + flightData: [normalizedFlightData], canonicalUrl: undefined, couldBeIntercepted: !!couldBeIntercepted, // TODO: the server should probably send a value for this. Default to false for now. diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 9d3594082df58..3e0034ff935fc 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -14,7 +14,6 @@ const { createFromFetch } = ( import type { FlightRouterState, NavigationFlightResponse, - FetchServerResponseResult, } from '../../../server/app-render/types' import { NEXT_ROUTER_PREFETCH_HEADER, @@ -30,6 +29,10 @@ import { import { callServer } from '../../app-call-server' import { PrefetchKind } from './router-reducer-types' import { hexHash } from '../../../shared/lib/hash' +import { + normalizeFlightData, + type NormalizedFlightData, +} from '../../flight-data-helpers' export interface FetchServerResponseOptions { readonly flightRouterState: FlightRouterState @@ -39,6 +42,14 @@ export interface FetchServerResponseOptions { readonly isHmrRefresh?: boolean } +export type FetchServerResponseResult = { + flightData: NormalizedFlightData[] | string + canonicalUrl: URL | undefined + couldBeIntercepted: boolean + isPrerender: boolean + postponed: boolean +} + function urlToUrlWithoutFlightMarker(url: string): URL { const urlWithoutFlightParameters = new URL(url, location.origin) urlWithoutFlightParameters.searchParams.delete(NEXT_RSC_UNION_QUERY) @@ -209,7 +220,7 @@ export async function fetchServerResponse( } return { - flightData: response.f, + flightData: normalizeFlightData(response.f), canonicalUrl: canonicalUrl, couldBeIntercepted: interception, isPrerender: isPrerender, diff --git a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx index 570cdcf00ba7e..26e4bcf54cb25 100644 --- a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx +++ b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.test.tsx @@ -1,26 +1,19 @@ import React from 'react' import { fillCacheWithNewSubTreeData } from './fill-cache-with-new-subtree-data' import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime' -import type { FlightData } from '../../../server/app-render/types' +import type { NormalizedFlightData } from '../../flight-data-helpers' -const getFlightData = (): FlightData => { +const getFlightData = (): NormalizedFlightData[] => { return [ - [ - 'children', - 'linking', - 'children', - 'about', - [ - 'about', - { - children: ['', {}], - }, - ], - ['about', {},

SubTreeData Injected!

], - <> - Head Injected! - , - ], + { + pathToSegment: ['children', 'linking', 'children'], + segmentPath: ['children', 'linking', 'children', 'about'], + segment: 'about', + tree: ['about', { children: ['', {}] }], + seedData: ['about',

SubTreeData Injected!

, {}, null], + head: 'Head Injected!', + isRootRender: false, + }, ] } @@ -88,9 +81,9 @@ describe('fillCacheWithNewSubtreeData', () => { } // Mirrors the way router-reducer values are passed in. - const flightDataPath = flightData[0] + const normalizedFlightData = flightData[0] - fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath) + fillCacheWithNewSubTreeData(cache, existingCache, normalizedFlightData) const expectedCache: CacheNode = { lazyData: null, diff --git a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts index b107a92003f2f..79b67fb0c8257 100644 --- a/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts +++ b/packages/next/src/client/components/router-reducer/fill-cache-with-new-subtree-data.ts @@ -1,15 +1,11 @@ import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime' -import type { FlightDataPath } from '../../../server/app-render/types' +import type { Segment } from '../../../server/app-render/types' import { invalidateCacheByRouterState } from './invalidate-cache-by-router-state' import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head' import { createRouterCacheKey } from './create-router-cache-key' import type { PrefetchCacheEntry } from './router-reducer-types' import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment' -import { - getFlightDataPartsFromPath, - getNextFlightSegmentPath, - isLastFlightDataPathEntry, -} from '../../flight-data-helpers' +import type { NormalizedFlightData } from '../../flight-data-helpers' /** * Common logic for filling cache with new sub tree data. @@ -17,113 +13,119 @@ import { function fillCacheHelper( newCache: CacheNode, existingCache: CacheNode, - flightDataPath: FlightDataPath, + flightData: NormalizedFlightData, prefetchEntry: PrefetchCacheEntry | undefined, fillLazyItems: boolean ): void { - const isLastEntry = isLastFlightDataPathEntry(flightDataPath) - const [parallelRouteKey, segment] = flightDataPath + const { + segmentPath, + seedData: cacheNodeSeedData, + tree: treePatch, + head, + } = flightData + let newCacheNode = newCache + let existingCacheNode = existingCache - const cacheKey = createRouterCacheKey(segment) + for (let i = 0; i < segmentPath.length; i += 2) { + const parallelRouteKey: string = segmentPath[i] + const segment: Segment = segmentPath[i + 1] - const existingChildSegmentMap = - existingCache.parallelRoutes.get(parallelRouteKey) + // segmentPath is a repeating tuple of parallelRouteKey and segment + // we know we've hit the last entry we've reached our final pair + const isLastEntry = i === segmentPath.length - 2 + const cacheKey = createRouterCacheKey(segment) - if (!existingChildSegmentMap) { - // Bailout because the existing cache does not have the path to the leaf node - // Will trigger lazy fetch in layout-router because of missing segment - return - } + const existingChildSegmentMap = + existingCacheNode.parallelRoutes.get(parallelRouteKey) - let childSegmentMap = newCache.parallelRoutes.get(parallelRouteKey) - if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) { - childSegmentMap = new Map(existingChildSegmentMap) - newCache.parallelRoutes.set(parallelRouteKey, childSegmentMap) - } + if (!existingChildSegmentMap) { + // Bailout because the existing cache does not have the path to the leaf node + // Will trigger lazy fetch in layout-router because of missing segment + continue + } - const existingChildCacheNode = existingChildSegmentMap.get(cacheKey) - let childCacheNode = childSegmentMap.get(cacheKey) - const { - seedData: cacheNodeSeedData, - tree: treePatch, - head, - } = getFlightDataPartsFromPath(flightDataPath) - - if (isLastEntry) { - if ( - cacheNodeSeedData && - (!childCacheNode || - !childCacheNode.lazyData || - childCacheNode === existingChildCacheNode) - ) { - const incomingSegment = cacheNodeSeedData[0] - const rsc = cacheNodeSeedData[1] - const loading = cacheNodeSeedData[3] + let childSegmentMap = newCacheNode.parallelRoutes.get(parallelRouteKey) + if (!childSegmentMap || childSegmentMap === existingChildSegmentMap) { + childSegmentMap = new Map(existingChildSegmentMap) + newCacheNode.parallelRoutes.set(parallelRouteKey, childSegmentMap) + } - childCacheNode = { - lazyData: null, - // When `fillLazyItems` is false, we only want to fill the RSC data for the layout, - // not the page segment. - rsc: fillLazyItems || incomingSegment !== PAGE_SEGMENT_KEY ? rsc : null, - prefetchRsc: null, - head: null, - prefetchHead: null, - loading, - parallelRoutes: - fillLazyItems && existingChildCacheNode - ? new Map(existingChildCacheNode.parallelRoutes) - : new Map(), - } + const existingChildCacheNode = existingChildSegmentMap.get(cacheKey) + let childCacheNode = childSegmentMap.get(cacheKey) - if (existingChildCacheNode && fillLazyItems) { - invalidateCacheByRouterState( - childCacheNode, - existingChildCacheNode, - treePatch - ) - } - if (fillLazyItems) { - fillLazyItemsTillLeafWithHead( - childCacheNode, - existingChildCacheNode, - treePatch, - cacheNodeSeedData, - head, - prefetchEntry - ) + if (isLastEntry) { + if ( + cacheNodeSeedData && + (!childCacheNode || + !childCacheNode.lazyData || + childCacheNode === existingChildCacheNode) + ) { + const incomingSegment = cacheNodeSeedData[0] + const rsc = cacheNodeSeedData[1] + const loading = cacheNodeSeedData[3] + + childCacheNode = { + lazyData: null, + // When `fillLazyItems` is false, we only want to fill the RSC data for the layout, + // not the page segment. + rsc: + fillLazyItems || incomingSegment !== PAGE_SEGMENT_KEY ? rsc : null, + prefetchRsc: null, + head: null, + prefetchHead: null, + loading, + parallelRoutes: + fillLazyItems && existingChildCacheNode + ? new Map(existingChildCacheNode.parallelRoutes) + : new Map(), + } + + if (existingChildCacheNode && fillLazyItems) { + invalidateCacheByRouterState( + childCacheNode, + existingChildCacheNode, + treePatch + ) + } + if (fillLazyItems) { + fillLazyItemsTillLeafWithHead( + childCacheNode, + existingChildCacheNode, + treePatch, + cacheNodeSeedData, + head, + prefetchEntry + ) + } + + childSegmentMap.set(cacheKey, childCacheNode) } + continue + } - childSegmentMap.set(cacheKey, childCacheNode) + if (!childCacheNode || !existingChildCacheNode) { + // Bailout because the existing cache does not have the path to the leaf node + // Will trigger lazy fetch in layout-router because of missing segment + continue } - return - } - if (!childCacheNode || !existingChildCacheNode) { - // Bailout because the existing cache does not have the path to the leaf node - // Will trigger lazy fetch in layout-router because of missing segment - return - } + if (childCacheNode === existingChildCacheNode) { + childCacheNode = { + lazyData: childCacheNode.lazyData, + rsc: childCacheNode.rsc, + prefetchRsc: childCacheNode.prefetchRsc, + head: childCacheNode.head, + prefetchHead: childCacheNode.prefetchHead, + parallelRoutes: new Map(childCacheNode.parallelRoutes), + loading: childCacheNode.loading, + } as CacheNode + childSegmentMap.set(cacheKey, childCacheNode) + } - if (childCacheNode === existingChildCacheNode) { - childCacheNode = { - lazyData: childCacheNode.lazyData, - rsc: childCacheNode.rsc, - prefetchRsc: childCacheNode.prefetchRsc, - head: childCacheNode.head, - prefetchHead: childCacheNode.prefetchHead, - parallelRoutes: new Map(childCacheNode.parallelRoutes), - loading: childCacheNode.loading, - } as CacheNode - childSegmentMap.set(cacheKey, childCacheNode) + // Move deeper into the cache nodes + newCacheNode = childCacheNode + existingCacheNode = existingChildCacheNode } - - fillCacheHelper( - childCacheNode, - existingChildCacheNode, - getNextFlightSegmentPath(flightDataPath), - prefetchEntry, - fillLazyItems - ) } /** @@ -132,17 +134,17 @@ function fillCacheHelper( export function fillCacheWithNewSubTreeData( newCache: CacheNode, existingCache: CacheNode, - flightDataPath: FlightDataPath, + flightData: NormalizedFlightData, prefetchEntry?: PrefetchCacheEntry ): void { - fillCacheHelper(newCache, existingCache, flightDataPath, prefetchEntry, true) + fillCacheHelper(newCache, existingCache, flightData, prefetchEntry, true) } export function fillCacheWithNewSubTreeDataButOnlyLoading( newCache: CacheNode, existingCache: CacheNode, - flightDataPath: FlightDataPath, + flightData: NormalizedFlightData, prefetchEntry?: PrefetchCacheEntry ): void { - fillCacheHelper(newCache, existingCache, flightDataPath, prefetchEntry, false) + fillCacheHelper(newCache, existingCache, flightData, prefetchEntry, false) } diff --git a/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx b/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx index 733cd38d86358..f55e0d28746eb 100644 --- a/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx +++ b/packages/next/src/client/components/router-reducer/invalidate-cache-below-flight-segmentpath.test.tsx @@ -1,27 +1,20 @@ import React from 'react' -import type { FlightData } from '../../../server/app-render/types' import { invalidateCacheBelowFlightSegmentPath } from './invalidate-cache-below-flight-segmentpath' import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime' import { fillCacheWithNewSubTreeData } from './fill-cache-with-new-subtree-data' +import type { NormalizedFlightData } from '../../flight-data-helpers' -const getFlightData = (): FlightData => { +const getFlightData = (): NormalizedFlightData[] => { return [ - [ - 'children', - 'linking', - 'children', - 'about', - [ - 'about', - { - children: ['', {}], - }, - ], - ['about', {},

About Page!

], - <> - About page! - , - ], + { + pathToSegment: ['children', 'linking', 'children'], + segmentPath: ['children', 'linking', 'children', 'about'], + segment: 'about', + tree: ['about', { children: ['', {}] }], + seedData: ['about',

About Page!

, {}, null], + head: 'About page!', + isRootRender: false, + }, ] } @@ -89,20 +82,19 @@ describe('invalidateCacheBelowFlightSegmentPath', () => { } // Mirrors the way router-reducer values are passed in. - const flightDataPath = flightData[0] - const flightSegmentPath = flightDataPath.slice(0, -3) + const normalizedFlightData = flightData[0] // Copy rsc for the root node of the cache. cache.rsc = existingCache.rsc cache.prefetchRsc = existingCache.prefetchRsc // Create a copy of the existing cache with the rsc applied. - fillCacheWithNewSubTreeData(cache, existingCache, flightDataPath) + fillCacheWithNewSubTreeData(cache, existingCache, normalizedFlightData) // Invalidate the cache below the flight segment path. This should remove the 'about' node. invalidateCacheBelowFlightSegmentPath( cache, existingCache, - flightSegmentPath + normalizedFlightData.segmentPath ) const expectedCache: CacheNode = { diff --git a/packages/next/src/client/components/router-reducer/ppr-navigations.ts b/packages/next/src/client/components/router-reducer/ppr-navigations.ts index 770db0425a355..821bcd3e31764 100644 --- a/packages/next/src/client/components/router-reducer/ppr-navigations.ts +++ b/packages/next/src/client/components/router-reducer/ppr-navigations.ts @@ -3,7 +3,6 @@ import type { FlightRouterState, FlightSegmentPath, Segment, - FetchServerResponseResult, } from '../../../server/app-render/types' import type { CacheNode, @@ -14,9 +13,9 @@ import { DEFAULT_SEGMENT_KEY, PAGE_SEGMENT_KEY, } from '../../../shared/lib/segment' -import { getFlightDataPartsFromPath } from '../../flight-data-helpers' import { matchSegment } from '../match-segments' import { createRouterCacheKey } from './create-router-cache-key' +import type { FetchServerResponseResult } from './fetch-server-response' // This is yet another tree type that is used to track pending promises that // need to be fulfilled once the dynamic data is received. The terminal nodes of @@ -355,8 +354,8 @@ export function listenForDynamicRequest( ) { responsePromise.then( ({ flightData }: FetchServerResponseResult) => { - for (const flightDataPath of flightData) { - if (typeof flightDataPath === 'string') { + for (const normalizedFlightData of flightData) { + if (typeof normalizedFlightData === 'string') { // Happens when navigating to page in `pages` from `app`. We shouldn't // get here because should have already handled this during // the prefetch. @@ -368,7 +367,7 @@ export function listenForDynamicRequest( tree: serverRouterState, seedData: dynamicData, head: dynamicHead, - } = getFlightDataPartsFromPath(flightDataPath) + } = normalizedFlightData if (!dynamicData) { // This shouldn't happen. PPR should always send back a response. diff --git a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts index 113e011e056ca..6a9eedde895df 100644 --- a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts +++ b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts @@ -1,4 +1,7 @@ -import { fetchServerResponse } from './fetch-server-response' +import { + fetchServerResponse, + type FetchServerResponseResult, +} from './fetch-server-response' import { PrefetchCacheEntryStatus, type PrefetchCacheEntry, @@ -6,7 +9,6 @@ import { type ReadonlyReducerState, } from './router-reducer-types' import { prefetchQueue } from './reducers/prefetch-reducer' -import type { FetchServerResponseResult } from '../../../server/app-render/types' const INTERCEPTION_CACHE_KEY_MARKER = '%' diff --git a/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts index 17eaa3b7abff0..b5474794bf1ce 100644 --- a/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts @@ -15,10 +15,6 @@ import type { CacheNode } from '../../../../shared/lib/app-router-context.shared import { createEmptyCacheNode } from '../../app-router' import { handleSegmentMismatch } from '../handle-segment-mismatch' import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree' -import { - getFlightDataPartsFromPath, - isRootFlightDataPath, -} from '../../../flight-data-helpers' // A version of refresh reducer that keeps the cache around instead of wiping all of it. function hmrRefreshReducerImpl( @@ -63,15 +59,14 @@ function hmrRefreshReducerImpl( let currentTree = state.tree let currentCache = state.cache - for (const flightDataPath of flightData) { - if (!isRootFlightDataPath(flightDataPath)) { + for (const normalizedFlightData of flightData) { + const { tree: treePatch, isRootRender } = normalizedFlightData + if (!isRootRender) { // TODO-APP: handle this case better console.log('REFRESH FAILED') return state } - const { tree: treePatch } = getFlightDataPartsFromPath(flightDataPath) - const newTree = applyRouterStatePatchToTree( // TODO-APP: remove '' [''], @@ -100,7 +95,11 @@ function hmrRefreshReducerImpl( if (canonicalUrlOverride) { mutable.canonicalUrl = canonicalUrlOverrideHref } - const applied = applyFlightData(currentCache, cache, flightDataPath) + const applied = applyFlightData( + currentCache, + cache, + normalizedFlightData + ) if (applied) { mutable.cache = cache diff --git a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts index 73e6767f82c2d..a0295f5178483 100644 --- a/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts @@ -31,10 +31,6 @@ import { } from '../prefetch-cache-utils' import { clearCacheNodeDataForSegmentPath } from '../clear-cache-node-data-for-segment-path' import { fillCacheWithNewSubTreeDataButOnlyLoading } from '../fill-cache-with-new-subtree-data' -import { - getFlightDataPartsFromPath, - isRootFlightDataPath, -} from '../../../flight-data-helpers' export function handleExternalUrl( state: ReadonlyReducerState, @@ -171,13 +167,14 @@ export function navigateReducer( let currentTree = state.tree let currentCache = state.cache let scrollableSegments: FlightSegmentPath[] = [] - for (const flightDataPath of flightData) { + for (const normalizedFlightData of flightData) { const { tree: treePatch, pathToSegment: flightSegmentPath, seedData, head, - } = getFlightDataPartsFromPath(flightDataPath) + isRootRender, + } = normalizedFlightData // TODO-APP: remove '' const flightSegmentPathWithLeadingEmpty = ['', ...flightSegmentPath] @@ -217,7 +214,7 @@ export function navigateReducer( // via updateCacheNodeOnNavigation. The current structure is just // an incremental step. seedData && - isRootFlightDataPath(flightDataPath) && + isRootRender && !prefetchValues.aliased && postponed ) { @@ -295,8 +292,7 @@ export function navigateReducer( // The prefetch cache entry was aliased -- this signals that we only fill in the cache with the // loading state and not the actual parallel route seed data. if (prefetchValues.aliased && seedData) { - // Root render - if (isRootFlightDataPath(flightDataPath)) { + if (isRootRender) { // Fill in the cache with the new loading / rsc data const rsc = seedData[1] const loading = seedData[3] @@ -313,7 +309,7 @@ export function navigateReducer( fillCacheWithNewSubTreeDataButOnlyLoading( cache, currentCache, - flightDataPath, + normalizedFlightData, prefetchValues ) } @@ -343,7 +339,7 @@ export function navigateReducer( applied = applyFlightData( currentCache, cache, - flightDataPath, + normalizedFlightData, prefetchValues ) } diff --git a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts index 73c518166d58a..6c8ea5c99bbc8 100644 --- a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts @@ -16,10 +16,6 @@ import { createEmptyCacheNode } from '../../app-router' import { handleSegmentMismatch } from '../handle-segment-mismatch' import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree' import { refreshInactiveParallelSegments } from '../refetch-inactive-parallel-segments' -import { - getFlightDataPartsFromPath, - isRootFlightDataPath, -} from '../../../flight-data-helpers' export function refreshReducer( state: ReadonlyReducerState, @@ -67,18 +63,19 @@ export function refreshReducer( // Remove cache.lazyData as it has been resolved at this point. cache.lazyData = null - for (const flightDataPath of flightData) { - if (!isRootFlightDataPath(flightDataPath)) { - // TODO-APP: handle this case better - console.log('REFRESH FAILED') - return state - } - + for (const normalizedFlightData of flightData) { const { tree: treePatch, seedData: cacheNodeSeedData, head, - } = getFlightDataPartsFromPath(flightDataPath) + isRootRender, + } = normalizedFlightData + + if (!isRootRender) { + // TODO-APP: handle this case better + console.log('REFRESH FAILED') + return state + } const newTree = applyRouterStatePatchToTree( // TODO-APP: remove '' diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts index 8809d083bc798..9b49a5f6ee51f 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts @@ -1,7 +1,6 @@ import type { ActionFlightResponse, ActionResult, - FlightData, } from '../../../../server/app-render/types' import { callServer } from '../../../app-call-server' import { @@ -41,14 +40,14 @@ import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-c import { handleSegmentMismatch } from '../handle-segment-mismatch' import { refreshInactiveParallelSegments } from '../refetch-inactive-parallel-segments' import { - getFlightDataPartsFromPath, - isRootFlightDataPath, + normalizeFlightData, + type NormalizedFlightData, } from '../../../flight-data-helpers' type FetchServerActionResult = { redirectLocation: URL | undefined actionResult?: ActionResult - actionFlightData?: FlightData | undefined | null + actionFlightData?: NormalizedFlightData[] | string revalidatedParts: { tag: boolean cookie: boolean @@ -125,7 +124,7 @@ async function fetchServerAction( if (location) { // if it was a redirection, then result is just a regular RSC payload return { - actionFlightData: response.f, + actionFlightData: normalizeFlightData(response.f), redirectLocation, revalidatedParts, } @@ -133,7 +132,7 @@ async function fetchServerAction( return { actionResult: response.a, - actionFlightData: response.f, + actionFlightData: normalizeFlightData(response.f), redirectLocation, revalidatedParts, } @@ -225,18 +224,19 @@ export function serverActionReducer( mutable.canonicalUrl = newHref } - for (const flightDataPath of flightData) { - if (!isRootFlightDataPath(flightDataPath)) { - // TODO-APP: handle this case better - console.log('SERVER ACTION APPLY FAILED') - return state - } - + for (const normalizedFlightData of flightData) { const { tree: treePatch, seedData: cacheNodeSeedData, head, - } = getFlightDataPartsFromPath(flightDataPath) + isRootRender, + } = normalizedFlightData + + if (!isRootRender) { + // TODO-APP: handle this case better + console.log('SERVER ACTION APPLY FAILED') + return state + } // Given the path can only have two items the items are only the router state and rsc for the root. const newTree = applyRouterStatePatchToTree( diff --git a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts index 866abc5a0e647..9238c748c29f7 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts @@ -13,7 +13,6 @@ import { handleMutable } from '../handle-mutable' import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime' import { createEmptyCacheNode } from '../../app-router' import { handleSegmentMismatch } from '../handle-segment-mismatch' -import { getFlightDataPartsFromPath } from '../../../flight-data-helpers' export function serverPatchReducer( state: ReadonlyReducerState, @@ -40,9 +39,9 @@ export function serverPatchReducer( let currentTree = state.tree let currentCache = state.cache - for (const flightDataPath of flightData) { + for (const normalizedFlightData of flightData) { const { segmentPath: flightSegmentPath, tree: treePatch } = - getFlightDataPartsFromPath(flightDataPath) + normalizedFlightData const newTree = applyRouterStatePatchToTree( // TODO-APP: remove '' @@ -74,7 +73,7 @@ export function serverPatchReducer( } const cache: CacheNode = createEmptyCacheNode() - applyFlightData(currentCache, cache, flightDataPath) + applyFlightData(currentCache, cache, normalizedFlightData) mutable.patchedTree = newTree mutable.cache = cache diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index 909b7afbc3b52..0df6b9276b98e 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -2,8 +2,8 @@ import type { CacheNode } from '../../../shared/lib/app-router-context.shared-ru import type { FlightRouterState, FlightSegmentPath, - FetchServerResponseResult, } from '../../../server/app-render/types' +import type { FetchServerResponseResult } from './fetch-server-response' export const ACTION_REFRESH = 'refresh' export const ACTION_NAVIGATE = 'navigate' diff --git a/packages/next/src/client/flight-data-helpers.ts b/packages/next/src/client/flight-data-helpers.ts index a16a16deee68f..01fa57faff42b 100644 --- a/packages/next/src/client/flight-data-helpers.ts +++ b/packages/next/src/client/flight-data-helpers.ts @@ -1,12 +1,13 @@ import type { CacheNodeSeedData, + FlightData, FlightDataPath, FlightRouterState, FlightSegmentPath, Segment, } from '../server/app-render/types' -export function getFlightDataPartsFromPath(flightDataPath: FlightDataPath): { +export type NormalizedFlightData = { /** * The full `FlightSegmentPath` inclusive of the final `Segment` */ @@ -19,7 +20,16 @@ export function getFlightDataPartsFromPath(flightDataPath: FlightDataPath): { tree: FlightRouterState seedData: CacheNodeSeedData | null head: React.ReactNode | null -} { + isRootRender: boolean +} + +// TODO: We should only have to export `normalizeFlightData`, however because the initial flight data +// that gets passed to `createInitialRouterState` doesn't conform to the `FlightDataPath` type (it's missing the root segment) +// we're currently exporting it so we can use it directly. This should be fixed as part of the unification of +// the different ways we express `FlightSegmentPath`. +export function getFlightDataPartsFromPath( + flightDataPath: FlightDataPath +): NormalizedFlightData { // tree, seedData, and head are *always* the last three items in the `FlightDataPath`. const [tree, seedData, head] = flightDataPath.slice(-3) // The `FlightSegmentPath` is everything except the last three items. For a root render, it won't be present. @@ -37,19 +47,10 @@ export function getFlightDataPartsFromPath(flightDataPath: FlightDataPath): { tree, seedData, head, + isRootRender: flightDataPath.length === 3, } } -export function isRootFlightDataPath(flightDataPath: FlightDataPath): boolean { - return flightDataPath.length === 3 -} - -export function isLastFlightDataPathEntry( - flightDataPath: FlightDataPath -): boolean { - return flightDataPath.length === 5 -} - export function getNextFlightSegmentPath( flightSegmentPath: FlightSegmentPath ): FlightSegmentPath { @@ -57,3 +58,15 @@ export function getNextFlightSegmentPath( // to get the next segment path. return flightSegmentPath.slice(2) } + +export function normalizeFlightData( + flightData: FlightData +): NormalizedFlightData[] | string { + // FlightData can be a string when the server didn't respond with a proper flight response, + // or when a redirect happens, to signal to the client that it needs to perform an MPA navigation. + if (typeof flightData === 'string') { + return flightData + } + + return flightData.map(getFlightDataPartsFromPath) +} diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 343378aec0a38..7db370c906be9 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -377,7 +377,12 @@ async function generateDynamicRSCPayload( ): Promise { // Flight data that is going to be passed to the browser. // Currently a single item array but in the future multiple patches might be combined in a single request. - let flightData: FlightData | null = null + + // We initialize `flightData` to an empty string because the client router knows how to tolerate + // it (treating it as an MPA navigation). The only time this function wouldn't generate flight data + // is for server actions, if the server action handler instructs this function to skip it. When the server + // action reducer sees a falsy value, it'll simply resolve the action with no data. + let flightData: FlightData = '' const { componentMod: { @@ -450,12 +455,7 @@ async function generateDynamicRSCPayload( // Otherwise, it's a regular RSC response. return { b: ctx.renderOpts.buildId, - // Anything besides an action response should have non-null flightData. - // We don't ever expect this to be null because `skipFlight` is only - // used when invoked by a server action, which is covered above. - // The client router can handle an empty string (treating it as an MPA navigation), - // so we'll use that as a fallback. - f: flightData ?? '', + f: flightData, } } @@ -703,7 +703,7 @@ async function getErrorRSCPayload( f: [[initialTree, initialSeedData, initialHead]], G: GlobalError, s: typeof ctx.renderOpts.postponed === 'string', - } satisfies RSCPayload + } satisfies InitialRSCPayload } // This component must run in an SSR context. It will render the RSC root component diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 81f6913eb91ec..c162ff66f52b3 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -240,15 +240,7 @@ export type ActionFlightResponse = { /** buildId */ b: string /** flightData */ - f: FlightData | null -} - -export type FetchServerResponseResult = { - flightData: FlightData - canonicalUrl: URL | undefined - couldBeIntercepted: boolean - isPrerender: boolean - postponed: boolean + f: FlightData } export type RSCPayload = diff --git a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts index b6de5bd741481..38dd9a7ac16ff 100644 --- a/packages/next/src/shared/lib/app-router-context.shared-runtime.ts +++ b/packages/next/src/shared/lib/app-router-context.shared-runtime.ts @@ -1,14 +1,12 @@ 'use client' +import type { FetchServerResponseResult } from '../../client/components/router-reducer/fetch-server-response' import type { FocusAndScrollRef, PrefetchKind, RouterChangeByServerResponse, } from '../../client/components/router-reducer/router-reducer-types' -import type { - FlightRouterState, - FetchServerResponseResult, -} from '../../server/app-render/types' +import type { FlightRouterState } from '../../server/app-render/types' import React from 'react' export type ChildSegmentMap = Map