next.js/packages/next/src/client/components/segment-cache/optimistic-routes.ts
optimistic-routes.ts891 lines30.2 KB
/**
 * Optimistic Routing (Known Routes)
 *
 * This module enables the client to predict route structure for URLs that
 * haven't been prefetched yet, based on previously learned route patterns.
 * When successful, this allows skipping the route tree prefetch request
 * entirely.
 *
 * The core idea is that many URLs map to the same route structure. For example,
 * /blog/post-1 and /blog/post-2 both resolve to /blog/[slug]. Once we've
 * prefetched one, we can predict the structure of the other.
 *
 * However, we can't always make this prediction. Static siblings (like
 * /blog/featured alongside /blog/[slug]) have different route structures.
 * When we learn a dynamic route, we also learn its static siblings so we
 * know when NOT to apply the prediction.
 *
 * Main entry points:
 *
 * 1. discoverKnownRoute: Called after receiving a route tree from the server.
 *    Traverses the route tree, compares URL parts to segments, and populates
 *    the known route tree if they match. Routes are always inserted into the
 *    cache.
 *
 * 2. matchKnownRoute: Called when looking up a route with no cache entry.
 *    Matches the candidate URL against learned patterns. Returns a synthetic
 *    cache entry if successful, or null to fall back to server resolution.
 *
 * Rewrite detection happens during traversal: if a URL path part doesn't match
 * the corresponding route segment, we stop populating the known route tree
 * (since the mapping is incorrect) but still insert the route into the cache.
 *
 * The known route tree is append-only with no eviction. Route patterns are
 * derived from the filesystem, so they don't become stale within a session.
 * Cache invalidation on deploy clears everything anyway.
 *
 * Current limitations (deopt to server resolution):
 * - Rewrites: Detected during traversal (tree not populated, but route cached)
 * - Intercepted routes: The route tree varies by referrer (Next-Url header),
 *   so we can't predict the correct structure from the URL alone. Patterns are
 *   still stored during discovery (so the trie stays populated for non-
 *   intercepted siblings), but matching bails out when the pattern is marked
 *   as interceptable.
 */

import type { DynamicParamTypesShort } from '../../../shared/lib/app-router-types'
import type { RouteTree, FulfilledRouteCacheEntry } from './cache'
import {
  EntryStatus,
  writeRouteIntoCache,
  fulfillRouteCacheEntry,
  getCurrentRouteCacheVersion,
  type PendingRouteCacheEntry,
  createMetadataRouteTree,
} from './cache'
import { isValueExpired } from './cache-map'
import { doesStaticSegmentAppearInURL } from '../../route-params'
import type { NormalizedPathname, NormalizedSearch } from './cache-key'
import {
  appendLayoutVaryPath,
  finalizeLayoutVaryPath,
  finalizePageVaryPath,
  finalizeMetadataVaryPath,
  type PartialSegmentVaryPath,
  type PageVaryPath,
} from './vary-path'

/**
 * The known route tree is analogous to a route table. A different routing
 * implementation might use regexes or URLPattern; ours uses a trie indexed
 * by URL path segments.
 *
 * Each node (KnownRoutePart) represents a position in the URL and can have:
 * - staticChildren: Map of literal segments to child nodes
 * - dynamicChild: A single dynamic segment node ([slug], [...params], etc.)
 * - pattern: A cache entry template for routes that terminate here
 *
 * This tree only contains segments that correspond to actual filesystem routes.
 * Route groups like (marketing) and parallel routes like @modal are not
 * included since they don't appear in URLs. Similarly, if a URL is rewritten
 * to a different filesystem path, the original URL segments don't appear here
 * — only the resolved filesystem route structure is stored.
 *
 * Example tree after learning /blog/[slug], /blog/featured, and /about:
 *
 *   ├── about
 *   └── blog
 *       ├── featured
 *       └── [slug]
 *
 * When matching /blog/hello:
 *   1. "blog" matches static child
 *   2. "hello" doesn't match "featured", falls through to [slug]
 *   3. Returns [slug]'s pattern with resolved param { slug: "hello" }
 */
