next.js/packages/next/src/server/app-render/postponed-state.ts
postponed-state.ts220 lines6.4 KB
import type {
  OpaqueFallbackRouteParamEntries,
  OpaqueFallbackRouteParams,
} from '../../server/request/fallback-params'
import { getDynamicParam } from '../../shared/lib/router/utils/get-dynamic-param'
import type { Params } from '../request/params'
import {
  createPrerenderResumeDataCache,
  createRenderResumeDataCache,
  type PrerenderResumeDataCache,
  type RenderResumeDataCache,
} from '../resume-data-cache/resume-data-cache'
import { stringifyResumeDataCache } from '../resume-data-cache/resume-data-cache'

export enum DynamicState {
  /**
   * The dynamic access occurred during the RSC render phase.
   */
  DATA = 1,

  /**
   * The dynamic access occurred during the HTML shell render phase.
   */
  HTML = 2,
}

/**
 * The postponed state for dynamic data.
 */
export type DynamicDataPostponedState = {
  /**
   * The type of dynamic state.
   */
  readonly type: DynamicState.DATA

  /**
   * The immutable resume data cache.
   */
  readonly renderResumeDataCache: RenderResumeDataCache
}

/**
 * The postponed state for dynamic HTML.
 */
export type DynamicHTMLPostponedState = {
  /**
   * The type of dynamic state.
   */
  readonly type: DynamicState.HTML

  /**
   * The postponed data used by React.
   */
  readonly data: [
    preludeState: DynamicHTMLPreludeState,
    postponed: ReactPostponed,
  ]

  /**
   * The immutable resume data cache.
   */
  readonly renderResumeDataCache: RenderResumeDataCache
}

export const enum DynamicHTMLPreludeState {
  Empty = 0,
  Full = 1,
}

type ReactPostponed = NonNullable<
  import('react-dom/static').PrerenderResult['postponed']
>

export type PostponedState =
  | DynamicDataPostponedState
  | DynamicHTMLPostponedState

export async function getDynamicHTMLPostponedState(
  postponed: ReactPostponed,
  preludeState: DynamicHTMLPreludeState,
  fallbackRouteParams: OpaqueFallbackRouteParams | null,
  resumeDataCache: PrerenderResumeDataCache | RenderResumeDataCache,
  isCacheComponentsEnabled: boolean
): Promise<string> {
  const data: DynamicHTMLPostponedState['data'] = [preludeState, postponed]
  const dataString = JSON.stringify(data)

  // If there are no fallback route params, we can just serialize the postponed
  // state as is.
  if (!fallbackRouteParams || fallbackRouteParams.size === 0) {
    // Serialized as `<postponedString.length>:<postponedString><renderResumeDataCache>`
    return `${dataString.length}:${dataString}${await stringifyResumeDataCache(
      createRenderResumeDataCache(resumeDataCache),
      isCacheComponentsEnabled
    )}`
  }

  const replacements: OpaqueFallbackRouteParamEntries = Array.from(
    fallbackRouteParams.entries()
  )
  const replacementsString = JSON.stringify(replacements)

  // Serialized as `<replacements.length><replacements><data>`
  const postponedString = `${replacementsString.length}${replacementsString}${dataString}`

  // Serialized as `<postponedString.length>:<postponedString><renderResumeDataCache>`
  return `${postponedString.length}:${postponedString}${await stringifyResumeDataCache(resumeDataCache, isCacheComponentsEnabled)}`
}

export async function getDynamicDataPostponedState(
  resumeDataCache: PrerenderResumeDataCache | RenderResumeDataCache,
  isCacheComponentsEnabled: boolean
): Promise<string> {
  return `4:null${await stringifyResumeDataCache(createRenderResumeDataCache(resumeDataCache), isCacheComponentsEnabled)}`
}

export function parsePostponedState(
  state: string,
  interpolatedParams: Params,
  maxPostponedStateSizeBytes: number | undefined
): PostponedState {
  try {
    const postponedStringLengthMatch = state.match(/^([0-9]*):/)?.[1]
    if (!postponedStringLengthMatch) {
      throw new Error(`Invariant: invalid postponed state ${state}`)
    }

    const postponedStringLength = parseInt(postponedStringLengthMatch)

    // We add a `:` to the end of the length as the first character of the
    // postponed string is the length of the replacement entries.
    const postponedString = state.slice(
      postponedStringLengthMatch.length + 1,
      postponedStringLengthMatch.length + postponedStringLength + 1
    )

    const renderResumeDataCache = createRenderResumeDataCache(
      state.slice(
        postponedStringLengthMatch.length + postponedStringLength + 1
      ),
      maxPostponedStateSizeBytes
    )

    try {
      if (postponedString === 'null') {
        return { type: DynamicState.DATA, renderResumeDataCache }
      }

      if (/^[0-9]/.test(postponedString)) {
        const match = postponedString.match(/^([0-9]*)/)?.[1]
        if (!match) {
          throw new Error(
            `Invariant: invalid postponed state ${JSON.stringify(postponedString)}`
          )
        }

        // This is the length of the replacements entries.
        const length = parseInt(match)
        const replacements = JSON.parse(
          postponedString.slice(
            match.length,
            // We then go to the end of the string.
            match.length + length
          )
        ) as OpaqueFallbackRouteParamEntries

        let postponed = postponedString.slice(match.length + length)
        for (const [
          segmentKey,
          [searchValue, dynamicParamType],
        ] of replacements) {
          const {
            treeSegment: [
              ,
              // This is the same value that'll be used in the postponed state
              // as it's part of the tree data. That's why we use it as the
              // replacement value.
              value,
            ],
          } = getDynamicParam(
            interpolatedParams,
            segmentKey,
            dynamicParamType,
            null,
            null // staticSiblings not needed for postponed state
          )

          postponed = postponed.replaceAll(searchValue, value)
        }

        return {
          type: DynamicState.HTML,
          data: JSON.parse(postponed),
          renderResumeDataCache,
        }
      }

      return {
        type: DynamicState.HTML,
        data: JSON.parse(postponedString),
        renderResumeDataCache,
      }
    } catch (err) {
      console.error('Failed to parse postponed state', err)
      return { type: DynamicState.DATA, renderResumeDataCache }
    }
  } catch (err) {
    console.error('Failed to parse postponed state', err)
    return {
      type: DynamicState.DATA,
      renderResumeDataCache: createPrerenderResumeDataCache(),
    }
  }
}

export function getPostponedFromState(state: DynamicHTMLPostponedState) {
  const [preludeState, postponed] = state.data
  return { preludeState, postponed }
}
Quest for Codev2.0.0
/
SIGN IN