next.js/packages/next/src/server/app-render/work-unit-async-storage.external.ts
work-unit-async-storage.external.ts647 lines20.9 KB
import type { AsyncLocalStorage } from 'async_hooks'
import type { DraftModeProvider } from '../async-storage/draft-mode-provider'
import type { ResponseCookies } from '../web/spec-extension/cookies'
import type { ReadonlyHeaders } from '../web/spec-extension/adapters/headers'
import type { ReadonlyRequestCookies } from '../web/spec-extension/adapters/request-cookies'
import type { CacheSignal } from './cache-signal'
import type { ResponseVaryParamsAccumulator } from './vary-params'
import type { DynamicTrackingState } from './dynamic-rendering'
import type { OpaqueFallbackRouteParams } from '../request/fallback-params'

// Share the instance module in the next-shared layer
import { workUnitAsyncStorageInstance } from './work-unit-async-storage-instance' with { 'turbopack-transition': 'next-shared' }
import type { ServerComponentsHmrCache } from '../response-cache'
import type {
  RenderResumeDataCache,
  PrerenderResumeDataCache,
} from '../resume-data-cache/resume-data-cache'
import type { Params } from '../request/params'
import type { ImplicitTags } from '../lib/implicit-tags'
import type { WorkStore } from './work-async-storage.external'
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../client/components/app-router-headers'
import { InvariantError } from '../../shared/lib/invariant-error'
import type { StagedRenderingController } from './staged-rendering'
import { RenderStage } from './staged-rendering'
import type { ValidationBoundaryTracking } from './instant-validation/boundary-tracking'
import type { InstantValidationSampleTracking } from './instant-validation/instant-samples'

export type WorkUnitPhase = 'action' | 'render' | 'after'

export interface CommonWorkUnitStore {
  /** NOTE: Will be mutated as phases change */
  phase: WorkUnitPhase
  readonly implicitTags: ImplicitTags
}

export interface RequestStore extends CommonWorkUnitStore {
  readonly type: 'request'

  /**
   * The URL of the request. This only specifies the pathname and the search
   * part of the URL.
   */
  readonly url: {
    /**
     * The pathname of the requested URL.
     */
    readonly pathname: string

    /**
     * The search part of the requested URL. If the request did not provide a
     * search part, this will be an empty string.
     */
    readonly search: string
  }

  readonly headers: ReadonlyHeaders
  // This is mutable because we need to reassign it when transitioning from the action phase to the render phase.
  // The cookie object itself is deliberately read only and thus can't be updated.
  cookies: ReadonlyRequestCookies
  readonly mutableCookies: ResponseCookies
  readonly userspaceMutableCookies: ResponseCookies
  readonly draftMode: DraftModeProvider
  readonly isHmrRefresh?: boolean
  readonly serverComponentsHmrCache?: ServerComponentsHmrCache

  readonly rootParams: Params

  /**
   * The resume data cache for this request. This will be a immutable cache.
   */
  renderResumeDataCache: RenderResumeDataCache | null

  stale?: number
  stagedRendering?: StagedRenderingController | null
  asyncApiPromises?: AsyncApiPromises
  cacheSignal?: CacheSignal | null
  prerenderResumeDataCache?: PrerenderResumeDataCache | null
  fallbackParams?: OpaqueFallbackRouteParams | null
  varyParamsAccumulator?: ResponseVaryParamsAccumulator | null

  // Only in build-time instant-validation
  // We mirror the controller/renderSignal from prerender stores to allow aborting the render
  // in case we hit an error that makes it unnecessary to continue
  controller?: AbortController
  renderSignal?: AbortSignal
  validationSamples?: InstantValidationSamples
  validationSampleTracking?: InstantValidationSampleTracking | null

  // DEV-only
  usedDynamic?: boolean
}

export type InstantValidationSamples = {
  params: Params | undefined
  searchParams: Record<string, string | string[] | null> | undefined
}

