next
Version:
The React Framework
251 lines (250 loc) • 10.2 kB
JavaScript
import { InvariantError } from '../../shared/lib/invariant-error';
import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolvers';
export var RenderStage = /*#__PURE__*/ function(RenderStage) {
RenderStage[RenderStage["Before"] = 1] = "Before";
RenderStage[RenderStage["Static"] = 2] = "Static";
RenderStage[RenderStage["Runtime"] = 3] = "Runtime";
RenderStage[RenderStage["Dynamic"] = 4] = "Dynamic";
RenderStage[RenderStage["Abandoned"] = 5] = "Abandoned";
return RenderStage;
}({});
export class StagedRenderingController {
constructor(abortSignal = null, hasRuntimePrefetch){
this.abortSignal = abortSignal;
this.hasRuntimePrefetch = hasRuntimePrefetch;
this.currentStage = 1;
this.staticInterruptReason = null;
this.runtimeInterruptReason = null;
this.staticStageEndTime = Infinity;
this.runtimeStageEndTime = Infinity;
this.runtimeStageListeners = [];
this.dynamicStageListeners = [];
this.runtimeStagePromise = createPromiseWithResolvers();
this.dynamicStagePromise = createPromiseWithResolvers();
this.mayAbandon = false;
if (abortSignal) {
abortSignal.addEventListener('abort', ()=>{
const { reason } = abortSignal;
if (this.currentStage < 3) {
this.runtimeStagePromise.promise.catch(ignoreReject) // avoid unhandled rejections
;
this.runtimeStagePromise.reject(reason);
}
if (this.currentStage < 4 || this.currentStage === 5) {
this.dynamicStagePromise.promise.catch(ignoreReject) // avoid unhandled rejections
;
this.dynamicStagePromise.reject(reason);
}
}, {
once: true
});
this.mayAbandon = true;
}
}
onStage(stage, callback) {
if (this.currentStage >= stage) {
callback();
} else if (stage === 3) {
this.runtimeStageListeners.push(callback);
} else if (stage === 4) {
this.dynamicStageListeners.push(callback);
} else {
// This should never happen
throw Object.defineProperty(new InvariantError(`Invalid render stage: ${stage}`), "__NEXT_ERROR_CODE", {
value: "E881",
enumerable: false,
configurable: true
});
}
}
canSyncInterrupt() {
// If we haven't started the render yet, it can't be interrupted.
if (this.currentStage === 1) {
return false;
}
const boundaryStage = this.hasRuntimePrefetch ? 4 : 3;
return this.currentStage < boundaryStage;
}
syncInterruptCurrentStageWithReason(reason) {
if (this.currentStage === 1) {
return;
}
// If Sync IO occurs during the initial (abandonable) render, we'll retry it,
// so we want a slightly different flow.
// See the implementation of `abandonRenderImpl` for more explanation.
if (this.mayAbandon) {
return this.abandonRenderImpl();
}
// If we're in the final render, we cannot abandon it. We need to advance to the Dynamic stage
// and capture the interruption reason.
switch(this.currentStage){
case 2:
{
this.staticInterruptReason = reason;
this.advanceStage(4);
return;
}
case 3:
{
// We only error for Sync IO in the runtime stage if the route
// is configured to use runtime prefetching.
// We do this to reflect the fact that during a runtime prefetch,
// Sync IO aborts aborts the render.
// Note that `canSyncInterrupt` should prevent us from getting here at all
// if runtime prefetching isn't enabled.
if (this.hasRuntimePrefetch) {
this.runtimeInterruptReason = reason;
this.advanceStage(4);
}
return;
}
case 4:
case 5:
default:
}
}
getStaticInterruptReason() {
return this.staticInterruptReason;
}
getRuntimeInterruptReason() {
return this.runtimeInterruptReason;
}
getStaticStageEndTime() {
return this.staticStageEndTime;
}
getRuntimeStageEndTime() {
return this.runtimeStageEndTime;
}
abandonRender() {
if (!this.mayAbandon) {
throw Object.defineProperty(new InvariantError('`abandonRender` called on a stage controller that cannot be abandoned.'), "__NEXT_ERROR_CODE", {
value: "E938",
enumerable: false,
configurable: true
});
}
this.abandonRenderImpl();
}
abandonRenderImpl() {
// In staged rendering, only the initial render is abandonable.
// We can abandon the initial render if
// 1. We notice a cache miss, and need to wait for caches to fill
// 2. A sync IO error occurs, and the render should be interrupted
// (this might be a lazy intitialization of a module,
// so we still want to restart in this case and see if it still occurs)
// In either case, we'll be doing another render after this one,
// so we only want to unblock the Runtime stage, not Dynamic, because
// unblocking the dynamic stage would likely lead to wasted (uncached) IO.
const { currentStage } = this;
switch(currentStage){
case 2:
{
this.currentStage = 5;
this.resolveRuntimeStage();
return;
}
case 3:
{
this.currentStage = 5;
return;
}
case 4:
case 1:
case 5:
break;
default:
{
currentStage;
}
}
}
advanceStage(stage) {
// If we're already at the target stage or beyond, do nothing.
// (this can happen e.g. if sync IO advanced us to the dynamic stage)
if (stage <= this.currentStage) {
return;
}
let currentStage = this.currentStage;
this.currentStage = stage;
if (currentStage < 3 && stage >= 3) {
this.staticStageEndTime = performance.now() + performance.timeOrigin;
this.resolveRuntimeStage();
}
if (currentStage < 4 && stage >= 4) {
this.runtimeStageEndTime = performance.now() + performance.timeOrigin;
this.resolveDynamicStage();
return;
}
}
/** Fire the `onStage` listeners for the runtime stage and unblock any promises waiting for it. */ resolveRuntimeStage() {
const runtimeListeners = this.runtimeStageListeners;
for(let i = 0; i < runtimeListeners.length; i++){
runtimeListeners[i]();
}
runtimeListeners.length = 0;
this.runtimeStagePromise.resolve();
}
/** Fire the `onStage` listeners for the dynamic stage and unblock any promises waiting for it. */ resolveDynamicStage() {
const dynamicListeners = this.dynamicStageListeners;
for(let i = 0; i < dynamicListeners.length; i++){
dynamicListeners[i]();
}
dynamicListeners.length = 0;
this.dynamicStagePromise.resolve();
}
getStagePromise(stage) {
switch(stage){
case 3:
{
return this.runtimeStagePromise.promise;
}
case 4:
{
return this.dynamicStagePromise.promise;
}
default:
{
stage;
throw Object.defineProperty(new InvariantError(`Invalid render stage: ${stage}`), "__NEXT_ERROR_CODE", {
value: "E881",
enumerable: false,
configurable: true
});
}
}
}
waitForStage(stage) {
return this.getStagePromise(stage);
}
delayUntilStage(stage, displayName, resolvedValue) {
const ioTriggerPromise = this.getStagePromise(stage);
const promise = makeDevtoolsIOPromiseFromIOTrigger(ioTriggerPromise, displayName, resolvedValue);
// Analogously to `makeHangingPromise`, we might reject this promise if the signal is invoked.
// (e.g. in the case where we don't want want the render to proceed to the dynamic stage and abort it).
// We shouldn't consider this an unhandled rejection, so we attach a noop catch handler here to suppress this warning.
if (this.abortSignal) {
promise.catch(ignoreReject);
}
return promise;
}
}
function ignoreReject() {}
// TODO(restart-on-cache-miss): the layering of `delayUntilStage`,
// `makeDevtoolsIOPromiseFromIOTrigger` and and `makeDevtoolsIOAwarePromise`
// is confusing, we should clean it up.
function makeDevtoolsIOPromiseFromIOTrigger(ioTrigger, displayName, resolvedValue) {
// If we create a `new Promise` and give it a displayName
// (with no userspace code above us in the stack)
// React Devtools will use it as the IO cause when determining "suspended by".
// In particular, it should shadow any inner IO that resolved/rejected the promise
// (in case of staged rendering, this will be the `setTimeout` that triggers the relevant stage)
const promise = new Promise((resolve, reject)=>{
ioTrigger.then(resolve.bind(null, resolvedValue), reject);
});
if (displayName !== undefined) {
// @ts-expect-error
promise.displayName = displayName;
}
return promise;
}
//# sourceMappingURL=staged-rendering.js.map