next
Version:
The React Framework
877 lines • 52.6 kB
JavaScript
/* eslint-disable @next/internal/no-ambiguous-jsx -- Bundled in entry-base so it gets the right JSX runtime. */ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
collectPrefetchHints: null,
collectSegmentData: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
collectPrefetchHints: function() {
return collectPrefetchHints;
},
collectSegmentData: function() {
return collectSegmentData;
}
});
const _jsxruntime = require("react/jsx-runtime");
const _approutertypes = require("../../shared/lib/app-router-types");
const _client = require("react-server-dom-webpack/client");
const _static = require("react-server-dom-webpack/static");
const _server = require("react-server-dom-webpack/server");
const _nodewebstreamshelper = require("../stream-utils/node-web-streams-helper");
const _segment = require("../../shared/lib/segment");
const _scheduler = require("../../lib/scheduler");
const _promisewithresolvers = require("../../shared/lib/promise-with-resolvers");
const _segmentvalueencoding = require("../../shared/lib/segment-cache/segment-value-encoding");
const _createerrorhandler = require("./create-error-handler");
const _prospectiverenderutils = require("./prospective-render-utils");
const _workasyncstorageexternal = require("./work-async-storage.external");
const filterStackFrame = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').filterStackFrameDEV : undefined;
const findSourceMapURL = process.env.NODE_ENV !== 'production' ? require('../lib/source-maps').findSourceMapURLDEV : undefined;
function onSegmentPrerenderError(error) {
const digest = (0, _createerrorhandler.getDigestForWellKnownError)(error);
if (digest) {
return digest;
}
// We don't need to log the errors because we would have already done that
// when generating the original Flight stream for the whole page.
if (process.env.NEXT_DEBUG_BUILD || process.env.__NEXT_VERBOSE_LOGGING) {
const workStore = _workasyncstorageexternal.workAsyncStorage.getStore();
(0, _prospectiverenderutils.printDebugThrownValueForProspectiveRender)(error, (workStore == null ? void 0 : workStore.route) ?? 'unknown route', _prospectiverenderutils.Phase.SegmentCollection);
}
}
/**
* Extract the FlightRouterState, seed data, and head from a prerendered
* InitialRSCPayload. Returns null if the payload doesn't match the expected
* shape: a single root path with no segment prefix, which has 4 elements
* ([tree, seedData, head, isHeadPartial], per getRSCPayload) or 3 when
* reconstructed without the isHeadPartial flag (per instant-validation).
*/ function extractFlightData(initialRSCPayload) {
const flightDataPaths = initialRSCPayload.f;
// FlightDataPath is an unsound type, hence the additional checks.
if (flightDataPaths.length !== 1 || flightDataPaths[0].length !== 3 && flightDataPaths[0].length !== 4) {
console.error('Internal Next.js error: InitialRSCPayload does not match the expected ' + 'shape for a prerendered page during segment prefetch generation.');
return null;
}
return {
buildId: initialRSCPayload.b,
flightRouterState: flightDataPaths[0][0],
seedData: flightDataPaths[0][1],
head: flightDataPaths[0][2]
};
}
async function collectSegmentData(isCacheComponentsEnabled, fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest, prefetchInlining, hints, isUpgradeableISRFallback) {
// Traverse the router tree and generate a prefetch response for each segment.
// A mutable map to collect the results as we traverse the route tree.
const resultMap = new Map();
// Before we start, warm up the module cache by decoding the page data once.
// Then we can assume that any remaining async tasks that occur the next time
// are due to hanging promises caused by dynamic data access. Note we only
// have to do this once per page, not per individual segment.
//
// The warm-up decode also tells us the page's own shell byte boundary (its
// `a` field): a byte offset into fullPageDataBuffer marking the end of the
// page's shell stage, null if the shell is identical to the full static
// response, or undefined if the render wasn't staged (no shell exists).
//
// And it tells us whether the render accessed runtime data (cookies,
// headers, fallback params, searchParams, ...): the settled value of the
// page's embedded access flag (its `u` field). Conservatively true when
// the page carries no flag (legacy render paths) or the decode fails.
let pageShellByteLength = undefined;
let runtimeDataAccessed = true;
try {
const pagePayload = await (0, _client.createFromReadableStream)(// Use a stream that never closes so pending references (dynamic
// holes) can't error the decode.
createUnclosingPrefetchStream((0, _nodewebstreamshelper.streamFromBuffer)(fullPageDataBuffer)), {
findSourceMapURL,
serverConsumerManifest
});
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
// `a` is a promise resolved mid-stream; the whole buffer is present, so
// it resolves. undefined means the render wasn't staged (no shell).
if (pagePayload.a !== undefined) {
pageShellByteLength = await pagePayload.a;
}
if (pagePayload.u !== undefined) {
runtimeDataAccessed = readRuntimeDataAccessed(pagePayload.u);
}
} catch {}
// All segment responses of a page share one decode of the page buffer, and
// one `release` promise that coordinates the two serialization stages: it
// resolves when the coordinator lets the shell-stage bytes settle into the
// rest of the page data (see renderSegmentPrefetch, which measures each
// response's shell boundary at that moment). Its resolved value is true
// when the page's shell is identical to its full static response — then so
// is every segment's, so each resolves `a` to null instead of a boundary.
//
// When the page has a real shell boundary the decode is staged: only the
// shell byte prefix is enqueued now, the rest held until the release. With
// no boundary there's nothing to stage on, so the whole buffer is decoded
// at once and the release is resolved eagerly (segment boundaries then fall
// out as 0 or null without any measurement).
const release = (0, _promisewithresolvers.createPromiseWithResolvers)();
let pageDataStream;
if (typeof pageShellByteLength === 'number') {
const prefixLength = pageShellByteLength;
pageDataStream = new ReadableStream({
async start (controller) {
// The shell byte prefix decodes to the page's shell variant:
// everything the staged page render serialized after its shell stage
// stays a pending reference until the release enqueues the rest.
controller.enqueue(fullPageDataBuffer.subarray(0, prefixLength));
await release.promise;
controller.enqueue(fullPageDataBuffer.subarray(prefixLength));
// Intentionally never closed, like createUnclosingPrefetchStream:
// the page stream may hold references that never resolve (dynamic
// holes), and Flight errors if the stream closes while any are pending.
}
});
} else {
pageDataStream = createUnclosingPrefetchStream((0, _nodewebstreamshelper.streamFromBuffer)(fullPageDataBuffer));
release.resolve(pageShellByteLength === null);
}
// Create an abort controller that we'll use to stop the stream.
const abortController = new AbortController();
const onCompletedProcessingRouteTree = async ()=>{
// Since all we're doing is decoding and re-encoding a cached prerender, if
// serializing the stream takes longer than a microtask, it must because of
// hanging promises caused by dynamic data.
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
abortController.abort();
};
// Generate a stream for the route tree prefetch. While we're walking the
// tree, we'll also spawn additional tasks to generate the segment prefetches.
// The promises for these tasks are pushed to a mutable array that we will
// await once the route tree is fully rendered.
const segmentTasks = [];
let treeStream;
try {
const prerenderResult = await (0, _static.prerender)(// RootTreePrefetch is not a valid return type for a React component, but
// we need to use a component so that when we decode the original stream
// inside of it, the side effects are transferred to the new stream.
// @ts-expect-error
/*#__PURE__*/ (0, _jsxruntime.jsx)(PrefetchTreeData, {
isClientParamParsingEnabled: isCacheComponentsEnabled,
pageDataStream: pageDataStream,
serverConsumerManifest: serverConsumerManifest,
clientModules: clientModules,
staleTime: staleTime,
segmentTasks: segmentTasks,
onCompletedProcessingRouteTree: onCompletedProcessingRouteTree,
prefetchInlining: prefetchInlining,
hints: hints,
isUpgradeableISRFallback: isUpgradeableISRFallback,
runtimeDataAccessed: runtimeDataAccessed,
shellStageRelease: release.promise
}), clientModules, {
filterStackFrame,
signal: abortController.signal,
onError: onSegmentPrerenderError
});
treeStream = prerenderResult.prelude;
// The tree walk has spawned every segment render, each against the fully
// available shell prefix; one task later they've flushed their shells and
// are blocked on the release for the rest.
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
} catch (error) {
// The release still fires (finally below), so the spawned tasks run to
// completion — but the throw skips the Promise.all that observes them.
// Absorb their rejections so a failing task can't crash as an unhandled
// rejection and mask this error.
void Promise.allSettled(segmentTasks);
throw error;
} finally{
// Start the second stage: settle the rest of the page data into the
// decode so each render can measure its boundary and finish. Runs on
// failure too, so a tree-render error can't strand the spawned tasks on
// a release that never comes. (false is inert here: a staged page's shell
// is always a strict prefix, and an unstaged release already resolved.)
// TODO: I don't think it's really necessary to unblock the spawned tasks.
// It's fine if they hang indefinitely; the tasks will be garbage collected.
release.resolve(false);
}
// Write the route tree to a special `/_tree` segment.
const treeBuffer = await (0, _nodewebstreamshelper.streamToBuffer)(treeStream);
resultMap.set('/_tree', treeBuffer);
// Also output the entire full page data response
resultMap.set('/_full', fullPageDataBuffer);
// Await the segment tasks in parallel and write the segment prefetches to
// the result map.
let hasPageSegment = false;
for (const [segmentPath, buffer] of (await Promise.all(segmentTasks))){
resultMap.set(segmentPath, buffer);
if (segmentPath.endsWith('__PAGE__')) {
hasPageSegment = true;
}
}
if (!hasPageSegment) {
// The build requires at least one segment path ending with __PAGE__ to
// register the catch-all segment data route. When all page segments are
// disabled (e.g. every leaf has runtime prefetching), no __PAGE__ entry
// is emitted. Write a dummy entry with a path that doesn't match any
// real route segment so the client will never request it.
//
// TODO: Remove the __PAGE__ requirement from the build instead of
// working around it here. The invariant is outdated now that segments
// can be disabled.
resultMap.set('/todo-remove-fake-segment/__PAGE__', Buffer.alloc(0));
}
return resultMap;
}
async function collectPrefetchHints(fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest, inlining, shouldAttemptStaticPrefetch) {
// Warm up the module cache, same as collectSegmentData.
try {
await (0, _client.createFromReadableStream)((0, _nodewebstreamshelper.streamFromBuffer)(fullPageDataBuffer), {
findSourceMapURL,
serverConsumerManifest
});
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
} catch {}
// Decode the Flight data to walk the route tree.
const initialRSCPayload = await (0, _client.createFromReadableStream)(createUnclosingPrefetchStream((0, _nodewebstreamshelper.streamFromBuffer)(fullPageDataBuffer)), {
findSourceMapURL,
serverConsumerManifest
});
const flightData = extractFlightData(initialRSCPayload);
if (flightData === null) {
return {
hints: 0,
slots: null
};
}
const { buildId, flightRouterState, seedData, head } = flightData;
// The hints every node starts from. The static-prefetch-attempt hint is
// page-global (the tracking that feeds it is), so it goes on every node,
// non-propagating — the client reads it per segment, and the runtime
// hint merging walks the manifest tree node-by-node.
const baseHints = shouldAttemptStaticPrefetch ? _approutertypes.PrefetchHint.ShouldAttemptStaticPrefetch : 0;
if (inlining === false) {
// Prefetch inlining is disabled: nothing to measure, and no inlining
// bits may be emitted (the client would act on them even though the
// responses aren't bundled). Just mirror the route tree's shape with
// the base hints on every node.
return createUniformHintTree(flightRouterState, baseHints);
}
const { maxSize, maxBundleSize } = inlining;
// Root params are forwarded once at the top level of each segment
// response, same as the page response's own root vary params; the client
// unions them into each segment's set at read time.
const rootVaryParamsIterable = initialRSCPayload.r ?? null;
// The page's staleTime iterable, forwarded into each segment response. When
// Cache Components is off the page carries no `s`, so wrap the eager value.
const staleTimeIterable = initialRSCPayload.s ?? createStaleTimeIterable(staleTime);
// The page's runtime-data-access flag, forwarded into each segment
// response's `needsRuntimeRequest`. This pass only measures sizes, so a
// conservative already-resolved `true` stands in when the page carries
// no `u`.
const needsRuntimeRequest = initialRSCPayload.u ?? Promise.resolve(true);
// This pass only measures gzip sizes for inlining hints, so nothing is
// staged (each response's `a` falls out as the no-shell sentinel, 0), but
// the responses are byte-identical in shape to the real ones — the point
// of measuring. Pre-resolving false is the "nothing staged" release.
const shellStageRelease = Promise.resolve(false);
// Measure the head (metadata/viewport) gzip size so the main traversal
// can decide whether to inline it into a page's bundle.
const [, headBuffer] = await renderSegmentPrefetch(buildId, staleTimeIterable, head, _segmentvalueencoding.HEAD_REQUEST_KEY, initialRSCPayload.h, rootVaryParamsIterable, clientModules, null, // Fallback-ness doesn't affect size, so pass false.
false, needsRuntimeRequest, shellStageRelease);
const headGzipSize = await getGzipSize(headBuffer);
// Mutable accumulator: the first segment that accepts the head sets this
// to true. Once set, subsequent segments skip the check.
const headInlineState = {
inlined: false
};
// Walk the tree with the parent-first, child-decides algorithm.
const { node } = await collectPrefetchHintsImpl(flightRouterState, buildId, staleTimeIterable, seedData, clientModules, _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY, null, baseHints, maxSize, maxBundleSize, headGzipSize, headInlineState, rootVaryParamsIterable, needsRuntimeRequest, shellStageRelease);
if (!headInlineState.inlined) {
// No page could accept the head. Set HeadOutlined on the root so the
// client knows to fetch the head separately.
node.hints |= _approutertypes.PrefetchHint.HeadOutlined;
}
return node;
}
// Measure a segment's gzip size and decide whether it should be inlined.
//
// These hints are computed once during build and never change for the
// lifetime of that deployment. The client can assume that hints delivered as
// part of one request will be the same during a subsequent request, given
// the same build ID. There's no skew to worry about as long as the build
// itself is consistent.
//
// In the Segment Cache, we split page prefetches into multiple requests so
// that each one can be cached and deduped independently. However, some
// segments are small enough that the potential caching benefits are not worth
// the additional network overhead. For these, we inline a parent's data into
// one of its children's responses, avoiding a separate request. The parent
// is inlined into the child (not the other way around) because the parent's
// response is more likely to be shared across multiple pages. The child's
// response is already page-specific, so adding the parent's data there
// doesn't meaningfully reduce deduplication. It's similar to how JS bundlers
// decide whether to inline a module into a chunk.
//
// The algorithm is parent-first, child-decides: the parent measures itself
// and passes its gzip size down. Each child decides whether to accept. A
// child rejects if the parent exceeds maxSize or if accepting would push
// the cumulative inlined bytes past maxBundleSize. This produces
// both ParentInlinedIntoSelf (on the child) and InlinedIntoChild (on the
// parent) in a single pass.
async function collectPrefetchHintsImpl(route, buildId, staleTimeIterable, seedData, clientModules, // TODO: Consider persisting the computed requestKey into the hints output
// so it doesn't need to be recomputed during the build. This might also
// suggest renaming prefetch-hints.json to something like
// segment-manifest.json, since it would contain more than just hints.
requestKey, parentGzipSize, // Hints every node starts from (the page-global static-prefetch-attempt
// hint); the inlining bits computed here are OR'd on top.
baseHints, maxSize, maxBundleSize, headGzipSize, headInlineState, rootVaryParamsIterable, needsRuntimeRequest, shellStageRelease) {
// Check if static prefetching is disabled for this segment
// (prefetch: 'force-disabled' / instant = false). Such segments act as
// transparent pass-throughs in the bundle chain: they contribute zero
// bytes of their own and pass parent data through to children. However,
// they cannot be the terminal of a chain — if no child accepts the parent
// data, the parent cannot be inlined into this segment because there's no
// static response to carry it. See the ParentInlinedIntoSelf check below.
//
// Partial Prefetching segments are NOT disabled even though they may need
// a runtime prefetch: they have static data — emitted unconditionally, so
// the client can attempt a static prefetch before deciding whether the
// runtime request is needed — and are measured and inlined like any other
// segment.
const isStaticPrefetchDisabled = ((route[4] ?? 0) & _approutertypes.StaticPrefetchDisabled) !== 0;
// Render current segment and measure its gzip size. Skip measurement for
// segments with static prefetching disabled since they contribute nothing.
let currentGzipSize = null;
if (!isStaticPrefetchDisabled && seedData !== null) {
const [, buffer] = await renderSegmentPrefetch(buildId, staleTimeIterable, seedData[0], requestKey, seedData[4], rootVaryParamsIterable, clientModules, null, // Size-measurement pass only; fallback-ness is irrelevant here.
false, needsRuntimeRequest, shellStageRelease);
currentGzipSize = await getGzipSize(buffer);
}
// Only offer this segment to its children for inlining if its gzip size
// is below maxSize. Segments with static prefetching disabled have
// nothing to offer (their slot in the bundle is null).
const sizeToInline = currentGzipSize !== null && currentGzipSize < maxSize ? currentGzipSize : null;
// Process children serially (not in parallel) to ensure deterministic
// results. Since this only runs at build time and the rendering is just
// re-encoding cached prerenders, this won't impact build times. Each child
// receives our gzip size and decides whether to inline us. Once a child
// accepts, we stop offering to remaining siblings — the parent is only
// inlined into one child. In parallel routes, this avoids duplicating the
// parent's data across multiple sibling responses.
const children = route[1];
const seedDataChildren = seedData !== null ? seedData[1] : null;
let slots = null;
let didInlineIntoChild = false;
let acceptingChildInlinedBytes = 0;
// Track the smallest inlinedBytes across all children so we know how much
// budget remains along the best path. When our own parent asks whether we
// can accept its data, the parent's bytes would flow through to the child
// with the most remaining headroom.
let smallestChildInlinedBytes = Infinity;
let hasChildren = false;
for(const parallelRouteKey in children){
hasChildren = true;
const childRoute = children[parallelRouteKey];
const childSegment = childRoute[0];
const childSeedData = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] ?? null : null;
const childRequestKey = (0, _segmentvalueencoding.appendSegmentRequestKeyPart)(requestKey, parallelRouteKey, (0, _segmentvalueencoding.createSegmentRequestKeyPart)(childSegment));
// Determine what size to offer children for inlining. Normally we offer
// our own size. But if static prefetching is disabled for this segment,
// it has no data of its own — instead it passes the parent's offer
// through to children. This allows a static grandparent to inline
// through a disabled intermediate segment into a static grandchild.
const sizeToOfferChild = isStaticPrefetchDisabled ? parentGzipSize : sizeToInline;
const childResult = await collectPrefetchHintsImpl(childRoute, buildId, staleTimeIterable, childSeedData, clientModules, childRequestKey, // Once a child has accepted us, stop offering to remaining siblings.
didInlineIntoChild ? null : sizeToOfferChild, baseHints, maxSize, maxBundleSize, headGzipSize, headInlineState, rootVaryParamsIterable, needsRuntimeRequest, shellStageRelease);
if (slots === null) {
slots = {};
}
slots[parallelRouteKey] = childResult.node;
if (childResult.node.hints & _approutertypes.PrefetchHint.ParentInlinedIntoSelf) {
// This child accepted our data — it will include our segment's
// response in its own. No need to track headroom anymore since
// we already know which child we're inlined into.
didInlineIntoChild = true;
acceptingChildInlinedBytes = childResult.inlinedBytes;
} else if (!didInlineIntoChild) {
// Track the child with the most remaining headroom. Used below
// when deciding whether to accept our own parent's data.
if (childResult.inlinedBytes < smallestChildInlinedBytes) {
smallestChildInlinedBytes = childResult.inlinedBytes;
}
}
}
// Leaf segment: no children have consumed any budget yet.
if (!hasChildren) {
smallestChildInlinedBytes = 0;
}
// Mark this segment as InlinedIntoChild if one of its children accepted.
// This means this segment doesn't need its own prefetch response — its
// data is included in the accepting child's response instead.
let hints = baseHints;
if (didInlineIntoChild) {
hints |= _approutertypes.PrefetchHint.InlinedIntoChild;
}
// inlinedBytes represents the total gzipped bytes of parent data inlined
// into the deepest "inlining target" along this branch. It starts at 0 at
// the leaves and grows as parents are inlined going back up the tree. If a
// child accepted us, our size is already counted in that child's value.
let inlinedBytes = didInlineIntoChild ? acceptingChildInlinedBytes : smallestChildInlinedBytes;
// Determine which segment is responsible for the head (metadata/viewport).
//
// The head is assigned to the first page bundle terminal that has budget
// room; otherwise it's outlined as a standalone response (HeadOutlined,
// set by the caller). Head can only be inlined into a page, not a layout,
// because pages may access additional params (e.g. searchParams) that
// layouts cannot. It must be a bundle terminal because only bundle
// terminals emit a static response of their own (the head bundle is
// appended in collectSegmentDataImpl's standalone-task branch) —
// assigning the head to an inlined segment would leave it out of every
// static response.
//
// A disabled segment (prefetch: 'force-disabled' / instant = false) is
// never a valid target — it has no response at all. A Partial Prefetching
// segment, by contrast, DOES have a static response (see
// isStaticPrefetchDisabled above) and is an ordinary candidate. Runtime
// prefetching gets no special treatment here: whether the client will
// actually issue a runtime request can't be known at build time (a static
// prefetch attempt may prove sufficient and skip it), so the head must
// always be reachable through the static responses — inlined into one of
// them, or outlined.
const isBundleTerminal = !didInlineIntoChild && !isStaticPrefetchDisabled;
const segment = route[0];
const isPageSegment = typeof segment === 'string' ? segment === _segment.PAGE_SEGMENT_KEY : segment[0] === _segment.PAGE_SEGMENT_KEY;
if (!headInlineState.inlined && isBundleTerminal && isPageSegment) {
// The head counts against the bundle budget.
if (inlinedBytes + headGzipSize < maxBundleSize) {
hints |= _approutertypes.PrefetchHint.HeadInlinedIntoSelf;
inlinedBytes += headGzipSize;
headInlineState.inlined = true;
}
}
// Decide whether to accept our own parent's data. Conditions:
//
// 1. The parent offered us a size (parentGzipSize is not null). It's null
// when the parent is too large to inline or when this is the root.
//
// 2. The total inlined bytes along this branch wouldn't exceed the budget.
// Even if each segment is individually small, at some point it no
// longer makes sense to keep adding bytes because the combined response
// is unique per URL and can't be deduped.
//
// 3. If this segment has static prefetching disabled, it can only accept
// the parent if it has successfully inlined into a child. A disabled
// segment is a transparent pass-through — it passes parent data through
// to descendants. But if no descendant accepted, there's no static
// response to carry the parent's data, so the parent must remain
// outlined.
//
// A node can be both InlinedIntoChild and ParentInlinedIntoSelf. This
// happens in multi-level chains: GP → P → C where all are small. C
// accepts P (P is InlinedIntoChild), then P also accepts GP (P is
// ParentInlinedIntoSelf). The result: C's response includes both P's
// and GP's data. The parent's data flows through to the deepest
// accepting descendant.
if (parentGzipSize !== null) {
// A disabled segment can only pass through — it needs a child to
// ultimately accept the parent's data.
const canAcceptParent = !isStaticPrefetchDisabled || didInlineIntoChild;
if (canAcceptParent && inlinedBytes + parentGzipSize < maxBundleSize) {
hints |= _approutertypes.PrefetchHint.ParentInlinedIntoSelf;
inlinedBytes += parentGzipSize;
}
}
return {
node: {
hints,
slots
},
inlinedBytes
};
}
// Mirrors the route tree's shape with the same hints on every node. Used by
// collectPrefetchHints when prefetch inlining is disabled: there are no sizes
// to measure, but the static-prefetch-attempt hint still needs a manifest
// tree — the client reads the bit per node, and the runtime hint merging
// (createFlightRouterStateFromLoaderTree) walks the manifest tree in parallel
// with the loader tree, so a bit that's missing from a node never reaches the
// corresponding segment.
function createUniformHintTree(route, hints) {
let slots = null;
const children = route[1];
for(const parallelRouteKey in children){
if (slots === null) {
slots = {};
}
slots[parallelRouteKey] = createUniformHintTree(children[parallelRouteKey], hints);
}
return {
hints,
slots
};
}
// We use gzip size rather than raw size because it better reflects the actual
// transfer cost. The inlining trade-off is about whether the overhead of an
// additional HTTP request (connection setup, headers, round trip) is worth
// the deduplication benefit of keeping a segment separate. Below some
// compressed size, the request overhead dominates and inlining is better.
// Above it, the deduplication benefit of a cacheable standalone response
// wins out.
async function getGzipSize(buffer) {
const stream = new Blob([
new Uint8Array(buffer)
]).stream().pipeThrough(new CompressionStream('gzip'));
const compressedBlob = await new Response(stream).blob();
return compressedBlob.size;
}
async function PrefetchTreeData({ isClientParamParsingEnabled, pageDataStream, serverConsumerManifest, clientModules, staleTime, segmentTasks, onCompletedProcessingRouteTree, prefetchInlining, hints, isUpgradeableISRFallback, runtimeDataAccessed, shellStageRelease }) {
// We're currently rendering a Flight response for the route tree prefetch.
// Inside this component, decode the Flight stream for the whole page. This is
// a hack to transfer the side effects from the original Flight stream (e.g.
// Float preloads) onto the Flight stream for the tree prefetch.
// TODO: React needs a better way to do this. Needed for Server Actions, too.
//
// This is the decode that everything downstream reads from: the route tree
// walk below and, through it, every segment task. When staged, the values
// extracted here hold pending references at the param-dependent holes; the
// shell byte prefix carries the full router tree and seed-data skeleton, so
// the root resolves from the prefix alone (the rest arrives at the release).
const initialRSCPayload = await (0, _client.createFromReadableStream)(pageDataStream, {
findSourceMapURL,
serverConsumerManifest
});
const flightData = extractFlightData(initialRSCPayload);
if (flightData === null) {
return null;
}
const { buildId, flightRouterState, seedData, head } = flightData;
// Root params are forwarded once at the top level of each segment
// response, same as the page response's own root vary params; the client
// unions them into each segment's set at read time.
const rootVaryParamsIterable = initialRSCPayload.r ?? null;
// The page's staleTime iterable, forwarded into each segment response. When
// Cache Components is off the page carries no `s`, so wrap the eager value.
const staleTimeIterable = initialRSCPayload.s ?? createStaleTimeIterable(staleTime);
// The page's runtime-data-access flag, forwarded into each segment
// response's `needsRuntimeRequest`. When the page carries no `u` (e.g.
// legacy render paths), wrap the flag the caller read from the warm-up
// decode in an already-resolved promise.
const needsRuntimeRequest = initialRSCPayload.u ?? Promise.resolve(runtimeDataAccessed);
// Only applies when prefetch inlining is enabled — the client doesn't
// know to look for the head inside a page's response otherwise.
const headIsInlined = prefetchInlining && hints !== null && !(hints.hints & _approutertypes.PrefetchHint.HeadOutlined);
// Compute the route metadata tree by traversing the FlightRouterState. As we
// walk the tree, we will also spawn a task to produce a prefetch response for
// each segment. When prefetch inlining is enabled, small segments are bundled
// into their children's responses based on the hint bits.
const headBundle = headIsInlined ? {
rsc: head,
varyParams: initialRSCPayload.h,
next: null
} : null;
const tree = collectSegmentDataImpl(isClientParamParsingEnabled, flightRouterState, buildId, staleTimeIterable, seedData, clientModules, _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY, segmentTasks, prefetchInlining, hints, null, headBundle, rootVaryParamsIterable, isUpgradeableISRFallback, needsRuntimeRequest, shellStageRelease);
// Spawn a task to produce a prefetch response for the "head" segment,
// unless it was inlined into a page's bundle.
if (!headIsInlined) {
segmentTasks.push((0, _scheduler.waitAtLeastOneReactRenderTask)().then(()=>renderSegmentPrefetch(buildId, staleTimeIterable, head, _segmentvalueencoding.HEAD_REQUEST_KEY, initialRSCPayload.h, rootVaryParamsIterable, clientModules, null, isUpgradeableISRFallback, needsRuntimeRequest, shellStageRelease)));
}
// Notify the abort controller that we're done processing the route tree.
// Anything async that happens after this point must be due to hanging
// promises in the original stream.
onCompletedProcessingRouteTree();
// Render the route tree to a special `/_tree` segment.
const treePrefetch = {
tree,
staleTime
};
if (buildId) {
treePrefetch.buildId = buildId;
}
return treePrefetch;
}
function collectSegmentDataImpl(isClientParamParsingEnabled, route, buildId, staleTimeIterable, seedData, clientModules, requestKey, segmentTasks, prefetchInlining, hintTree, parentBundle, headBundle, rootVaryParamsIterable, isUpgradeableISRFallback, needsRuntimeRequest, shellStageRelease) {
// Union the hints already embedded in the FlightRouterState with the
// separately-computed build-time hints. During the initial build, the
// FlightRouterState was produced before collectPrefetchHints ran, so
// inlining hints (ParentInlinedIntoSelf, InlinedIntoChild) won't be in
// route[4] yet. On subsequent renders the hints are already in the
// FlightRouterState, so the union is idempotent.
//
// Always strip InliningHintsStale from the result. That bit is only
// relevant for the initial RSC payload baked into HTML — the /_tree
// response produced here always has correct hints, so the client should
// never see InliningHintsStale in a /_tree response.
const prefetchHints = ((route[4] ?? 0) | (hintTree !== null ? hintTree.hints : 0)) & ~_approutertypes.PrefetchHint.InliningHintsStale;
// The params this segment's own output varies on, forwarded into its
// response as-is. Root params are forwarded separately, once per response.
const varyParams = seedData !== null ? seedData[4] : null;
// If static prefetching is disabled for this segment
// (prefetch: 'force-disabled' / instant = false), it still participates in
// the bundle chain but with null data. The client will skip creating a
// cache entry for it.
//
// Partial Prefetching segments are NOT disabled even though they may need
// a runtime prefetch: their static data is emitted UNCONDITIONALLY — it
// can't be gated on the ShouldAttemptStaticPrefetch hint, because the
// client walks bundles positionally and the null-slot positions must be
// deterministic from build-time config alone. The client uses the data to
// attempt a static prefetch before deciding whether the segment's runtime
// request is actually needed.
const staticPrefetchDisabled = (prefetchHints & _approutertypes.StaticPrefetchDisabled) !== 0;
const rsc = seedData !== null && !staticPrefetchDisabled ? seedData[0] : null;
// Determine whether this segment's data should be accumulated into a
// child's response (inlining) or spawned as its own task. When inlining
// is disabled, the hint bits may still be set (they're computed at build
// time regardless) but we ignore them — every segment is rendered
// standalone because the client doesn't know how to parse bundled
// responses.
let childBundle = null;
if (prefetchInlining && prefetchHints & _approutertypes.PrefetchHint.InlinedIntoChild) {
// This segment is small enough that its data will be included in one
// of its children's responses. Don't spawn a separate task — prepend
// this segment's data onto the linked list so the accepting child can
// bundle it into its response.
if (seedData !== null) {
childBundle = {
rsc,
varyParams,
next: parentBundle
};
}
} else {
// This segment is not inlined into a child. Spawn a task to render it.
// If it has ParentInlinedIntoSelf, the accumulated parents are included
// in its response. Otherwise parentBundle is null and it renders as a
// standalone single-segment response.
//
// Skip spawning a task if rsc is null (disabled segment) — there's no
// data to serve and the client won't request it.
if (seedData !== null && rsc !== null) {
let bundle = prefetchHints & _approutertypes.PrefetchHint.ParentInlinedIntoSelf ? parentBundle : null;
// If this page accepts the head, append it at the tail of the chain.
if (headBundle !== null && prefetchHints & _approutertypes.PrefetchHint.HeadInlinedIntoSelf) {
headBundle.next = bundle;
bundle = headBundle;
}
segmentTasks.push((0, _scheduler.waitAtLeastOneReactRenderTask)().then(()=>renderSegmentPrefetch(buildId, staleTimeIterable, rsc, requestKey, varyParams, rootVaryParamsIterable, clientModules, bundle, isUpgradeableISRFallback, needsRuntimeRequest, shellStageRelease)));
}
// childBundle stays null — reset the accumulator for children.
}
// Metadata about the segment. Sent as part of the tree prefetch. Null if
// there are no children.
let slotMetadata = null;
const children = route[1];
const seedDataChildren = seedData !== null ? seedData[1] : null;
for(const parallelRouteKey in children){
const childRoute = children[parallelRouteKey];
const childSegment = childRoute[0];
const childSeedData = seedDataChildren !== null ? seedDataChildren[parallelRouteKey] ?? null : null;
const childRequestKey = (0, _segmentvalueencoding.appendSegmentRequestKeyPart)(requestKey, parallelRouteKey, (0, _segmentvalueencoding.createSegmentRequestKeyPart)(childSegment));
const childHintTree = hintTree !== null && hintTree.slots !== null ? hintTree.slots[parallelRouteKey] ?? null : null;
const childTree = collectSegmentDataImpl(isClientParamParsingEnabled, childRoute, buildId, staleTimeIterable, childSeedData, clientModules, childRequestKey, segmentTasks, prefetchInlining, childHintTree, childBundle, headBundle, rootVaryParamsIterable, isUpgradeableISRFallback, needsRuntimeRequest, shellStageRelease);
if (slotMetadata === null) {
slotMetadata = {};
}
slotMetadata[parallelRouteKey] = childTree;
}
const segment = route[0];
let name;
let param;
if (typeof segment === 'string') {
name = segment;
param = null;
} else {
name = segment[0];
param = {
type: segment[2],
// This value is omitted from the prefetch response when cacheComponents
// is enabled.
key: isClientParamParsingEnabled ? null : segment[1],
siblings: segment[3]
};
}
// Metadata about the segment. Sent to the client as part of the
// tree prefetch.
return {
name,
param,
prefetchHints,
slots: slotMetadata
};
}
/**
* Renders one segment prefetch response, and with it a shell byte boundary
* (`a`) — the per-segment analogue of the route-level `a`. The boundary lets a
* client later truncate the buffered response and re-decode the prefix into
* the segment's shell variant (param-dependent content reduced to pending
* references) without a second request.
*
* That boundary is the whole reason this isn't a plain re-serialize. A shell
* is a temporal property of the staged page render, already collapsed by the
* time the page stream is decoded, so it can't be recovered from the settled
* values. Instead the response is serialized against a staged decode of the
* page (the caller drip-feeds it in two stages): the shell rows flush first,
* and the byte count when the rest is released is the boundary.
*
* Uses the streaming renderer, not `prerender`, because the boundary must be
* observed mid-stream — `prerender` only exposes its prelude once finished.
*/ async function renderSegmentPrefetch(buildId, staleTime, rsc, requestKey, varyParams, rootVaryParams, clientModules, bundle, isUpgradeableISRFallback, needsRuntimeRequest, shellStageRelease) {
const streamInfoStage = (0, _promisewithresolvers.createPromiseWithResolvers)();
// Build the data array by walking the bundle list, terminal (requested)
// segment first. Always an array, even for a single segment; the terminal
// always has non-null rsc — disabled segments are skipped by the caller.
const data = [];
let node = {
rsc,
varyParams,
next: bundle
};
while(node !== null){
const elementRsc = node.rsc;
if (elementRsc === null) {
// Static prefetching disabled (prefetch: 'force-disabled'; Partial
// Prefetching segments carry real data): a null placeholder keeps the
// array aligned with the client's bundle list, which skips a cache
// entry for the slot.
data.push(null);
} else {
// We can determine if a segment contains only partial data if it takes
// longer than a task to encode, because dynamic data is encoded as an
// infinite promise. We must do this in a separate Flight prerender from
// the one that actually generates the prefetch stream because we need
// to include the result in the stream itself.
const contentIsComplete = new Promise(async (resolve)=>{
// Wait for the input stream to be fully unblocked before checking if
// the data can be decoded synchronously.
await streamInfoStage.promise;
// If the data is fully static, this will resolve synchronously.
// Otherwise, the promise stays unresolved forever, and so does
// whatever field it's encoded into in the outer response.
await (0, _static.prerender)(elementRsc, clientModules, {
filterStackFrame,
onError () {}
});
resolve();
});
data.push({
rsc: elementRsc,
isPartial: contentIsComplete,
staleTime,
varyParams: node.varyParams
});
}
node = node.next;
}
const responseKey = requestKey === _segmentvalueencoding.ROOT_SEGMENT_REQUEST_KEY ? '/_index' : requestKey;
// `a` (see SegmentPrefetchResponse['a']) is resolved below, once the shell
// stage's bytes have been counted.
let totalByteLength = 0;
const shellByteOffset = (0, _promisewithresolvers.createPromiseWithResolvers)();
// Wrap in the response envelope with the build ID at the top level.
const payload = {
buildId: buildId ?? '',
data,
isUpgradeableISRFallback,
a: shellByteOffset.promise,
rootVaryParams,
needsRuntimeRequest
};
const abortController = new AbortController();
const segmentStream = (0, _server.renderToReadableStream)(payload, clientModules, {
filterStackFrame,
signal: abortController.signal,
onError (error) {
if (abortController.signal.aborted) {
// Expected: aborting the render "errors" every reference that is
// still pending, i.e. the dynamic holes. The corresponding error
// rows are discarded below.
return undefined;
}
return onSegmentPrerenderError(error);
}
});
// Consume the stream as it's emitted, counting bytes so the shell boundary
// can be measured. Reads settle within a microtask of each enqueue, so by
// the time the release resolves (in a task after the renderer's last
// flush), every byte the renderer has emitted has been counted.
const reader = segmentStream.getReader();
const chunksPromise = new Promise(async (resolve)=>{
const chunks = [];
while(true){
const { done, value } = await reader.read();
if (done) {
break;
}
if (abortController.signal.aborted) {
continue;
}
chunks.push(value);
totalByteLength += value.byteLength;
}
resolve(chunks);
});
// The release's value is true when the page's shell is its entire static
// response — then so is every segment's, so `a` is null (no separate shell
// to extract). Otherwise we measure.
const shellIsFullResponse = await shellStageRelease;
// The shell stage is complete: everything the render can emit from the shell
// prefix has flushed, so this is the shell's byte length.
const byteLengthAfterShellStage = totalByteLength;
// Wait one task for the rest of the segment data (past the page's own shell
// boundary) to flush, per the timing rule: one macrotask after the release
// enqueues it into the input decode, the render has emitted all of it.
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
// Resolve `a`: null when the page said its shell is the whole response
// (shellIsFullResponse) — then so is every segment's. Otherwise resolve the
// measured boundary, even if no segment *content* follows it: the
// stage-dependent metadata (`staleTime`, `needsRuntimeRequest`) always
// lands its post-shell values and completion rows after this point, so a
// truncated decode is meaningful for every segment of a staged page. When
// the page wasn't staged at all, the release resolved before any bytes
// flushed and the measured boundary falls out as 0, the "no shell"
// sentinel.
if (shellIsFullResponse) {
shellByteOffset.resolve(null);
} else {
shellByteOffset.resolve(byteLengthAfterShellStage);
}
// Now write the stream metadata (`a`, the `isPartial` promises, and a
// post-shell `needsRuntimeRequest` resolution). This is gated behind
// streamInfoStage so it lands strictly after the boundary measurement
// above — the post-shell values must not count as (or leak into) the
// shell prefix.
streamInfoStage.resolve();
// Wait for the metadata rows to flush before halting — two macrotasks,
// each a distinct hop:
// 1. streamInfoStage unblocks the completeness probe renders; a static
// segment's probe resolves within this task (a partial one never does,
// which is how it stays pending → read as partial).
// 2. those resolutions (and the already-resolved `a`) ping the render,
// which emits their rows; the consumer reads that chunk here.
// Halting after only the first hop would drop the not-yet-flushed metadata.
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
await (0, _scheduler.waitAtLeastOneReactRenderTask)();
// We're done writing, so we can abort the stream.
abortController.abort();
return [
responseKey,
Buffer.concat(await chunksPromise)
];
}
/**
* Reads the page's runtime-data-access flag (the payload's `u`) from a decode
* of the fully-settled page buffer. Because every byte is present, the
* promise's row (if the render emitted one) is already visible on its
* thenable status, so this never blocks — the same trick the client cache
* uses to read staleTime from a buffered response.
*
* - fulfilled: the recorded flag.
* - pending: `false`. A successful render always settles the flag (prerender
* completion resolves `false`), so a pending row can only appear in an
* aborted render's buffer, where it means no access was recorded before
* the abort.
* - rejected: `true`, c