export type AsyncApiPromises = {
  cookies: Promise<ReadonlyRequestCookies>
  earlyCookies: Promise<ReadonlyRequestCookies>

  mutableCookies: Promise<ReadonlyRequestCookies>
  earlyMutableCookies: Promise<ReadonlyRequestCookies>

  headers: Promise<ReadonlyHeaders>
  earlyHeaders: Promise<ReadonlyHeaders>

  sharedParamsParent: Promise<string>
  earlySharedParamsParent: Promise<string>

  sharedSearchParamsParent: Promise<string>
  earlySharedSearchParamsParent: Promise<string>

  // Connection is not a runtime promise and doesn't
  // need to distinguish between early and late
  connection: Promise<undefined>

  // IO is not a runtime promise and doesn't
  // need to distinguish between early and late
  io: Promise<undefined>
}

/**
 * Returns true if the current render stage is an early stage (EarlyStatic or
 * EarlyRuntime). The early stages are for runtime-prefetchable segments. When
 * true, runtime APIs should use the early promise variant that resolves at
 * EarlyRuntime rather than Runtime.
 */
export function isInEarlyRenderStage(requestStore: RequestStore): boolean {
  const stagedRendering = requestStore.stagedRendering
  if (stagedRendering) {
    return (
      stagedRendering.currentStage === RenderStage.EarlyStatic ||
      stagedRendering.currentStage === RenderStage.EarlyRuntime
    )
  }
  return false
}

/**
 * The Prerender store is for tracking information related to prerenders.
 *
 * It can be used for both RSC and SSR prerendering and should be scoped as close
 * to the individual `renderTo...` API call as possible. To keep the type simple
 * we don't distinguish between RSC and SSR prerendering explicitly but instead
 * use conditional object properties to infer which mode we are in. For instance cache tracking
 * only needs to happen during the RSC prerender when we are prospectively prerendering
 * to fill all caches.
 */
export type PrerenderStoreModern =
  | PrerenderStoreModernClient
  | PrerenderStoreModernServer
  | PrerenderStoreModernRuntime
  | ValidationStoreClient

/** Like `PrerenderStoreModern`, but only including static prerenders (i.e. not runtime prerenders) */
export type StaticPrerenderStoreModern = Exclude<
  PrerenderStoreModern,
  PrerenderStoreModernRuntime | ValidationStoreClient
>

export interface PrerenderStoreModernClient
  extends PrerenderStoreModernCommon,
    StaticPrerenderStoreCommon {
  readonly type: 'prerender-client'
}

export interface ValidationStoreClient extends PrerenderStoreModernCommon {
  readonly type: 'validation-client'
  readonly boundaryState: ValidationBoundaryTracking | null
  validationSamples: InstantValidationSamples | null
  validationSampleTracking: InstantValidationSampleTracking | null
  fallbackRouteParams: OpaqueFallbackRouteParams | null
}

export interface PrerenderStoreModernServer
  extends PrerenderStoreModernCommon,
    StaticPrerenderStoreCommon {
  readonly type: 'prerender'
}

export interface PrerenderStoreModernRuntime
  extends PrerenderStoreModernCommon {
  readonly type: 'prerender-runtime'

  /**
   * The staged rendering controller for this prerender. Models stage
   * transitions (Before → Static → Runtime → Dynamic). Null for prospective
   * renders where all stages run without sequencing.
   */
  readonly stagedRendering: StagedRenderingController | null

  readonly headers: RequestStore['headers']
  readonly cookies: RequestStore['cookies']
  readonly draftMode: RequestStore['draftMode']
}

export interface RevalidateStore {
  // Collected revalidate times and tags for this document during the prerender.
  revalidate: number // in seconds. 0 means dynamic. INFINITE_CACHE and higher means never revalidate.
  expire: number // server expiration time
  stale: number // client expiration time
  tags: null | string[]
}