type KnownRoutePartBase = {
  // Known static paths at this level. The null vs Map distinction is
  // semantically meaningful:
  // - null: Static siblings are UNKNOWN at this level (e.g., webpack dev mode
  //   where routes are compiled on-demand). If there's a dynamicChild, we
  //   can't safely match it because the URL might be an unknown static sibling.
  // - Map (even if empty): Static siblings are KNOWN. We can safely match a
  //   dynamicChild if the URL doesn't match any entry in the Map.
  staticChildren: Map<string, KnownRoutePart> | null

  // The cache entry that serves as a pattern for this route.
  // When a URL matches, we clone this and substitute param values.
  // null means we know this path exists (from static siblings) but haven't
  // learned its structure yet.
  pattern: FulfilledRouteCacheEntry | null

  // TODO: For prefix rewrite support. When true, this part may not appear in
  // the candidate URL because it was injected by a rewrite.
  // mayBeSkippedInURL: boolean
}

// The dynamic child fields are structured as a union so that narrowing on
// dynamicChild also narrows dynamicChildParamName and dynamicChildParamType.
type KnownRoutePartWithoutDynamicChild = KnownRoutePartBase & {
  dynamicChild: null
  dynamicChildParamName: null
  dynamicChildParamType: null
}

type KnownRoutePartWithDynamicChild = KnownRoutePartBase & {
  dynamicChild: KnownRoutePart
  dynamicChildParamName: string
  dynamicChildParamType: DynamicParamTypesShort
}

type KnownRoutePart =
  | KnownRoutePartWithoutDynamicChild
  | KnownRoutePartWithDynamicChild

/**
 * Param values extracted during URL matching. Used to reify the template.
 * - string for regular dynamic [param]
 * - string[] for catch-all [...param] and optional catch-all [[...param]]
 */
type ResolvedParams = Map<string, string | string[]>

/**
 * Read the pattern from a KnownRoutePart, evicting it if expired.
 *
 * This prevents stale patterns (e.g. from InliningHintsStale route entries
 * with staleAt = -1) from being cloned into synthetic entries indefinitely.
 * Once evicted, the pattern slot can be repopulated by the next
 * discoverKnownRoute call with a fresh entry from a /_tree response.
 */
function readPattern(
  now: number,
  part: KnownRoutePart
): FulfilledRouteCacheEntry | null {
  const pattern = part.pattern
  if (pattern === null) {
    return null
  }
  if (isValueExpired(now, getCurrentRouteCacheVersion(), pattern)) {
    // The pattern is expired. Null it out so the slot can be repopulated.
    part.pattern = null
    return null
  }
  return pattern
}

function createEmptyPart(): KnownRoutePart {
  return {
    staticChildren: null,
    dynamicChild: null,
    dynamicChildParamName: null,
    dynamicChildParamType: null,
    pattern: null,
  }
}

// The root of the known route tree.
let knownRouteTreeRoot: KnownRoutePart = createEmptyPart()

/**
 * Learns a route pattern from a server response and inserts it into the cache.
 *
 * Called after receiving a route tree from the server (initial load, navigation,
 * or prefetch). Traverses the route tree, compares URL parts to segments, and
 * populates the known route tree if they match. Routes are always inserted into
 * the cache regardless of whether the URL matches the route structure.
 *
 * When pendingEntry is provided, it's fulfilled and used. When null, an entry
 * is created and inserted into the route cache map.
 *
 * When hasDynamicRewrite is true, the route entry is marked as having a
 * dynamic rewrite, which prevents it from being used as a template for future
 * predictions. This is set when we detect a mismatch between what we predicted
 * and what the server returned.
 *
 * Returns the fulfilled route cache entry.
 */
