next
Version:
The React Framework
343 lines (342 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
RenderStage: null,
StagedRenderingController: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
RenderStage: function() {
return RenderStage;
},
StagedRenderingController: function() {
return StagedRenderingController;
}
});
const _invarianterror = require("../../shared/lib/invariant-error");
const _promisewithresolvers = require("../../shared/lib/promise-with-resolvers");
var RenderStage = /*#__PURE__*/ function(RenderStage) {
RenderStage[RenderStage["Before"] = 1] = "Before";
RenderStage[RenderStage["EarlyStatic"] = 2] = "EarlyStatic";
RenderStage[RenderStage["Static"] = 3] = "Static";
RenderStage[RenderStage["EarlyRuntime"] = 4] = "EarlyRuntime";
RenderStage[RenderStage["Runtime"] = 5] = "Runtime";
RenderStage[RenderStage["Dynamic"] = 6] = "Dynamic";
RenderStage[RenderStage["Abandoned"] = 7] = "Abandoned";
return RenderStage;
}({});
class StagedRenderingController {
constructor(abortSignal, abandonController, shouldTrackSyncIO){
this.abortSignal = abortSignal;
this.abandonController = abandonController;
this.shouldTrackSyncIO = shouldTrackSyncIO;
this.currentStage = 1;
this.syncInterruptReason = null;
this.staticStageEndTime = Infinity;
this.runtimeStageEndTime = Infinity;
this.staticStageListeners = [];
this.earlyRuntimeStageListeners = [];
this.runtimeStageListeners = [];
this.dynamicStageListeners = [];
this.staticStagePromise = (0, _promisewithresolvers.createPromiseWithResolvers)();
this.earlyRuntimeStagePromise = (0, _promisewithresolvers.createPromiseWithResolvers)();
this.runtimeStagePromise = (0, _promisewithresolvers.createPromiseWithResolvers)();
this.dynamicStagePromise = (0, _promisewithresolvers.createPromiseWithResolvers)();
if (abortSignal) {
abortSignal.addEventListener('abort', ()=>{
// Reject all stage promises that haven't already been resolved.
// If a promise was already resolved via advanceStage, the reject
// is a no-op. The ignoreReject handler suppresses unhandled
// rejection warnings for promises that no one is awaiting.
const { reason } = abortSignal;
this.staticStagePromise.promise.catch(ignoreReject);
this.staticStagePromise.reject(reason);
this.earlyRuntimeStagePromise.promise.catch(ignoreReject);
this.earlyRuntimeStagePromise.reject(reason);
this.runtimeStagePromise.promise.catch(ignoreReject);
this.runtimeStagePromise.reject(reason);
this.dynamicStagePromise.promise.catch(ignoreReject);
this.dynamicStagePromise.reject(reason);
}, {
once: true
});
}
if (abandonController) {
abandonController.signal.addEventListener('abort', ()=>{
this.abandonRender();
}, {
once: true
});
}
}
onStage(stage, callback) {
if (this.currentStage >= stage) {
callback();
} else if (stage === 3) {
this.staticStageListeners.push(callback);
} else if (stage === 4) {
this.earlyRuntimeStageListeners.push(callback);
} else if (stage === 5) {
this.runtimeStageListeners.push(callback);
} else if (stage === 6) {
this.dynamicStageListeners.push(callback);
} else {
// This should never happen
throw Object.defineProperty(new _invarianterror.InvariantError(`Invalid render stage: ${stage}`), "__NEXT_ERROR_CODE", {
value: "E881",
enumerable: false,
configurable: true
});
}
}
shouldTrackSyncInterrupt() {
if (!this.shouldTrackSyncIO) {
return false;
}
switch(this.currentStage){
case 1:
// If we haven't started the render yet, it can't be interrupted.
return false;
case 2:
case 3:
return true;
case 4:
// EarlyRuntime is for runtime-prefetchable segments. Sync IO
// should error because it would abort a runtime prefetch.
return true;
case 5:
// Runtime is for non-prefetchable segments. Sync IO is fine there
// because in practice this segment will never be runtime prefetched
return false;
case 6:
case 7:
return false;
default:
return false;
}
}
syncInterruptCurrentStageWithReason(reason) {
if (this.currentStage === 1) {
return;
}
// If the render has already been abandoned, there's nothing to interrupt.
if (this.currentStage === 7) {
return;
}
// If Sync IO occurs during an abandonable render, we trigger the abandon.
// The abandon listener will call abandonRender which advances through
// stages to let caches fill before marking as Abandoned.
if (this.abandonController) {
this.abandonController.abort();
return;
}
if (this.abortSignal) {
// If this is an abortable render, we capture the interruption reason and stop advancing.
// We don't release any more promises.
// The caller is expected to abort the signal.
this.syncInterruptReason = reason;
this.currentStage = 7;
return;
}
// If we're in a non-abandonable & non-abortable render,
// we need to advance to the Dynamic stage and capture the interruption reason.
// (in dev, this will be the restarted render)
switch(this.currentStage){
case 2:
case 3:
case 4:
{
// EarlyRuntime is for runtime-prefetchable segments. Sync IO here
// means the prefetch would be aborted too early.
this.syncInterruptReason = reason;
this.advanceStage(6);
return;
}
case 5:
{
// canSyncInterrupt returns false for Runtime, so we should
// never get here. Defensive no-op.
return;
}
case 6:
default:
}
}
getSyncInterruptReason() {
return this.syncInterruptReason;
}
getStaticStageEndTime() {
return this.staticStageEndTime;
}
getRuntimeStageEndTime() {
return this.runtimeStageEndTime;
}
abandonRender() {
// 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 next stage, not Dynamic, because
// unblocking the dynamic stage would likely lead to wasted (uncached) IO.
const { currentStage } = this;
switch(currentStage){
case 2:
{
this.resolveStaticStage();
}
// intentional fallthrough
case 3:
{
this.resolveEarlyRuntimeStage();
}
// intentional fallthrough
case 4:
{
this.resolveRuntimeStage();
}
// intentional fallthrough
case 5:
{
this.currentStage = 7;
return;
}
case 6:
case 1:
case 7:
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.resolveStaticStage();
}
if (currentStage < 4 && stage >= 4) {
this.resolveEarlyRuntimeStage();
}
if (currentStage < 5 && stage >= 5) {
this.staticStageEndTime = performance.now() + performance.timeOrigin;
this.resolveRuntimeStage();
}
if (currentStage < 6 && stage >= 6) {
this.runtimeStageEndTime = performance.now() + performance.timeOrigin;
this.resolveDynamicStage();
return;
}
}
/** Fire the `onStage` listeners for the static stage and unblock any promises waiting for it. */ resolveStaticStage() {
const staticListeners = this.staticStageListeners;
for(let i = 0; i < staticListeners.length; i++){
staticListeners[i]();
}
staticListeners.length = 0;
this.staticStagePromise.resolve();
}
/** Fire the `onStage` listeners for the early runtime stage and unblock any promises waiting for it. */ resolveEarlyRuntimeStage() {
const earlyRuntimeListeners = this.earlyRuntimeStageListeners;
for(let i = 0; i < earlyRuntimeListeners.length; i++){
earlyRuntimeListeners[i]();
}
earlyRuntimeListeners.length = 0;
this.earlyRuntimeStagePromise.resolve();
}
/** 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.staticStagePromise.promise;
}
case 4:
{
return this.earlyRuntimeStagePromise.promise;
}
case 5:
{
return this.runtimeStagePromise.promise;
}
case 6:
{
return this.dynamicStagePromise.promise;
}
default:
{
stage;
throw Object.defineProperty(new _invarianterror.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