interface PrerenderStoreModernCommon
  extends CommonWorkUnitStore,
    RevalidateStore {
  /**
   * The render signal is aborted after React's `prerender` function is aborted
   * (using a separate signal), which happens in two cases:
   *
   * 1. When all caches are filled during the prospective prerender.
   * 2. When the final prerender is aborted immediately after the prerender was
   *    started.
   *
   * It can be used to reject any pending I/O, including hanging promises. This
   * allows React to properly track the async I/O in dev mode, which yields
   * better owner stacks for dynamic validation errors.
   */
  readonly renderSignal: AbortSignal

  /**
   * This is the AbortController which represents the boundary between Prerender
   * and dynamic. In some renders it is the same as the controller for React,
   * but in others it is a separate controller. It should be aborted whenever we
   * are no longer in the prerender phase of rendering. Typically this is after
   * one task, or when you call a sync API which requires the prerender to end
   * immediately.
   */
  readonly controller: AbortController

  /**
   * When not null, this signal is used to track cache reads during prerendering
   * and to await all cache reads completing, before aborting the prerender.
   */
  readonly cacheSignal: null | CacheSignal

  /**
   * During some prerenders we want to track dynamic access.
   */
  readonly dynamicTracking: null | DynamicTrackingState

  readonly rootParams: Params

  /**
   * A mutable resume data cache for this prerender.
   */
  prerenderResumeDataCache: PrerenderResumeDataCache | null

  /**
   * An immutable resume data cache for this prerender. This may be provided
   * instead of the `prerenderResumeDataCache` if the prerender is not supposed
   * to fill caches, and only read from prefilled caches, e.g. when prerendering
   * an optional fallback shell.
   */
  renderResumeDataCache: RenderResumeDataCache | null

  /**
   * The HMR refresh hash is only provided in dev mode. It is needed for the dev
   * warmup render to ensure that the cache keys will be identical for the
   * subsequent dynamic render.
   */
  readonly hmrRefreshHash: string | undefined

  /**
   * A mutable accumulator for per-segment vary params during prerender. Tracks
   * which route params each segment actually accesses, allowing the client
   * cache to re-key entries for better sharing across different param values.
   */
  readonly varyParamsAccumulator: ResponseVaryParamsAccumulator | null
}

interface StaticPrerenderStoreCommon {
  /**
   * The set of unknown route parameters. Accessing these will be tracked as
   * a dynamic access.
   */
  readonly fallbackRouteParams: OpaqueFallbackRouteParams | null

  /**
   * When true, the page is prerendered as a fallback shell, while allowing any
   * dynamic accesses to result in an empty shell. This is the case when there
   * are also routes prerendered with a more complete set of params.
   * Prerendering those routes would catch any invalid dynamic accesses.
   */
  readonly allowEmptyStaticShell: boolean
}

export interface PrerenderStorePPR
  extends CommonWorkUnitStore,
    RevalidateStore {
  readonly type: 'prerender-ppr'
  readonly rootParams: Params
  readonly dynamicTracking: null | DynamicTrackingState

  /**
   * The set of unknown route parameters. Accessing these will be tracked as
   * a dynamic access.
   */
  readonly fallbackRouteParams: OpaqueFallbackRouteParams | null

  /**
   * The resume data cache for this prerender.
   */
  prerenderResumeDataCache: PrerenderResumeDataCache
}

export interface PrerenderStoreLegacy
  extends CommonWorkUnitStore,
    RevalidateStore {
  readonly type: 'prerender-legacy'
  readonly rootParams: Params
}

export type PrerenderStore =
  | PrerenderStoreLegacy
  | PrerenderStorePPR
  | PrerenderStoreModern

// /** Like `PrerenderStoreModern`, but only including static prerenders (i.e. not runtime prerenders) */
export type StaticPrerenderStore = Exclude<
  PrerenderStore,
  PrerenderStoreModernRuntime | ValidationStoreClient
>

export interface CommonCacheStore
  extends Omit<CommonWorkUnitStore, 'implicitTags'> {
  /**
   * A cache work unit store might not always have an outer work unit store,
   * from which implicit tags could be inherited.
   */
  readonly implicitTags: ImplicitTags | undefined
  /**
   * Draft mode is only available if the outer work unit store is a request
   * store and draft mode is enabled.
   */
  readonly draftMode: DraftModeProvider | undefined
}

export interface CommonUseCacheStore extends CommonCacheStore, RevalidateStore {
  explicitRevalidate: undefined | number // explicit revalidate time from cacheLife() calls
  explicitExpire: undefined | number // server expiration time
  explicitStale: undefined | number // client expiration time
  readonly hmrRefreshHash: string | undefined
  readonly isHmrRefresh: boolean
  readonly serverComponentsHmrCache: ServerComponentsHmrCache | undefined
  readonly forceRevalidate: boolean
  readonly outerOwnerStack: string | undefined
}

