.
This commit is contained in:
+293
@@ -0,0 +1,293 @@
|
||||
import { workUnitAsyncStorage } from './work-unit-async-storage.external';
|
||||
function createSegmentVaryParamsAccumulator() {
|
||||
const accumulator = {
|
||||
varyParams: new Set(),
|
||||
status: 'pending',
|
||||
value: new Set(),
|
||||
then (onfulfilled) {
|
||||
if (onfulfilled) {
|
||||
if (accumulator.status === 'pending') {
|
||||
accumulator.resolvers.push(onfulfilled);
|
||||
} else {
|
||||
onfulfilled(accumulator.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
resolvers: []
|
||||
};
|
||||
return accumulator;
|
||||
}
|
||||
/**
|
||||
* A singleton accumulator that's already resolved to an empty Set. 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 resolve later
|
||||
* - Resilient: resolves correctly even if other tracking fails
|
||||
* - Memory efficient: reuses the same object
|
||||
*/ const emptySet = new Set();
|
||||
export const emptyVaryParamsAccumulator = {
|
||||
varyParams: emptySet,
|
||||
status: 'fulfilled',
|
||||
value: emptySet,
|
||||
then (onfulfilled) {
|
||||
if (onfulfilled) {
|
||||
onfulfilled(emptySet);
|
||||
}
|
||||
},
|
||||
resolvers: []
|
||||
};
|
||||
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 = createSegmentVaryParamsAccumulator();
|
||||
const rootParams = createSegmentVaryParamsAccumulator();
|
||||
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 a thenable that resolves to the segment's vary params once rendering
|
||||
* is complete. The thenable can be passed directly to React Flight for
|
||||
* serialization.
|
||||
*/ export function createVaryParamsAccumulator() {
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'prerender':
|
||||
case 'prerender-runtime':
|
||||
{
|
||||
const responseAccumulator = workUnitStore.varyParamsAccumulator;
|
||||
if (responseAccumulator !== null) {
|
||||
const accumulator = createSegmentVaryParamsAccumulator();
|
||||
responseAccumulator.segments.add(accumulator);
|
||||
return accumulator;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'prerender-ppr':
|
||||
case 'prerender-legacy':
|
||||
case 'request':
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'prerender-client':
|
||||
case 'validation-client':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
break;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export function getMetadataVaryParamsAccumulator() {
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'prerender':
|
||||
case 'prerender-runtime':
|
||||
{
|
||||
const responseAccumulator = workUnitStore.varyParamsAccumulator;
|
||||
if (responseAccumulator !== null) {
|
||||
return responseAccumulator.head;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'prerender-ppr':
|
||||
case 'prerender-legacy':
|
||||
case 'request':
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'prerender-client':
|
||||
case 'validation-client':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
return null;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export function getVaryParamsThenable(accumulator) {
|
||||
return accumulator;
|
||||
}
|
||||
export function getMetadataVaryParamsThenable() {
|
||||
const accumulator = getMetadataVaryParamsAccumulator();
|
||||
if (accumulator !== null) {
|
||||
return getVaryParamsThenable(accumulator);
|
||||
}
|
||||
return 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;
|
||||
export function getRootParamsVaryParamsAccumulator() {
|
||||
const workUnitStore = workUnitAsyncStorage.getStore();
|
||||
if (workUnitStore) {
|
||||
switch(workUnitStore.type){
|
||||
case 'prerender':
|
||||
case 'prerender-runtime':
|
||||
{
|
||||
const responseAccumulator = workUnitStore.varyParamsAccumulator;
|
||||
if (responseAccumulator !== null) {
|
||||
return responseAccumulator.rootParams;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'prerender-ppr':
|
||||
case 'prerender-legacy':
|
||||
case 'request':
|
||||
case 'cache':
|
||||
case 'private-cache':
|
||||
case 'prerender-client':
|
||||
case 'validation-client':
|
||||
case 'unstable-cache':
|
||||
case 'generate-static-params':
|
||||
return null;
|
||||
default:
|
||||
workUnitStore;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Records that a param was accessed. Adds the param name to the accumulator's
|
||||
* varyParams set.
|
||||
*/ export function accumulateVaryParam(accumulator, paramName) {
|
||||
accumulator.varyParams.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) {
|
||||
const underlyingSearchParamsWithVarying = {};
|
||||
for(const searchParamName in originalSearchParamsObject){
|
||||
Object.defineProperty(underlyingSearchParamsWithVarying, searchParamName, {
|
||||
get () {
|
||||
// TODO: Unlike path params, we don't vary track each search param
|
||||
// individually. The entire search string is treated as a single param.
|
||||
// This may change in the future.
|
||||
accumulateVaryParam(accumulator, '?');
|
||||
return originalSearchParamsObject[searchParamName];
|
||||
},
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
return underlyingSearchParamsWithVarying;
|
||||
}
|
||||
/**
|
||||
* Resolves all segment accumulators in a ResponseVaryParamsAccumulator with
|
||||
* their final vary params. Call this after rendering is complete.
|
||||
*
|
||||
* Each segment's thenable is resolved with its vary params merged with the
|
||||
* root params. If we can't track vary params (e.g., legacy prerender), simply
|
||||
* don't call this function - the client treats unresolved thenables as
|
||||
* "unknown" vary params.
|
||||
*/ export async function finishAccumulatingVaryParams(responseAccumulator) {
|
||||
const rootVaryParams = responseAccumulator.rootParams.varyParams;
|
||||
// Resolve head
|
||||
finishSegmentAccumulator(responseAccumulator.head, rootVaryParams);
|
||||
// Resolve each segment
|
||||
for (const segmentAccumulator of responseAccumulator.segments){
|
||||
finishSegmentAccumulator(segmentAccumulator, rootVaryParams);
|
||||
}
|
||||
// Now that the thenables are resolved, Flight should be able to flush the
|
||||
// vary params into the response stream. This work gets scheduled internally
|
||||
// by Flight using a microtask as soon as we notify the thenable listeners.
|
||||
//
|
||||
// We need to ensure that Flight's pending queues are emptied before this
|
||||
// function returns; the caller will abort the prerender immediately after.
|
||||
// We can't use a macrotask, because that would allow dynamic IO to sneak
|
||||
// into the response. So we use microtasks instead.
|
||||
//
|
||||
// The exact number of awaits here isn't important (indeed, one seems to be
|
||||
// sufficient, at the time of writing), as long as we wait enough ticks for
|
||||
// Flight to finish writing the response.
|
||||
//
|
||||
// Anything that remains in Flight's internal queue after these awaits must
|
||||
// be actual dynamic IO, not caused by pending vary params tasks. In other
|
||||
// words, failing to do this would cause us to treat a fully static prerender
|
||||
// as if it were partially dynamic.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
function finishSegmentAccumulator(accumulator, rootVaryParams) {
|
||||
if (accumulator.status !== 'pending') {
|
||||
return;
|
||||
}
|
||||
const merged = new Set(accumulator.varyParams);
|
||||
for (const param of rootVaryParams){
|
||||
merged.add(param);
|
||||
}
|
||||
accumulator.value = merged;
|
||||
accumulator.status = 'fulfilled';
|
||||
for (const resolver of accumulator.resolvers){
|
||||
resolver(merged);
|
||||
}
|
||||
accumulator.resolvers = [];
|
||||
}
|
||||
|
||||
//# sourceMappingURL=vary-params.js.map
|
||||
Reference in New Issue
Block a user