export function discoverKnownRoute(
  now: number,
  pathname: string,
  nextUrl: string | null,
  pendingEntry: PendingRouteCacheEntry | null,
  routeTree: RouteTree,
  metadataVaryPath: PageVaryPath,
  couldBeIntercepted: boolean,
  canonicalUrl: string,
  supportsPerSegmentPrefetching: boolean,
  hasDynamicRewrite: boolean
): FulfilledRouteCacheEntry {
  const tree = routeTree

  const pathnameParts = pathname.split('/').filter((p) => p !== '')
  const firstPart = pathnameParts.length > 0 ? pathnameParts[0] : null
  const remainingParts = pathnameParts.length > 0 ? pathnameParts.slice(1) : []

  if (pendingEntry !== null) {
    // Fulfill the pending entry first
    const fulfilledEntry = fulfillRouteCacheEntry(
      now,
      pendingEntry,
      tree,
      metadataVaryPath,
      couldBeIntercepted,
      canonicalUrl,
      supportsPerSegmentPrefetching
    )
    if (hasDynamicRewrite) {
      fulfilledEntry.hasDynamicRewrite = true
    }
    // Populate the known route tree (handles rewrite detection internally).
    // The entry is already in the cache; this just stores it as a pattern
    // if the URL matches the route structure.
    discoverKnownRoutePart(
      knownRouteTreeRoot,
      tree,
      firstPart,
      remainingParts,
      fulfilledEntry,
      now,
      pathname,
      nextUrl,
      tree,
      metadataVaryPath,
      couldBeIntercepted,
      canonicalUrl,
      supportsPerSegmentPrefetching,
      hasDynamicRewrite
    )
    return fulfilledEntry
  }

  // No pending entry - discoverKnownRoutePart will create one and insert it
  // into the cache, or return an existing pattern if one exists.
  return discoverKnownRoutePart(
    knownRouteTreeRoot,
    tree,
    firstPart,
    remainingParts,
    null,
    now,
    pathname,
    nextUrl,
    tree,
    metadataVaryPath,
    couldBeIntercepted,
    canonicalUrl,
    supportsPerSegmentPrefetching,
    hasDynamicRewrite
  )
}

/**
 * Gets or creates the dynamic child node for a KnownRoutePart.
 * A node can have at most one dynamic child (you can't have both [slug] and
 * [id] at the same route level), so we either return existing or create new.
 */
function discoverDynamicChild(
  part: KnownRoutePart,
  paramName: string,
  paramType: DynamicParamTypesShort
): KnownRoutePart {
  if (part.dynamicChild !== null) {
    return part.dynamicChild
  }
  const newChild = createEmptyPart()
  // Type assertion needed because we're converting from "without" to "with"
  // dynamic child variant.
  const mutablePart = part as unknown as KnownRoutePartWithDynamicChild
  mutablePart.dynamicChild = newChild
  mutablePart.dynamicChildParamName = paramName
  mutablePart.dynamicChildParamType = paramType
  return newChild
}

/**
 * Recursive workhorse for discoverKnownRoute.
 *
 * Walks the route tree and URL parts in parallel, building out the known
 * route tree as it goes. At each step:
 * 1. Determines if the current segment appears in the URL (dynamic/static)
 * 2. Validates URL matches route structure (detects rewrites)
 * 3. Creates/updates the corresponding KnownRoutePart node
 * 4. Records static siblings for future matching
 * 5. Recurses into child slots (parallel routes)
 *
 * If a URL/route mismatch is detected (rewrite), we stop building the known
 * route tree but still cache the route entry for direct lookup.
 */