export interface PublicUseCacheStore extends CommonUseCacheStore {
  readonly type: 'cache'

  /**
   * The root params for the current route. `undefined` when nested inside
   * `unstable_cache`, which doesn't carry root params. Currently, `"use cache"`
   * inside `unstable_cache` is allowed, so this case must be handled. The error
   * message in `getRootParam` assumes this is the only scenario where
   * `rootParams` is `undefined`.
   */
  readonly rootParams: Params | undefined
  /**
   * Tracks which root param names were read during this cache invocation.
   */
  readonly readRootParamNames: Set<string>
}

export interface PrivateUseCacheStore extends CommonUseCacheStore {
  readonly type: 'private-cache'

  readonly headers: ReadonlyHeaders
  readonly cookies: ReadonlyRequestCookies

  /**
   * Private caches don't currently need to track read root params for the cache
   * key because they're not persisted anywhere.
   */
  readonly rootParams: Params
}

export type UseCacheStore = PublicUseCacheStore | PrivateUseCacheStore

export interface UnstableCacheStore extends CommonCacheStore {
  readonly type: 'unstable-cache'
  /**
   * Always `undefined` for `unstable_cache` — root params are not available in
   * this context. If a `"use cache"` function nested inside `unstable_cache`
   * tries to access root params, it will encounter `undefined` here and throw.
   */
  readonly rootParams: undefined
}

/**
 * The Cache store is for tracking information inside a "use cache" or
 * unstable_cache context. A cache store shadows an outer request store (if
 * present) as a work unit, so that we never accidentally expose any request or
 * page specific information to cache functions, unless it's explicitly desired.
 * For those exceptions, the data is copied over from the request store to the
 * cache store, instead of generally making the request store available to cache
 * functions.
 */
export type CacheStore = UseCacheStore | UnstableCacheStore

export interface GenerateStaticParamsStore extends CommonWorkUnitStore {
  readonly type: 'generate-static-params'
  readonly rootParams: Params
}

export type WorkUnitStore =
  | RequestStore
  | CacheStore
  | PrerenderStore
  | GenerateStaticParamsStore

export type WorkUnitAsyncStorage = AsyncLocalStorage<WorkUnitStore>

export { workUnitAsyncStorageInstance as workUnitAsyncStorage }

