next.js/packages/next/src/client/components/router-reducer/create-initial-router-state.ts
create-initial-router-state.ts262 lines9.8 KB
import type { InitialRSCPayload } from '../../../shared/lib/app-router-types'

import { createHrefFromUrl } from './create-href-from-url'
import { extractPathFromFlightRouterState } from './compute-changed-path'

import type { AppRouterState } from './router-reducer-types'
import { getFlightDataPartsFromPath } from '../../flight-data-helpers'
import { createInitialCacheNodeForHydration } from './ppr-navigations'
import {
  convertRootFlightRouterStateToRouteTree,
  getStaleAt,
  processRuntimePrefetchStream,
  writeDynamicRenderResponseIntoCache,
  writeStaticStageResponseIntoCache,
} from '../segment-cache/cache'
import { FetchStrategy } from '../segment-cache/types'
import {
  UnknownDynamicStaleTime,
  computeDynamicStaleAt,
} from '../segment-cache/bfcache'
import { decodeStaticStage } from './fetch-server-response'
import { discoverKnownRoute } from '../segment-cache/optimistic-routes'
import type { NormalizedSearch } from '../segment-cache/cache-key'

export interface InitialRouterStateParameters {
  navigatedAt: number
  initialRSCPayload: InitialRSCPayload
  initialFlightStreamForCache?: ReadableStream<Uint8Array> | null
  location: Location | null
}