function discoverKnownRoutePart(
  parentKnownRoutePart: KnownRoutePart,
  routeTree: RouteTree,
  urlPart: string | null,
  remainingParts: string[],
  existingEntry: FulfilledRouteCacheEntry | null,
  // These are passed through unchanged for entry creation at the leaf
  now: number,
  pathname: string,
  nextUrl: string | null,
  fullTree: RouteTree,
  metadataVaryPath: PageVaryPath,
  couldBeIntercepted: boolean,
  canonicalUrl: string,
  supportsPerSegmentPrefetching: boolean,
  hasDynamicRewrite: boolean
): FulfilledRouteCacheEntry {
  const segment = routeTree.segment

  let segmentAppearsInURL: boolean
  let paramName: string | null = null
  let paramType: DynamicParamTypesShort | null = null
  let staticSiblings: readonly string[] | null = null

  if (typeof segment === 'string') {
    segmentAppearsInURL = doesStaticSegmentAppearInURL(segment)
  } else {
    // Dynamic segment tuple: [paramName, paramCacheKey, paramType, staticSiblings]
    paramName = segment[0]
    paramType = segment[2]
    staticSiblings = segment[3]
    segmentAppearsInURL = true
  }

  let knownRoutePart: KnownRoutePart = parentKnownRoutePart
  let nextUrlPart: string | null = urlPart
  let nextRemainingParts: string[] = remainingParts

  if (segmentAppearsInURL) {
    // Check for mismatch: if this is a static segment, the URL part must match
    if (paramName === null && urlPart !== segment) {
      // URL doesn't match route structure (likely a rewrite).
      // Don't populate the known route tree, just write the route into the
      // cache and return immediately.
      if (existingEntry !== null) {
        return existingEntry
      }
      return writeRouteIntoCache(
        now,
        pathname as NormalizedPathname,
        nextUrl,
        fullTree,
        metadataVaryPath,
        couldBeIntercepted,
        canonicalUrl,
        supportsPerSegmentPrefetching
      )
    }

    // URL matches route structure. Build the known route tree.
    if (paramName !== null && paramType !== null) {
      // Dynamic segment
      knownRoutePart = discoverDynamicChild(
        parentKnownRoutePart,
        paramName,
        paramType
      )

      // Record static siblings as placeholder parts.
      // IMPORTANT: We use the null vs Map distinction to track whether
      // siblings are known at this level:
      // - staticChildren: null = siblings unknown (can't safely match dynamic)
      // - staticChildren: Map = siblings known (even if empty)
      // This matters in dev mode where webpack may not know all siblings yet.
      if (staticSiblings !== null) {
        // Siblings are known - ensure we have a Map (even if empty)
        if (parentKnownRoutePart.staticChildren === null) {
          parentKnownRoutePart.staticChildren = new Map()
        }
        for (const sibling of staticSiblings) {
          if (!parentKnownRoutePart.staticChildren.has(sibling)) {
            parentKnownRoutePart.staticChildren.set(sibling, createEmptyPart())
          }
        }
      }
    } else {
      // Static segment
      if (parentKnownRoutePart.staticChildren === null) {
        parentKnownRoutePart.staticChildren = new Map()
      }
      let existingChild = parentKnownRoutePart.staticChildren.get(urlPart!)
      if (existingChild === undefined) {
        existingChild = createEmptyPart()
        parentKnownRoutePart.staticChildren.set(urlPart!, existingChild)
      }
      knownRoutePart = existingChild
    }

    // Advance to next URL part
    nextUrlPart = remainingParts.length > 0 ? remainingParts[0] : null
    nextRemainingParts =
      remainingParts.length > 0 ? remainingParts.slice(1) : []
  }
  // else: Transparent segment (route group, __PAGE__, etc.)
  // Stay at the same known route part, don't advance URL parts

  // Recurse into child routes. A route tree can have multiple parallel routes
  // (e.g., @modal alongside children). Each parallel route is a separate
  // branch, but they all share the same URL - we just need to traverse all
  // branches to build out the known route tree.
  const slots = routeTree.slots
  let resultFromChildren: FulfilledRouteCacheEntry | null = null
  if (slots !== null) {
    for (const parallelRouteKey in slots) {
      const childRouteTree = slots[parallelRouteKey]
      // Skip branches with refreshState set - these were reused from a
      // different route (e.g., a "default" parallel slot) and don't represent
      // the actual route structure for this URL.
      if (childRouteTree.refreshState !== null) {
        continue
      }
      const result = discoverKnownRoutePart(
        knownRoutePart,
        childRouteTree,
        nextUrlPart,
        nextRemainingParts,
        existingEntry,
        now,
        pathname,
        nextUrl,
        fullTree,
        metadataVaryPath,
        couldBeIntercepted,
        canonicalUrl,
        supportsPerSegmentPrefetching,
        hasDynamicRewrite
      )
      // All parallel route branches share the same URL, so they should all
      // reach compatible leaf nodes. We capture any result.
      resultFromChildren = result
    }
    if (resultFromChildren !== null) {
      return resultFromChildren
    }
    // Defensive fallback: no children returned a result. This shouldn't happen
    // for valid route trees, but handle it gracefully.
    if (existingEntry !== null) {
      return existingEntry
    }
    return writeRouteIntoCache(
      now,
      pathname as NormalizedPathname,
      nextUrl,
      fullTree,
      metadataVaryPath,
      couldBeIntercepted,
      canonicalUrl,
      supportsPerSegmentPrefetching
    )
  }

  // Reached a page node. Create/get the route cache entry and store as a
  // pattern. First, check if there's already a pattern for this route.
  const existingPattern = readPattern(now, knownRoutePart)
  if (existingPattern !== null) {
    // If this route has a dynamic rewrite, mark the existing pattern.
    if (hasDynamicRewrite) {
      existingPattern.hasDynamicRewrite = true
    }
    return existingPattern
  }

  // Get or create the entry
  let entry: FulfilledRouteCacheEntry
  if (existingEntry !== null) {
    // Already have a fulfilled entry, use it directly. It's already in the
    // route cache map.
    entry = existingEntry
  } else {
    // Create the entry and insert it into the route cache map.
    entry = writeRouteIntoCache(
      now,
      pathname as NormalizedPathname,
      nextUrl,
      fullTree,
      metadataVaryPath,
      couldBeIntercepted,
      canonicalUrl,
      supportsPerSegmentPrefetching
    )
  }

  if (hasDynamicRewrite) {
    entry.hasDynamicRewrite = true
  }

  // Store as pattern
  knownRoutePart.pattern = entry
  return entry
}