export function throwForMissingRequestStore(callingExpression: string): never {
  throw new Error(
    `\`${callingExpression}\` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
  )
}

export function throwInvariantForMissingStore(): never {
  throw new InvariantError('Expected workUnitAsyncStorage to have a store.')
}

export function getPrerenderResumeDataCache(
  workUnitStore: WorkUnitStore
): PrerenderResumeDataCache | null {
  switch (workUnitStore.type) {
    case 'prerender':
    case 'prerender-runtime':
    case 'prerender-ppr':
      return workUnitStore.prerenderResumeDataCache
    case 'prerender-client':
    case 'validation-client':
      // TODO eliminate fetch caching in client scope and stop exposing this data
      // cache during SSR.
      return workUnitStore.prerenderResumeDataCache
    case 'request': {
      // In dev, we might fill caches even during a dynamic request.
      if (workUnitStore.prerenderResumeDataCache) {
        return workUnitStore.prerenderResumeDataCache
      }
      // fallthrough
    }
    case 'prerender-legacy':
    case 'cache':
    case 'private-cache':
    case 'unstable-cache':
    case 'generate-static-params':
      return null
    default:
      return workUnitStore satisfies never
  }
}

export function getRenderResumeDataCache(
  workUnitStore: WorkUnitStore
): RenderResumeDataCache | null {
  switch (workUnitStore.type) {
    case 'request':
    case 'prerender':
    case 'prerender-runtime':
    case 'prerender-client':
    case 'validation-client':
      if (workUnitStore.renderResumeDataCache) {
        // If we are in a prerender, we might have a render resume data cache
        // that is used to read from prefilled caches.
        return workUnitStore.renderResumeDataCache
      }
    // fallthrough
    case 'prerender-ppr':
      // Otherwise we return the mutable resume data cache here as an immutable
      // version of the cache as it can also be used for reading.
      return workUnitStore.prerenderResumeDataCache ?? null
    case 'cache':
    case 'private-cache':
    case 'unstable-cache':
    case 'prerender-legacy':
    case 'generate-static-params':
      return null
    default:
      return workUnitStore satisfies never
  }
}

export function getHmrRefreshHash(
  workUnitStore: WorkUnitStore
): string | undefined {
  if (process.env.__NEXT_DEV_SERVER) {
    switch (workUnitStore.type) {
      case 'cache':
      case 'private-cache':
      case 'prerender':
      case 'prerender-runtime':
        return workUnitStore.hmrRefreshHash
      case 'request':
        return workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value
      case 'prerender-client':
      case 'validation-client':
      case 'prerender-ppr':
      case 'prerender-legacy':
      case 'unstable-cache':
      case 'generate-static-params':
        break
      default:
        workUnitStore satisfies never
    }
  }

  return undefined
}

export function isHmrRefresh(workUnitStore: WorkUnitStore): boolean {
  if (process.env.__NEXT_DEV_SERVER) {
    switch (workUnitStore.type) {
      case 'cache':
      case 'private-cache':
      case 'request':
        return workUnitStore.isHmrRefresh ?? false
      case 'prerender':
      case 'prerender-client':
      case 'validation-client':
      case 'prerender-runtime':
      case 'prerender-ppr':
      case 'prerender-legacy':
      case 'unstable-cache':
      case 'generate-static-params':
        break
      default:
        workUnitStore satisfies never
    }
  }

  return false
}

export function getServerComponentsHmrCache(
  workUnitStore: WorkUnitStore
): ServerComponentsHmrCache | undefined {
  if (process.env.__NEXT_DEV_SERVER) {
    switch (workUnitStore.type) {
      case 'cache':
      case 'private-cache':
      case 'request':
        return workUnitStore.serverComponentsHmrCache
      case 'prerender':
      case 'prerender-client':
      case 'validation-client':
      case 'prerender-runtime':
      case 'prerender-ppr':
      case 'prerender-legacy':
      case 'unstable-cache':
      case 'generate-static-params':
        break
      default:
        workUnitStore satisfies never
    }
  }

  return undefined
}

/**
 * Returns a draft mode provider only if draft mode is enabled.
 */
export function getDraftModeProviderForCacheScope(
  workStore: WorkStore,
  workUnitStore: WorkUnitStore
): DraftModeProvider | undefined {
  if (workStore.isDraftMode) {
    switch (workUnitStore.type) {
      case 'cache':
      case 'private-cache':
      case 'unstable-cache':
      case 'prerender-runtime':
      case 'request':
        return workUnitStore.draftMode
      case 'prerender':
      case 'prerender-client':
      case 'validation-client':
      case 'prerender-ppr':
      case 'prerender-legacy':
      case 'generate-static-params':
        break
      default:
        workUnitStore satisfies never
    }
  }

  return undefined
}

export function getStagedRenderingController(
  workUnitStore: WorkUnitStore
): StagedRenderingController | null {
  switch (workUnitStore.type) {
    case 'request':
    case 'prerender-runtime':
      return workUnitStore.stagedRendering ?? null
    case 'prerender':
    case 'prerender-client':
    case 'validation-client':
    case 'prerender-ppr':
    case 'prerender-legacy':
    case 'cache':
    case 'private-cache':
    case 'unstable-cache':
    case 'generate-static-params':
      return null
    default:
      return workUnitStore satisfies never
  }
}

export function getCacheSignal(
  workUnitStore: WorkUnitStore
): CacheSignal | null {
  switch (workUnitStore.type) {
    case 'prerender':
    case 'prerender-client':
    case 'validation-client':
    case 'prerender-runtime':
      return workUnitStore.cacheSignal
    case 'request': {
      // In dev, we might fill caches even during a dynamic request.
      if (workUnitStore.cacheSignal) {
        return workUnitStore.cacheSignal
      }
      // fallthrough
    }
    case 'prerender-ppr':
    case 'prerender-legacy':
    case 'cache':
    case 'private-cache':
    case 'unstable-cache':
    case 'generate-static-params':
      return null
    default:
      return workUnitStore satisfies never
  }
}
Quest for Codev2.0.0
/
SIGN IN