import type {
Thenable,
PendingThenable,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';
import type {ComponentStackNode} from './ReactFizzComponentStack';
import noop from 'shared/noop';
import {currentTaskInDEV} from './ReactFizzCurrentTask';
export opaque type ThenableState = Array<Thenable<any>>;
export const SuspenseException: mixed = new Error(
"Suspense Exception: This is not a real error! It's an implementation " +
'detail of `use` to interrupt the current render. You must either ' +
'rethrow it immediately, or move the `use` call outside of the ' +
'`try/catch` block. Capturing without rethrowing will lead to ' +
'unexpected behavior.\n\n' +
'To handle async errors, wrap your component in an error boundary, or ' +
"call the promise's `.catch` method and pass the result to `use`.",
);
export function createThenableState(): ThenableState {
return [];
}
export function trackUsedThenable<T>(
thenableState: ThenableState,
thenable: Thenable<T>,
index: number,
): T {
const previous = thenableState[index];
if (previous === undefined) {
thenableState.push(thenable);
} else {
if (previous !== thenable) {
thenable.then(noop, noop);
thenable = previous;
}
}
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
throw rejectedError;
}
default: {
if (typeof thenable.status === 'string') {
thenable.then(noop, noop);
} else {
const pendingThenable: PendingThenable<T> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
}
switch ((thenable: Thenable<T>).status) {
case 'fulfilled': {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case 'rejected': {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
throw rejectedThenable.reason;
}
}
suspendedThenable = thenable;
if (__DEV__ && shouldCaptureSuspendedCallSite) {
captureSuspendedCallSite();
}
throw SuspenseException;
}
}
}
export function readPreviousThenable<T>(
thenableState: ThenableState,
index: number,
): void | T {
const previous = thenableState[index];
if (previous === undefined) {
return undefined;
} else {
return (previous: any).value;
}
}
let suspendedThenable: Thenable<any> | null = null;
export function getSuspendedThenable(): Thenable<mixed> {
if (suspendedThenable === null) {
throw new Error(
'Expected a suspended thenable. This is a bug in React. Please file ' +
'an issue.',
);
}
const thenable = suspendedThenable;
suspendedThenable = null;
return thenable;
}
let shouldCaptureSuspendedCallSite: boolean = false;
export function setCaptureSuspendedCallSiteDEV(capture: boolean): void {
if (!__DEV__) {
throw new Error(
'setCaptureSuspendedCallSiteDEV was called in a production environment. ' +
'This is a bug in React.',
);
}
shouldCaptureSuspendedCallSite = capture;
}
let suspendedCallSiteStack: ComponentStackNode | null = null;
let suspendedCallSiteDebugTask: ConsoleTask | null = null;
function captureSuspendedCallSite(): void {
const currentTask = currentTaskInDEV;
if (currentTask === null) {
throw new Error(
'Expected to have a current task when tracking a suspend call site. ' +
'This is a bug in React.',
);
}
const currentComponentStack = currentTask.componentStack;
if (currentComponentStack === null) {
throw new Error(
'Expected to have a component stack on the current task when ' +
'tracking a suspended call site. This is a bug in React.',
);
}
suspendedCallSiteStack = {
parent: currentComponentStack.parent,
type: currentComponentStack.type,
owner: currentComponentStack.owner,
stack: Error('react-stack-top-frame'),
};
suspendedCallSiteDebugTask = currentTask.debugTask;
}
export function getSuspendedCallSiteStackDEV(): ComponentStackNode | null {
if (__DEV__) {
if (suspendedCallSiteStack === null) {
return null;
}
const callSite = suspendedCallSiteStack;
suspendedCallSiteStack = null;
return callSite;
} else {
throw new Error(
'getSuspendedCallSiteDEV was called in a production environment. ' +
'This is a bug in React.',
);
}
}
export function getSuspendedCallSiteDebugTaskDEV(): ConsoleTask | null {
if (__DEV__) {
if (suspendedCallSiteDebugTask === null) {
return null;
}
const debugTask = suspendedCallSiteDebugTask;
suspendedCallSiteDebugTask = null;
return debugTask;
} else {
throw new Error(
'getSuspendedCallSiteDebugTaskDEV was called in a production environment. ' +
'This is a bug in React.',
);
}
}
export function ensureSuspendableThenableStateDEV(
thenableState: ThenableState,
): () => void {
if (__DEV__) {
const lastThenable = thenableState[thenableState.length - 1];
switch (lastThenable.status) {
case 'fulfilled':
const previousThenableValue = lastThenable.value;
const previousThenableThen = lastThenable.then.bind(lastThenable);
delete lastThenable.value;
delete (lastThenable: any).status;
lastThenable.then = noop;
return () => {
lastThenable.then = previousThenableThen;
lastThenable.value = previousThenableValue;
lastThenable.status = 'fulfilled';
};
case 'rejected':
const previousThenableReason = lastThenable.reason;
delete lastThenable.reason;
delete (lastThenable: any).status;
return () => {
lastThenable.reason = previousThenableReason;
lastThenable.status = 'rejected';
};
}
return noop;
} else {
throw new Error(
'ensureSuspendableThenableStateDEV was called in a production environment. ' +
'This is a bug in React.',
);
}
}