/**
 * Attempts to match a URL against learned route patterns.
 *
 * Returns a synthetic FulfilledRouteCacheEntry if the URL matches a known
 * pattern, or null if no match is found (fall back to server resolution).
 */
export function matchKnownRoute(
  now: number,
  pathname: string,
  search: NormalizedSearch
): FulfilledRouteCacheEntry | null {
  const pathnameParts = pathname.split('/').filter((p) => p !== '')
  const resolvedParams: ResolvedParams = new Map()
  const match = matchKnownRoutePart(
    now,
    knownRouteTreeRoot,
    pathnameParts,
    0,
    resolvedParams
  )

  if (match === null) {
    return null
  }

  const matchedPart = match.part
  const pattern = match.pattern

  // If the pattern could be intercepted, we can't safely use it for prediction.
  // Interception routes resolve to different route trees depending on the
  // referrer (the Next-Url header), which means the same URL can map to
  // different page components depending on where the navigation originated.
  // Since the known route tree only stores a single pattern per URL shape, we
  // can't distinguish between the intercepted and non-intercepted cases, so we
  // bail out to server resolution.
  //
  // TODO: We could store interception behavior in the known route tree itself
  // (e.g., which segments use interception markers and what they resolve to).
  // With enough information embedded in the trie, we could match interception
  // routes entirely on the client without a server round-trip.
  if (pattern.couldBeIntercepted) {
    return null
  }

  // "Reify" the pattern: clone the template tree with concrete param values.
  // This substitutes resolved params (e.g., slug: "hello") into dynamic
  // segments and recomputes vary paths for correct segment cache keying.
  const acc: ReifyAccumulator = { metadataVaryPath: null }
  const reifiedTree = reifyRouteTree(
    pattern.tree,
    resolvedParams,
    search,
    null, // Start with null partial vary path at the root
    acc
  )

  // The metadata tree is a flat page node without the intermediate layout
  // structure. Clone it with the updated metadata vary path collected during
  // the main tree traversal.
  const metadataVaryPath = acc.metadataVaryPath
  if (metadataVaryPath === null) {
    // This shouldn't be reachable for a valid route tree.
    return null
  }
  const reifiedMetadata = createMetadataRouteTree(metadataVaryPath)

  // Create a synthetic (predicted) entry and store it as the new pattern.
  //
  // Why replace the pattern? We intentionally update the pattern with this
  // synthetic entry so that if our prediction was wrong (server returns a
  // different pathname due to dynamic rewrite), the entry gets marked with
  // hasDynamicRewrite. Future predictions for this route will see the flag
  // and bail out to server resolution instead of making the same mistake.
  const syntheticEntry: FulfilledRouteCacheEntry = {
    canonicalUrl: pathname + search,
    status: EntryStatus.Fulfilled,
    blockedTasks: null,
    tree: reifiedTree,
    metadata: reifiedMetadata,
    couldBeIntercepted: pattern.couldBeIntercepted,
    supportsPerSegmentPrefetching: pattern.supportsPerSegmentPrefetching,
    hasDynamicRewrite: false,
    renderedSearch: search,
    ref: null,
    size: pattern.size,
    staleAt: pattern.staleAt,
    version: pattern.version,
  }

  matchedPart.pattern = syntheticEntry

  return syntheticEntry
}

/**
 * Result of a successful match: the matched tree node and its pattern.
 * We return both because the caller needs to update the pattern after
 * creating a synthetic entry (for dynamic rewrite detection).
 */
type KnownRouteMatch = {
  part: KnownRoutePart
  pattern: FulfilledRouteCacheEntry
} | null

/**
 * Recursively matches a URL against the known route tree.
 *
 * Matching priority (most specific first):
 * 1. Static children - exact path segment match
 * 2. Dynamic child - [param], [...param], [[...param]]
 * 3. Direct pattern - when no more URL parts remain
 *
 * Collects resolved param values in resolvedParams as it traverses.
 * Returns null if no match found (caller should fall back to server).
 */
