UNPKG

next

Version:

The React Framework

435 lines (434 loc) • 25.4 kB
/** * Decoding of RSC server responses (the transport format defined in * shared/lib/rsc-transport) into the client's own representations. This is * the only place on the client that consumes transport types; everything * downstream operates on RouteTree / NavigationSeed / CacheNode. */ import { PrefetchHint, SubtreePrefetchHints, propagateSubtreeBits } from '../../../shared/lib/app-router-types'; import { readFulfilledValue } from '../../../shared/lib/rsc-transport'; import { decodeVaryParams } from '../../../shared/lib/segment-cache/vary-params-decoding'; import { ROOT_SEGMENT_REQUEST_KEY, appendSegmentRequestKeyPart, createSegmentRequestKeyPart } from '../../../shared/lib/segment-cache/segment-value-encoding'; import { DEFAULT_SEGMENT_KEY, PAGE_SEGMENT_KEY } from '../../../shared/lib/segment'; import { InvariantError } from '../../../shared/lib/invariant-error'; import { doesStaticSegmentAppearInURL, getCacheKeyForDynamicParam, parseDynamicParamFromURLPart } from '../../route-params'; import { splitPathnameIntoParts } from './cache-key'; import { appendLayoutVaryPath, finalizeVaryPath, getPartialVaryPath, getShellSegmentVaryPath } from './vary-path'; import { convertFlightRouterStateToRouteTree, convertRootFlightRouterStateToRouteTree, createMetadataRouteTree, createRootRouteTree, getHeadRequestKey } from './cache'; import { computeDynamicStaleAt } from './bfcache'; /** * During a client navigation or prefetch, the server responds with a * transport tree that covers only the parts of the route that have changed. * This overlays it onto the base tree to produce a full RouteTree — slots the * response carries no information about are reused from the client's current * state — with the response's render output (RSCSegmentData) attached to * each node. * * "Create", not "decode": callers may pass no transport data at all * (refreshes and history restores do), in which case this converts the base * tree alone rather than decoding a response. */ export function createNavigationSeed(now, // Null when the response is not an overlay over existing client state — // per-segment prefetch responses, whose root-anchored tree covers its own // spine. Must be non-null when transportData is null (there'd be nothing // to convert otherwise). currentTree, transportData, // The response's root vary params (its `r` field): the root params // accessed anywhere in the response, emitted once at the response level // and unioned into the head's and every segment's own drained set here at // the decode boundary. Pass null when the response streams in // incrementally (navigation and reducer flows): the wire iterables can // only be drained completely from a fully-buffered response, so their sets // decode as null ("unknown; key on all params") without touching the wire // iterables. rootVaryParams, // Whether anything in the response is not fully resolved: dynamic holes, runtime holes, anything suspended. // Boolean-form nodes resolve their partiality to this value (their wire // boolean is a render-wide constant that carries no per-node information — // see decodeTransportNode), and under Cache Components so does a // boolean-form head (see the head read below); staged (promise-form) // nodes encode it per-node and ignore it. Only segment-cache writes // consume the decoded partiality, so callers whose seeds are never // written to the cache may pass the conservative value (true). isResponsePartial, // The pathname the response was rendered for. Required to resolve dynamic // segments the server sent without a param value (`k: null`); see // decodeTransportTreeIntoRouteTree. Callers whose responses always carry // concrete values (navigation responses) may pass null. renderedPathname, // Already normalized by the response reader (see getRenderedSearch); the // router state stores it as a plain string, so it is re-branded here. renderedSearch, // Where to key the head. Null derives it from the route's own first page // node (see createRouteTreeNode). Per-segment prefetch payloads pass the // route's own metadata vary path instead: a standalone head response's tree // is a bare root identity with no page node. metadataVaryPath, dynamicStaleTimeSeconds) { const normalizedRenderedSearch = renderedSearch; const acc = { metadataVaryPath: null, treeDivergedFromBase: false }; let routeTree; let headData = null; if (transportData !== null) { routeTree = decodeTransportTreeIntoRouteTree(transportData.t, currentTree, rootVaryParams, isResponsePartial, renderedPathname, normalizedRenderedSearch, acc); const transportHead = transportData.h; if (transportHead !== undefined) { // The wire form of `p` determines which signal is authoritative for // the head's partiality, mirroring the per-node rule in // decodeTransportNode: // // - Promise form (per-segment prefetch responses, fully buffered // before they're decoded): partiality is encoded exactly, per node, // via the staged encoding, so the thenable-status read is // authoritative. // - Boolean form (navigation and live-render responses): when Cache // Components is enabled, the server's flag (isPossiblyPartialHead in // app-render.tsx) is unreliable: it's computed before the head is // serialized, so it's conservatively `true` for every // statically-generated PPR page — even pages whose head is actually // complete — and it's `false` for live-render responses whose head // is actually partial (e.g. a route with an async // `generateMetadata`). So we ignore it and derive the head's // partiality from whether the response itself was partial, exactly // as the per-node rule does for segments. A non-partial response // carries a complete head; a partial (postponed) one does not. // Without Cache Components, the server sends the correct // isHeadPartial, so the wire boolean is used as-is. headData = { rsc: transportHead.r, isPartial: typeof transportHead.p === 'boolean' ? process.env.__NEXT_CACHE_COMPONENTS ? isResponsePartial : transportHead.p : readFulfilledIsPartial(transportHead.p), varyParams: decodeVaryParams(transportHead.v, rootVaryParams), staleTimeSeconds: transportHead.s !== undefined ? readFulfilledStaleTimeSeconds(transportHead.s) : null }; } } else { if (currentTree === null) { throw new InvariantError('Cannot convert a server response with no transport data and no ' + 'base tree.'); } routeTree = convertRootFlightRouterStateToRouteTree(currentTree, normalizedRenderedSearch, acc); } if (metadataVaryPath === null) { metadataVaryPath = acc.metadataVaryPath; if (metadataVaryPath === null) { // Every route renders a page, so a rendered tree always has a node to // key the head under. throw new InvariantError('Cannot key the head of a server response: its tree has no page ' + 'segment.'); } } return { root: createRootRouteTree(routeTree, createMetadataRouteTree(metadataVaryPath, routeTree.prefetchHints, headData)), renderedSearch: normalizedRenderedSearch, dynamicStaleAt: computeDynamicStaleAt(now, dynamicStaleTimeSeconds), treeDivergedFromBase: acc.treeDivergedFromBase }; } /** * Creates a RouteTree node for a segment, with its identity and cache-key * information (vary paths, the normalized segment value, the refresh state) * initialized, and the remaining fields set to their defaults. The caller * finishes initializing those in place after recursing into the children. * Shared by FlightRouterState conversion, transport decoding, and subtree * rebasing so their routing identity stays consistent. */ export function createRouteTreeNode(originalSegment, isRootParam, requestKey, parentPartialVaryPath, renderedSearch, refreshState, acc) { let segment; let partialVaryPath; let varyPath; if (Array.isArray(originalSegment)) { const paramCacheKey = originalSegment[1]; const paramName = originalSegment[0]; partialVaryPath = appendLayoutVaryPath(parentPartialVaryPath, paramCacheKey, paramName, isRootParam); varyPath = finalizeVaryPath(requestKey, null, partialVaryPath); segment = originalSegment; } else { // This segment does not have a param. Inherit the partial vary path of // the parent. partialVaryPath = parentPartialVaryPath; if (requestKey.endsWith(PAGE_SEGMENT_KEY)) { // This is a page segment. segment = PAGE_SEGMENT_KEY; varyPath = finalizeVaryPath(requestKey, renderedSearch, partialVaryPath); // The head is keyed under the route's own first page and varies on the // same params as that page (see getHeadRequestKey). A page reused from // another URL carries a refresh state and never keys it. if (refreshState === null && acc.metadataVaryPath === null) { acc.metadataVaryPath = finalizeVaryPath(getHeadRequestKey(requestKey), renderedSearch, partialVaryPath); } } else { // This is a layout segment. segment = originalSegment; varyPath = finalizeVaryPath(requestKey, null, partialVaryPath); } } return { requestKey, segment, shellVaryPath: getShellSegmentVaryPath(varyPath), refreshState, data: null, varyPath, slots: null, prefetchHints: 0 }; } /** * Decodes a response's transport tree into a RouteTree, using the client's * current router state as the base for the parts of the route the response * carries no information about. * * The response is an overlay over the base: * * - Nodes with rendered output — and nodes with no data at all, which are * server-sent structure whose output the client fetches lazily — are * authoritative: their identity, hints, and subtree come entirely from * the response. * - Skipped nodes (data with a null rsc) sit on the path from the root down * to the rendered subtrees. The client is expected to already have them, * so their refresh state and hints are inherited from the base tree, and * any slot the response doesn't mention is reused from the base as-is. * * TODO: The base is a FlightRouterState only because that's the * representation the client router currently renders from (the router * reducer's `state.tree`, which the render tree and layout-router are * keyed against). Once the rendering path is updated to use RouteTree as its * source of truth, the base tree here can be a RouteTree, and the base-only * conversion path (convertFlightRouterStateToRouteTree) goes away with it. */ export function decodeTransportTreeIntoRouteTree(transportNode, baseRouterState, // The response's root vary params, unioned into every segment's drained // set. Pass null when vary params are unavailable or unwanted; see // createNavigationSeed. rootVaryParams, // The response-level partiality, which boolean-form nodes resolve their // own partiality to; see createNavigationSeed. isResponsePartial, // The pathname the response was rendered for (from the response headers). // Required to resolve dynamic segments the server sent without a param // value (`k: null` — per-segment prefetch responses omit the value to stay // cacheable across param values); the client parses the value from the // pathname instead. Callers whose responses always carry concrete values // (navigation responses) may pass null. renderedPathname, renderedSearch, acc) { const pathnameParts = renderedPathname !== null ? splitPathnameIntoParts(renderedPathname) : null; return decodeTransportNode(transportNode, resolveTransportSegment(transportNode.s, pathnameParts, 0), baseRouterState ?? undefined, baseRouterState ?? undefined, rootVaryParams, isResponsePartial, ROOT_SEGMENT_REQUEST_KEY, null, renderedSearch, pathnameParts, 0, acc); } /** * Converts a segment's wire identity to the client `Segment` type, resolving * dynamic segments whose param value the server omitted (`k: null`) by * parsing the value from the rendered pathname. `pathnamePartsIndex` is the * URL position this segment occupies (tracked by the tree walk: incremented * only for segments that appear in the URL, so route groups and other * virtual segments don't consume a part). */ function resolveTransportSegment(transportSegment, pathnameParts, pathnamePartsIndex) { if (typeof transportSegment === 'string') { return transportSegment; } const paramKey = transportSegment.k; if (paramKey !== null) { return [ transportSegment.n, paramKey, transportSegment.t, transportSegment.s ]; } if (pathnameParts === null) { throw new InvariantError('Cannot resolve a dynamic segment that has no param value: the ' + 'response provides no rendered pathname to parse it from.'); } const paramValue = parseDynamicParamFromURLPart(transportSegment.t, pathnameParts, pathnamePartsIndex); return [ transportSegment.n, getCacheKeyForDynamicParam(paramValue), transportSegment.t, transportSegment.s ]; } function doSegmentsMatch(baseSegment, segment) { if (typeof baseSegment === 'string' || typeof segment === 'string') { // Static segments have to match exactly. return baseSegment === segment; } // Both segments are dynamic. The static sibling hints aren't part of the // segment's identity, so only compare the param name, type, and value. const [baseParamName, baseParamValue, baseParamType] = baseSegment; const [paramName, paramValue, paramType] = segment; return baseParamName === paramName && baseParamType === paramType && baseParamValue === paramValue; } function decodeTransportNode(node, // The node's identity, already resolved by the caller (the parent's child // loop, which has the URL position needed to parse omitted param values). originalSegment, base, // The base node to compare segment identities against (see // NavigationSeed.treeDivergedFromBase). Tracked separately from `base`: // inheritance drops the base inside authoritative subtrees, where the // comparison must continue, and keeps it through inactive parallel routes, // where the comparison must stop. compareBase, rootVaryParams, isResponsePartial, requestKey, parentPartialVaryPath, parentRenderedSearch, pathnameParts, // The URL position this node's children read from. pathnamePartsIndex, acc) { const nodeData = node.d; const inheritsFromBase = nodeData !== undefined && nodeData.r === null; // The base node this position inherits from, when it does. const inheritedBase = inheritsFromBase ? base : undefined; if (compareBase !== undefined && !acc.treeDivergedFromBase) { // Every transport node echoes the segment's identity, even "skipped" // ones, so each position can be compared against the base. const transportSegment = node.s; if (typeof transportSegment !== 'string' && transportSegment.k == null) { // The server omitted the param value for the client to parse from the // URL (see resolveTransportSegment). Nothing to compare; the children // are still checked. } else { const baseSegment = compareBase[0]; if (originalSegment === DEFAULT_SEGMENT_KEY) { // A default filled in by the server is not a claim about the // position's identity. } else if (!doSegmentsMatch(baseSegment, originalSegment)) { acc.treeDivergedFromBase = true; } } } const baseHints = inheritedBase !== undefined ? inheritedBase[4] ?? 0 : 0; let prefetchHints = node.h ?? baseHints; // This segment's param (if any) is a root param iff the segment is at or // above the root layout, which the server marks directly. const isRootParam = (prefetchHints & PrefetchHint.IsRootLayoutOrAbove) !== 0; // Inherited positions keep the base tree's refresh state. Its rendered // search is updated to this response's, since all pages within the same // response share the same search value. (The refresh state acts like a // "context provider" for inactive parallel routes.) const baseCompressedRefreshState = inheritedBase !== undefined ? inheritedBase[2] ?? null : null; const refreshState = baseCompressedRefreshState !== null ? { canonicalUrl: baseCompressedRefreshState[0], renderedSearch: parentRenderedSearch } : null; const renderedSearch = refreshState !== null ? refreshState.renderedSearch : parentRenderedSearch; const tree = createRouteTreeNode(originalSegment, isRootParam, requestKey, parentPartialVaryPath, renderedSearch, refreshState, acc); const partialVaryPath = getPartialVaryPath(tree.varyPath); let slots = null; const transportChildren = node.c; const baseChildren = inheritedBase !== undefined ? inheritedBase[1] : undefined; if (transportChildren !== undefined) { for (const [parallelRouteKey, childNode] of transportChildren){ const childBase = baseChildren !== undefined ? baseChildren[parallelRouteKey] : undefined; const childSegment = resolveTransportSegment(childNode.s, pathnameParts, pathnamePartsIndex); let childCompareBase; if (compareBase !== undefined && !acc.treeDivergedFromBase) { const childCompareCandidate = compareBase[1][parallelRouteKey]; if (childCompareCandidate === undefined) { // A slot the base tree doesn't have. Unless the server merely // filled it with a default, the trees have different structures. if (childSegment !== DEFAULT_SEGMENT_KEY) { acc.treeDivergedFromBase = true; } } else if ((childCompareCandidate[2] ?? null) !== null) { // The base branch carries a refresh state: an inactive parallel // route reused from a different route (e.g. a "default" slot). The // server's answer is expected to differ, so skip the branch. } else { childCompareBase = childCompareCandidate; } } // Only advance the URL position for segments that appear in the URL. // Virtual segments, like route groups, don't consume a part. const childDoesAppearInURL = typeof childSegment === 'string' ? doesStaticSegmentAppearInURL(childSegment) : true; const childPathnamePartsIndex = childDoesAppearInURL ? pathnamePartsIndex + 1 : pathnamePartsIndex; const childRequestKey = appendSegmentRequestKeyPart(requestKey, parallelRouteKey, createSegmentRequestKeyPart(childSegment)); const childTree = decodeTransportNode(childNode, childSegment, childBase, childCompareBase, rootVaryParams, isResponsePartial, childRequestKey, partialVaryPath, renderedSearch, pathnameParts, childPathnamePartsIndex, acc); if (slots === null) { slots = new Map(); } slots.set(parallelRouteKey, childTree); } } if (baseChildren !== undefined) { // Slots the response carries no information about are reused from the // base tree, structure-only. for(const parallelRouteKey in baseChildren){ if (transportChildren !== undefined && transportChildren.has(parallelRouteKey)) { continue; } const childBase = baseChildren[parallelRouteKey]; const childRequestKey = appendSegmentRequestKeyPart(requestKey, parallelRouteKey, createSegmentRequestKeyPart(childBase[0])); const childTree = convertFlightRouterStateToRouteTree(childBase, childRequestKey, partialVaryPath, renderedSearch, acc); if (slots === null) { slots = new Map(); } slots.set(parallelRouteKey, childTree); } } if (inheritsFromBase) { // Recompute the propagated "subtree" prefetch hints for this segment, // since its children may combine response and base subtrees. Mirrors the // propagation done on the server in createTransportTreeFromLoaderTree. let propagated = prefetchHints & ~SubtreePrefetchHints; if (slots !== null) { for (const childTree of slots.values()){ propagated = propagateSubtreeBits(propagated, childTree.prefetchHints); } } prefetchHints = propagated; } if (nodeData !== undefined) { tree.data = { rsc: nodeData.r, // The wire form of `p` determines which signal is authoritative for // this segment's partiality: // // - Boolean form (navigation and live-render responses): the wire // value is a render-wide constant (`isPossiblyPartialResponse` in // create-component-tree.tsx), identical on every node, so it carries // no per-node information — and it's inaccurate in both directions: // `true` for every node of a statically-generated PPR page even when // the page is actually complete, and `false` for every node of a // dynamic render even when this decode is a truncated stage prefix // whose dynamic rows landed past the boundary. The caller's // response-level value captures both (the `~`/`#` marker for whole // responses; truncation-implied partiality for stage decodes), so it // replaces the wire boolean here. // - Promise form (per-segment prefetch responses, fully buffered // before they're decoded): partiality is encoded per node, exactly, // and survives the truncated shell double-decode — a fulfillment row // past the boundary reads as partial. The fulfillment (or its // absence) is already visible on the thenable's status, so it's // authoritative and the response-level value is ignored. isPartial: typeof nodeData.p === 'boolean' ? isResponsePartial : readFulfilledIsPartial(nodeData.p), // The source of the params this segment's output depends on: the // segment's wire iterable, drained here, unioning in the response-level // root params (same buffered-read reasoning as `p` above), or decoded // as null ("unknown") when the caller passed no root params — see // createNavigationSeed. varyParams: decodeVaryParams(nodeData.v, rootVaryParams), // Per-node staleTime, only present in per-segment prefetch responses // (same buffered-read reasoning as `p` above). staleTimeSeconds: nodeData.s !== undefined ? readFulfilledStaleTimeSeconds(nodeData.s) : null }; } tree.slots = slots; tree.prefetchHints = prefetchHints; return tree; } // A sentinel `readFulfilledValue` fallback that no fulfillment can produce, // for reads that only care whether the row settled at all. const notFulfilled = Symbol(); /** * Reads a segment's partialness from its `isPartial` promise. (The * fulfillment value is void — partialness is encoded as the ABSENCE of a * fulfillment.) The server fulfills it only for a fully-static segment and * leaves it pending for a partial one (see the promise form of * `TransportSegmentData['p']`), so partial == not fulfilled. A pending row, * or a truncated shell decode whose fulfillment landed past the boundary, * reads as partial, which is correct either way. */ export function readFulfilledIsPartial(isPartial) { return readFulfilledValue(isPartial, notFulfilled) === notFulfilled; } /** * Reads a staleTime (in seconds) from the staleTime async iterable of a * fully-buffered response. Because the bytes are all present, each yielded * value is already visible on its chunk's thenable status, so this drains * synchronously and takes the last value (the final staleTime, matching the * async `resolveStaleAt` in cache.ts). Returns null when no usable value was * yielded — e.g. a truncated shell decode whose value landed past the * boundary — so the caller can fall back to its response-level staleness. */ export function readFulfilledStaleTimeSeconds(staleTime) { const iterator = staleTime[Symbol.asyncIterator](); let staleTimeSeconds; while(true){ const chunk = readFulfilledValue(iterator.next(), undefined); if (chunk === undefined || chunk.done) { break; } staleTimeSeconds = chunk.value; } if (staleTimeSeconds === undefined || isNaN(staleTimeSeconds)) { return null; } return staleTimeSeconds; } //# sourceMappingURL=decode-server-response.js.map