export function createInitialRouterState({
  navigatedAt,
  initialRSCPayload,
  initialFlightStreamForCache,
  location,
}: InitialRouterStateParameters): AppRouterState {
  const {
    c: initialCanonicalUrlParts,
    f: initialFlightData,
    q: initialRenderedSearch,
    i: initialCouldBeIntercepted,
    S: initialSupportsPerSegmentPrefetching,
    s: initialStaleTime,
    l: initialStaticStageByteLength,
    h: initialHeadVaryParams,
    p: initialRuntimePrefetchStream,
    d: initialDynamicStaleTimeSeconds,
  } = initialRSCPayload

  // When initialized on the server, the canonical URL is provided as an array of parts.
  // 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,
  } = normalizedFlightData
  // 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.)

  const canonicalUrl =
    // location.href is read as the initial value for canonicalUrl in the browser
    // This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates in the useEffect further down in this file.
    location
      ? // window.location does not have the same type as URL but has all the fields createHrefFromUrl needs.
        createHrefFromUrl(location)
      : initialCanonicalUrl

  // Convert the initial FlightRouterState into the RouteTree type.
  // NOTE: The metadataVaryPath isn't used for anything currently because the
  // head is embedded into the CacheNode tree, but eventually we'll lift it out
  // and store it on the top-level state object.
  //
  // For statically-generated-at-build-time HTML pages, the FlightRouterState
  // baked into the initial RSC payload won't have the correct segment inlining
  // hints because those are computed after the pre-render. The server marks
  // these trees with InliningHintsStale, which causes the route cache entry
  // to be immediately expired. The next prefetch will re-fetch the tree with
  // correct hints from the /_tree response.
  const acc = { metadataVaryPath: null }
  const initialRouteTree = convertRootFlightRouterStateToRouteTree(
    initialTree,
    initialRenderedSearch as NormalizedSearch,
    acc
  )
  const metadataVaryPath = acc.metadataVaryPath
  const initialTask = createInitialCacheNodeForHydration(
    navigatedAt,
    initialRouteTree,
    initialSeedData,
    initialHead,
    computeDynamicStaleAt(
      navigatedAt,
      initialDynamicStaleTimeSeconds ?? UnknownDynamicStaleTime
    )
  )

  // The following only applies in the browser (location !== null) since neither
  // route learning nor segment cache state persists from SSR to client.
  if (location !== null && metadataVaryPath !== null) {
    // Learn the route pattern so we can predict it for future navigations.
    discoverKnownRoute(
      Date.now(),
      location.pathname,
      null, // nextUrl — initial render is never an interception
      null, // No pending entry
      initialRouteTree,
      metadataVaryPath,
      initialCouldBeIntercepted,
      canonicalUrl,
      initialSupportsPerSegmentPrefetching,
      false // hasDynamicRewrite
    )

    // Write the initial seed data into the segment cache so subsequent
    // navigations to the initial page can serve cached segments instantly.
    if (initialSeedData !== null && initialStaleTime !== undefined) {
      if (
        initialStaticStageByteLength !== undefined &&
        initialFlightStreamForCache != null
      ) {
        // Partially static page — truncate the cloned Flight stream at the
        // static stage byte boundary, decode, and cache the static subset.
        decodeStaticStage<InitialRSCPayload>(
          initialFlightStreamForCache,
          initialStaticStageByteLength,
          undefined
        )
          .then(async (staticStageResponse) => {
            const now = Date.now()
            const staleAt = await getStaleAt(now, staticStageResponse.s)

            writeStaticStageResponseIntoCache(
              now,
              staticStageResponse.f,
              undefined, // no build ID mismatch check for initial HTML
              staticStageResponse.h,
              staleAt,
              initialTree,
              initialRenderedSearch,
              true // isResponsePartial
            )
          })
          .catch(() => {
            // The static stage processing failed. Not fatal — the page
            // rendered normally, we just won't write into the cache.
          })
      } else {
        // Fully static page — cache the entire decoded seed data as-is. We're
        // not using the initial response here (which would allow us to combine
        // the two branches) to avoid unnecessary decoding of the Flight data,
        // since we can just take the seed data that we already decoded during
        // hydration and write it into the cache directly.
        const now = Date.now()

        getStaleAt(now, initialStaleTime)
          .then((staleAt) => {
            writeStaticStageResponseIntoCache(
              now,
              initialFlightData,
              undefined, // buildId — not applicable for initial HTML
              initialHeadVaryParams,
              staleAt,
              initialTree,
              initialRenderedSearch,
              false // isResponsePartial
            )
          })
          .catch(() => {
            // The static stage processing failed. Not fatal — the page
            // rendered normally, we just won't write into the cache.
          })

        // Cancel the stream clone — fully static path doesn't need it.
        initialFlightStreamForCache?.cancel()
      }
    } else {
      // No caching — cancel the unused stream clone.
      initialFlightStreamForCache?.cancel()
    }

    // If the initial RSC payload includes an embedded runtime prefetch stream,
    // decode it and write the runtime data into the segment cache. This allows
    // subsequent navigations to serve runtime-prefetchable content from cache
    // without a separate prefetch request.
    if (initialRuntimePrefetchStream != null) {
      processRuntimePrefetchStream(
        Date.now(),
        initialRuntimePrefetchStream,
        initialTree,
        initialRenderedSearch
      )
        .then((processed) => {
          if (processed !== null) {
            writeDynamicRenderResponseIntoCache(
              Date.now(),
              FetchStrategy.PPRRuntime,
              processed.flightDatas,
              processed.buildId,
              processed.isResponsePartial,
              processed.headVaryParams,
              processed.staleAt,
              processed.navigationSeed,
              null
            )
          }
        })
        .catch(() => {
          // Runtime prefetch cache write failed. Not fatal — the page rendered
          // normally, we just won't cache runtime data.
        })
    }
  }

  // NOTE: We intentionally don't check if any data needs to be fetched from the
  // server. We assume the initial hydration payload is sufficient to render
  // the page.
  //
  // The completeness of the initial data is an important property that we rely
  // on as a last-ditch mechanism for recovering the app; we must always be able
  // to reload a fresh HTML document to get to a consistent state.
  //
  // In the future, there may be cases where the server intentionally sends
  // partial data and expects the client to fill in the rest, in which case this
  // logic may change. (There already is a similar case where the server sends
  // _no_ hydration data in the HTML document at all, and the client fetches it
  // separately, but that's different because we still end up hydrating with a
  // complete tree.)

  const initialState = {
    tree: initialTask.route,
    cache: initialTask.node,
    pushRef: {
      pendingPush: false,
      mpaNavigation: false,
      // First render needs to preserve the previous window.history.state
      // to avoid it being overwritten on navigation back/forward with MPA Navigation.
      preserveCustomHistoryState: true,
    },
    focusAndScrollRef: {
      scrollRef: null,
      forceScroll: false,
      onlyHashChange: false,
      hashFragment: null,
    },
    canonicalUrl,
    renderedSearch: initialRenderedSearch,
    // the || operator is intentional, the pathname can be an empty string
    nextUrl:
      (extractPathFromFlightRouterState(initialTree) || location?.pathname) ??
      null,
    previousNextUrl: null,
    debugInfo: null,
  }

  return initialState
}
Quest for Codev2.0.0
/
SIGN IN