next
Version:
The React Framework
265 lines (264 loc) • 11.1 kB
JavaScript
import { getVaryParamsAccumulator, workUnitAsyncStorage } from './work-unit-async-storage.external';
/**
* Accumulates vary params for a single segment (or for metadata/rootParams).
*
* A VaryParamsAccumulator is an `AsyncIterable<string>` that can be serialized
* by React Flight. As params are accessed during render, each newly-seen param
* name is `add`ed, which yields it into the Flight stream immediately. After
* rendering, call `close()` (via `finishAccumulatingVaryParams`) to end the
* iteration.
*
* Because each access is flushed into the stream as it happens, the set of
* accessed params is built up incrementally, with no step at the end of the
* render that has to run for the client to read anything. If the prerender is
* aborted by sync I/O, the params yielded before the abort are already in the
* stream — and they're exactly the params the partial response depends on.
* This mirrors how `StaleTimeIterable` works (see stale-time.ts).
*
* Each name is emitted at most once: `add` dedupes against the set of
* already-yielded names, so the stream never contains a duplicate.
*
* NOTE: like `StaleTimeIterable`, this supports a single concurrent iteration
* (Flight iterates it exactly once). The shared empty singleton below is the
* only instance referenced by more than one segment, and it only ever yields
* "done", so concurrent iteration of it is safe.
*/ export class VaryParamsAccumulator {
/**
* Records that a param was accessed. Yields the name into the stream the
* first time it's seen; subsequent accesses of the same name are no-ops.
*/ add(paramName) {
if (this._done || this._seen.has(paramName)) {
return;
}
this._seen.add(paramName);
if (this._resolve !== null) {
this._resolve({
value: paramName,
done: false
});
this._resolve = null;
} else {
this._buffer.push(paramName);
}
}
/** Ends the iteration. Best-effort: if skipped (e.g. on a sync-I/O abort),
* the consumer simply reads the params yielded so far. */ close() {
if (this._done) {
return;
}
this._done = true;
if (this._resolve !== null) {
this._resolve({
value: undefined,
done: true
});
this._resolve = null;
}
}
[Symbol.asyncIterator]() {
return {
next: ()=>{
if (this._buffer.length > 0) {
return Promise.resolve({
value: this._buffer.shift(),
done: false
});
}
if (this._done) {
return Promise.resolve({
value: undefined,
done: true
});
}
return new Promise((resolve)=>{
this._resolve = resolve;
});
}
};
}
constructor(){
this._resolve = null;
this._done = false;
this._buffer = [];
// The set of param names already yielded. Doubles as the dedupe guard so the
// same name is never emitted twice.
this._seen = new Set();
}
}
/**
* A singleton accumulator that's already closed with no params. Use this for
* segments where we know upfront that no params will be accessed, such as
* client components or segments without user code.
*
* Benefits:
* - No need to accumulate or close later
* - Resilient: reads as an empty set even if other tracking fails
* - Memory efficient: reuses the same object
*
* It's never added to `ResponseVaryParamsAccumulator.segments` (callers pass it
* directly), so `finishAccumulatingVaryParams` doesn't touch it.
*/ export const emptyVaryParamsAccumulator = new VaryParamsAccumulator();
emptyVaryParamsAccumulator.close();
export function createResponseVaryParamsAccumulator() {
// Create the head and rootParams accumulators as top-level fields.
// Segment accumulators are added to the segments set as they are created.
const head = new VaryParamsAccumulator();
const rootParams = new VaryParamsAccumulator();
const segments = new Set();
return {
head,
rootParams,
segments
};
}
/**
* Allocates a new VaryParamsAccumulator and adds it to the response accumulator
* associated with the current WorkUnitStore.
*
* Returns an iterable that yields the segment's vary params as they're
* accessed. The iterable can be passed directly to React Flight for
* serialization.
*/ export function createVaryParamsAccumulator() {
const workUnitStore = workUnitAsyncStorage.getStore();
if (!workUnitStore) {
return null;
}
const responseAccumulator = getVaryParamsAccumulator(workUnitStore);
if (!responseAccumulator) {
return null;
}
const accumulator = new VaryParamsAccumulator();
responseAccumulator.segments.add(accumulator);
return accumulator;
}
export function getMetadataVaryParamsAccumulator() {
var _getVaryParamsAccumulator;
const workUnitStore = workUnitAsyncStorage.getStore();
if (!workUnitStore) {
return null;
}
return ((_getVaryParamsAccumulator = getVaryParamsAccumulator(workUnitStore)) == null ? void 0 : _getVaryParamsAccumulator.head) ?? null;
}
// The metadata and viewport are always delivered in a single payload, so they
// don't need to be tracked separately. This may change in the future, but for
// now this is just an alias.
export const getViewportVaryParamsAccumulator = getMetadataVaryParamsAccumulator;
/**
* Returns the response-level root params iterable for serialization. Root
* params are emitted once at the top level (not folded into every segment);
* the client unions them into each segment's set.
*/ export function getRootParamsVaryParamsAccumulator() {
var _getVaryParamsAccumulator;
const workUnitStore = workUnitAsyncStorage.getStore();
if (!workUnitStore) {
return null;
}
return ((_getVaryParamsAccumulator = getVaryParamsAccumulator(workUnitStore)) == null ? void 0 : _getVaryParamsAccumulator.rootParams) ?? null;
}
/**
* Records that a param was accessed. Adds the param name to the accumulator.
*/ export function accumulateVaryParam(accumulator, paramName) {
accumulator.add(paramName);
}
/**
* Records a root param access.
*/ export function accumulateRootVaryParam(paramName) {
const rootParamsAccumulator = getRootParamsVaryParamsAccumulator();
if (rootParamsAccumulator !== null) {
accumulateVaryParam(rootParamsAccumulator, paramName);
}
}
export function createVaryingParams(accumulator, originalParamsObject, optionalCatchAllParamName) {
if (optionalCatchAllParamName !== null) {
// When there's an optional catch-all param with no value (e.g.,
// [[...slug]] at /), the param doesn't exist as a property on the params
// object. Use a Proxy to track all param access — both existing params
// and the missing optional param — including enumeration patterns like
// Object.keys(), spread, for...in, and `in` checks.
return new Proxy(originalParamsObject, {
get (target, prop, receiver) {
if (typeof prop === 'string') {
if (prop === optionalCatchAllParamName || Object.prototype.hasOwnProperty.call(target, prop)) {
accumulateVaryParam(accumulator, prop);
}
}
return Reflect.get(target, prop, receiver);
},
has (target, prop) {
if (prop === optionalCatchAllParamName) {
accumulateVaryParam(accumulator, optionalCatchAllParamName);
}
return Reflect.has(target, prop);
},
ownKeys (target) {
// Enumerating the params object means the user's code may depend on
// which params are present, so conservatively track the optional
// param as accessed.
accumulateVaryParam(accumulator, optionalCatchAllParamName);
return Reflect.ownKeys(target);
}
});
}
// When there's no optional catch-all, all params exist as properties on the
// object, so we can use defineProperty getters instead of a Proxy. This is
// faster because the engine can optimize property access on regular objects
// more aggressively than Proxy trap calls.
const underlyingParamsWithVarying = {};
for(const paramName in originalParamsObject){
Object.defineProperty(underlyingParamsWithVarying, paramName, {
get () {
accumulateVaryParam(accumulator, paramName);
return originalParamsObject[paramName];
},
enumerable: true
});
}
return underlyingParamsWithVarying;
}
export function createVaryingSearchParams(accumulator, originalSearchParamsObject) {
// Search params have no fixed schema, so any access — missing-key reads, `in`
// checks, or enumeration — must register as varying. A Proxy is required
// (rather than per-property getters) so that enumeration of an empty
// searchParams object still triggers a vary. All accesses bucket into the
// single sentinel '?'; the segment is keyed by the whole query string.
// TODO: Split into per-param tracking if the cache key evolves.
return new Proxy(originalSearchParamsObject, {
get (target, prop, receiver) {
if (typeof prop === 'string') {
accumulateVaryParam(accumulator, '?');
}
return Reflect.get(target, prop, receiver);
},
has (target, prop) {
if (typeof prop === 'string') {
accumulateVaryParam(accumulator, '?');
}
return Reflect.has(target, prop);
},
ownKeys (target) {
accumulateVaryParam(accumulator, '?');
return Reflect.ownKeys(target);
}
});
}
/**
* Closes all accumulators in a ResponseVaryParamsAccumulator, ending their
* iterables. Call this after rendering is complete.
*
* This does NOT merge root params into each segment — root params are
* serialized separately (at the top level of the response) and unioned in by
* the client. And it's best-effort: if it's skipped because the render was
* aborted by sync I/O, the consumer just reads the params each iterable yielded
* before the abort.
*
* If we can't track vary params (e.g., legacy prerender), simply don't call
* this function - the client treats a missing iterable as "unknown" vary
* params.
*/ export function finishAccumulatingVaryParams(responseAccumulator) {
responseAccumulator.head.close();
responseAccumulator.rootParams.close();
for (const segmentAccumulator of responseAccumulator.segments){
segmentAccumulator.close();
}
}
//# sourceMappingURL=vary-params.js.map