function matchKnownRoutePart(
  now: number,
  part: KnownRoutePart,
  pathnameParts: string[],
  partIndex: number,
  resolvedParams: ResolvedParams
): KnownRouteMatch {
  const urlPart =
    partIndex < pathnameParts.length ? pathnameParts[partIndex] : null

  // If staticChildren is null, we don't know what static routes exist at this
  // level. This happens in webpack dev mode where routes are compiled
  // on-demand. We can't safely match a dynamicChild because the URL part might
  // be a static sibling we haven't discovered yet. Example: We know
  // /blog/[slug] exists, but haven't compiled /blog/featured. A request for
  // /blog/featured would incorrectly match /blog/[slug].
  if (part.staticChildren === null) {
    // The only safe match is a direct pattern when no URL parts remain.
    if (urlPart === null) {
      const pattern = readPattern(now, part)
      if (pattern !== null && !pattern.hasDynamicRewrite) {
        return { part, pattern }
      }
    }
    return null
  }

  // Static children take priority over dynamic. This ensures /blog/featured
  // matches its own route rather than /blog/[slug].
  if (urlPart !== null) {
    const staticChild = part.staticChildren.get(urlPart)
    if (staticChild !== undefined) {
      // Check if this is an "unknown" placeholder part. These are created when
      // we learn about static siblings (from the route tree's staticSiblings
      // field) but haven't prefetched them yet. We know the path exists but
      // don't know its structure, so we can't predict it.
      if (
        staticChild.pattern === null &&
        staticChild.dynamicChild === null &&
        staticChild.staticChildren === null
      ) {
        // Bail out - server must resolve this route.
        return null
      }
      const match = matchKnownRoutePart(
        now,
        staticChild,
        pathnameParts,
        partIndex + 1,
        resolvedParams
      )
      if (match !== null) {
        return match
      }
      // Static child is a real node (not a placeholder) but its subtree
      // didn't match the remaining URL parts. This means the route exists
      // in the static subtree but hasn't been fully discovered yet. Do not
      // fall through to try the dynamic child — the static match is
      // authoritative. Bail out to server resolution.
      return null
    }
  }

  // Try dynamic child
  if (part.dynamicChild !== null) {
    const dynamicPart = part.dynamicChild
    const paramName = part.dynamicChildParamName
    const paramType = part.dynamicChildParamType
    const dynamicPattern = readPattern(now, dynamicPart)

    switch (paramType) {
      case 'c':
        // Required catch-all [...param]: consumes 1+ URL parts
        if (
          dynamicPattern !== null &&
          !dynamicPattern.hasDynamicRewrite &&
          urlPart !== null
        ) {
          resolvedParams.set(paramName, pathnameParts.slice(partIndex))
          return { part: dynamicPart, pattern: dynamicPattern }
        }
        break
      case 'oc': {
        // Optional catch-all [[...param]]: consumes 0+ URL parts
        if (dynamicPattern !== null && !dynamicPattern.hasDynamicRewrite) {
          if (urlPart !== null) {
            resolvedParams.set(paramName, pathnameParts.slice(partIndex))
            return { part: dynamicPart, pattern: dynamicPattern }
          }
          // urlPart is null - can match with zero parts, but a direct pattern
          // (e.g., page.tsx alongside [[...param]]) takes precedence.
          const directPattern = readPattern(now, part)
          if (directPattern === null || directPattern.hasDynamicRewrite) {
            resolvedParams.set(paramName, [])
            return { part: dynamicPart, pattern: dynamicPattern }
          }
        }
        break
      }
      case 'd':
        // Regular dynamic [param]: consumes exactly 1 URL part.
        // Unlike catch-all which terminates here, regular dynamic must
        // continue recursing to find the leaf pattern.
        if (urlPart !== null) {
          resolvedParams.set(paramName, urlPart)
          return matchKnownRoutePart(
            now,
            dynamicPart,
            pathnameParts,
            partIndex + 1,
            resolvedParams
          )
        }
        break
      // Intercepted routes use relative path markers like (.), (..), (...)
      // Their behavior depends on navigation context (soft vs hard nav),
      // so we can't predict them client-side. Defer to server.
      case 'ci(..)(..)':
      case 'ci(.)':
      case 'ci(..)':
      case 'ci(...)':
      case 'di(..)(..)':
      case 'di(.)':
      case 'di(..)':
      case 'di(...)':
        return null
      default:
        paramType satisfies never
    }
  }

  // No children matched. If we've consumed all URL parts, check for a direct
  // pattern at this node (the route terminates here).
  if (urlPart === null) {
    const pattern = readPattern(now, part)
    if (pattern !== null && !pattern.hasDynamicRewrite) {
      return { part, pattern }
    }
  }

  return null
}

/**
 * Accumulator for collecting data during reifyRouteTree traversal.
 * metadataVaryPath is collected from the first page node encountered
 * (parallel routes may have multiple pages, but metadata uses the first).
 */
type ReifyAccumulator = {
  metadataVaryPath: PageVaryPath | null
}

/**
 * "Reify" means to make concrete - we take an abstract pattern (the template
 * route tree) and produce a concrete instance with actual param values.
 *
 * This function clones a RouteTree, substituting dynamic segment values from
 * resolvedParams and computing new vary paths. The vary path encodes param
 * values so segment cache entries can be correctly keyed.
 *
 * Example: Pattern for /blog/[slug] with resolvedParams { slug: "hello" }
 * produces a tree where segment [slug] has cacheKey "hello".
 */
function reifyRouteTree(
  pattern: RouteTree,
  resolvedParams: ResolvedParams,
  search: NormalizedSearch,
  parentPartialVaryPath: PartialSegmentVaryPath | null,
  acc: ReifyAccumulator
): RouteTree {
  const originalSegment = pattern.segment

  let newSegment = originalSegment
  let partialVaryPath: PartialSegmentVaryPath | null

  if (typeof originalSegment !== 'string') {
    // Dynamic segment: compute new cache key and append to partial vary path
    const paramName = originalSegment[0]
    const paramType = originalSegment[2]
    const staticSiblings = originalSegment[3]
    const newValue = resolvedParams.get(paramName)
    if (newValue !== undefined) {
      const newCacheKey = Array.isArray(newValue)
        ? newValue.join('/')
        : newValue
      newSegment = [paramName, newCacheKey, paramType, staticSiblings]
      partialVaryPath = appendLayoutVaryPath(
        parentPartialVaryPath,
        newCacheKey,
        paramName
      )
    } else {
      // Param not found in resolvedParams - keep original and inherit partial
      // TODO: This should never happen. Bail out with null.
      partialVaryPath = parentPartialVaryPath
    }
  } else {
    // Static segment: inherit partial vary path from parent
    partialVaryPath = parentPartialVaryPath
  }

  // Recurse into children with the (possibly updated) partial vary path
  let newSlots: Record<string, RouteTree> | null = null
  if (pattern.slots !== null) {
    newSlots = {}
    for (const key in pattern.slots) {
      newSlots[key] = reifyRouteTree(
        pattern.slots[key],
        resolvedParams,
        search,
        partialVaryPath,
        acc
      )
    }
  }

  if (pattern.isPage) {
    // Page segment: finalize with search params
    const newVaryPath = finalizePageVaryPath(
      pattern.requestKey,
      search,
      partialVaryPath
    )
    // Collect metadata vary path (first page wins, same as original algorithm)
    if (acc.metadataVaryPath === null) {
      acc.metadataVaryPath = finalizeMetadataVaryPath(
        pattern.requestKey,
        search,
        partialVaryPath
      )
    }
    return {
      requestKey: pattern.requestKey,
      segment: newSegment,
      refreshState: pattern.refreshState,
      slots: newSlots,

      prefetchHints: pattern.prefetchHints,
      isPage: true,
      varyPath: newVaryPath,
    }
  } else {
    // Layout segment: finalize without search params
    const newVaryPath = finalizeLayoutVaryPath(
      pattern.requestKey,
      partialVaryPath
    )
    return {
      requestKey: pattern.requestKey,
      segment: newSegment,
      refreshState: pattern.refreshState,
      slots: newSlots,

      prefetchHints: pattern.prefetchHints,
      isPage: false,
      varyPath: newVaryPath,
    }
  }
}

/**
 * Resets the known route tree. Called during development when routes may
 * change due to hot reloading.
 */
export function resetKnownRoutes(): void {
  knownRouteTreeRoot = createEmptyPart()
}
Quest for Codev2.0.0
/
SIGN IN