Unify promise switch statements
There are two different switch statements that we use to unwrap a `use`-ed promise, but there really only needs to be one. This was a factoring artifact that arose because I implemented the yieldy `status` instrumentation thing before I implemented `use` (for promises that are thrown directly during render, which is the old Suspense pattern that will be superseded by `use`).
This commit is contained in:
parent
7572e4931f
commit
fa77f52e74
|
@ -137,7 +137,6 @@ import {now} from './Scheduler';
|
|||
import {
|
||||
prepareThenableState,
|
||||
trackUsedThenable,
|
||||
getPreviouslyUsedThenableAtIndex,
|
||||
} from './ReactFiberThenable.new';
|
||||
|
||||
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
|
||||
|
@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
|
|||
};
|
||||
}
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
function use<T>(usable: Usable<T>): T {
|
||||
if (usable !== null && typeof usable === 'object') {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
|
@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
|
|||
// Track the position of the thenable within this fiber.
|
||||
const index = thenableIndexCounter;
|
||||
thenableIndexCounter += 1;
|
||||
|
||||
// TODO: Unify this switch statement with the one in trackUsedThenable.
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
|
||||
index,
|
||||
);
|
||||
if (prevThenableAtIndex !== null) {
|
||||
if (thenable !== prevThenableAtIndex) {
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
}
|
||||
switch (prevThenableAtIndex.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = prevThenableAtIndex.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError: mixed = prevThenableAtIndex.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
// The thenable still hasn't resolved. Suspend with the same
|
||||
// thenable as last time to avoid redundant listeners.
|
||||
throw prevThenableAtIndex;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is the first time something has been used at this index.
|
||||
// Stash the thenable at the current index so we can reuse it during
|
||||
// the next attempt.
|
||||
trackUsedThenable(thenable, index);
|
||||
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
}
|
||||
return trackUsedThenable(thenable, index);
|
||||
} else if (
|
||||
usable.$$typeof === REACT_CONTEXT_TYPE ||
|
||||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
|
||||
|
|
|
@ -137,7 +137,6 @@ import {now} from './Scheduler';
|
|||
import {
|
||||
prepareThenableState,
|
||||
trackUsedThenable,
|
||||
getPreviouslyUsedThenableAtIndex,
|
||||
} from './ReactFiberThenable.old';
|
||||
|
||||
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
|
||||
|
@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
|
|||
};
|
||||
}
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
function use<T>(usable: Usable<T>): T {
|
||||
if (usable !== null && typeof usable === 'object') {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
|
@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
|
|||
// Track the position of the thenable within this fiber.
|
||||
const index = thenableIndexCounter;
|
||||
thenableIndexCounter += 1;
|
||||
|
||||
// TODO: Unify this switch statement with the one in trackUsedThenable.
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
|
||||
index,
|
||||
);
|
||||
if (prevThenableAtIndex !== null) {
|
||||
if (thenable !== prevThenableAtIndex) {
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
}
|
||||
switch (prevThenableAtIndex.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = prevThenableAtIndex.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError: mixed = prevThenableAtIndex.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
// The thenable still hasn't resolved. Suspend with the same
|
||||
// thenable as last time to avoid redundant listeners.
|
||||
throw prevThenableAtIndex;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is the first time something has been used at this index.
|
||||
// Stash the thenable at the current index so we can reuse it during
|
||||
// the next attempt.
|
||||
trackUsedThenable(thenable, index);
|
||||
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
}
|
||||
return trackUsedThenable(thenable, index);
|
||||
} else if (
|
||||
usable.$$typeof === REACT_CONTEXT_TYPE ||
|
||||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
|
||||
|
|
|
@ -17,8 +17,7 @@ import type {
|
|||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
const {ReactCurrentActQueue} = ReactSharedInternals;
|
||||
|
||||
// TODO: Sparse arrays are bad for performance.
|
||||
export opaque type ThenableState = Array<Thenable<any> | void>;
|
||||
export opaque type ThenableState = Array<Thenable<any>>;
|
||||
|
||||
let thenableState: ThenableState | null = null;
|
||||
|
||||
|
@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
||||
function noop(): void {}
|
||||
|
||||
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
|
||||
if (__DEV__ && ReactCurrentActQueue.current !== null) {
|
||||
ReactCurrentActQueue.didUsePromise = true;
|
||||
}
|
||||
|
@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
|||
if (thenableState === null) {
|
||||
thenableState = [thenable];
|
||||
} else {
|
||||
thenableState[index] = thenable;
|
||||
const previous = thenableState[index];
|
||||
if (previous === undefined) {
|
||||
thenableState.push(thenable);
|
||||
} else {
|
||||
if (previous !== thenable) {
|
||||
// Reuse the previous thenable, and drop the new one. We can assume
|
||||
// they represent the same value, because components are idempotent.
|
||||
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
thenable = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We use an expando to track the status and result of a thenable so that we
|
||||
|
@ -80,21 +94,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
|||
// If the thenable doesn't have a status, set it to "pending" and attach
|
||||
// a listener that will update its status and result when it resolves.
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled':
|
||||
case 'rejected':
|
||||
// A thenable that already resolved shouldn't have been thrown, so this is
|
||||
// unexpected. Suggests a mistake in a userspace data library. Don't track
|
||||
// this thenable, because if we keep trying it will likely infinite loop
|
||||
// without ever resolving.
|
||||
// TODO: Log a warning?
|
||||
break;
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
if (typeof thenable.status === 'string') {
|
||||
// Only instrument the thenable if the status if not defined. If
|
||||
// it's defined, but an unknown value, assume it's been instrumented by
|
||||
// some custom userspace implementation. We treat it as "pending".
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const pendingThenable: PendingThenable<mixed> = (thenable: any);
|
||||
pendingThenable.status = 'pending';
|
||||
pendingThenable.then(
|
||||
|
@ -113,19 +126,16 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
|||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviouslyUsedThenableAtIndex<T>(
|
||||
index: number,
|
||||
): Thenable<T> | null {
|
||||
if (thenableState !== null) {
|
||||
const thenable = thenableState[index];
|
||||
if (thenable !== undefined) {
|
||||
return thenable;
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -17,8 +17,7 @@ import type {
|
|||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
const {ReactCurrentActQueue} = ReactSharedInternals;
|
||||
|
||||
// TODO: Sparse arrays are bad for performance.
|
||||
export opaque type ThenableState = Array<Thenable<any> | void>;
|
||||
export opaque type ThenableState = Array<Thenable<any>>;
|
||||
|
||||
let thenableState: ThenableState | null = null;
|
||||
|
||||
|
@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
||||
function noop(): void {}
|
||||
|
||||
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
|
||||
if (__DEV__ && ReactCurrentActQueue.current !== null) {
|
||||
ReactCurrentActQueue.didUsePromise = true;
|
||||
}
|
||||
|
@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
|||
if (thenableState === null) {
|
||||
thenableState = [thenable];
|
||||
} else {
|
||||
thenableState[index] = thenable;
|
||||
const previous = thenableState[index];
|
||||
if (previous === undefined) {
|
||||
thenableState.push(thenable);
|
||||
} else {
|
||||
if (previous !== thenable) {
|
||||
// Reuse the previous thenable, and drop the new one. We can assume
|
||||
// they represent the same value, because components are idempotent.
|
||||
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
thenable = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We use an expando to track the status and result of a thenable so that we
|
||||
|
@ -80,21 +94,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
|||
// If the thenable doesn't have a status, set it to "pending" and attach
|
||||
// a listener that will update its status and result when it resolves.
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled':
|
||||
case 'rejected':
|
||||
// A thenable that already resolved shouldn't have been thrown, so this is
|
||||
// unexpected. Suggests a mistake in a userspace data library. Don't track
|
||||
// this thenable, because if we keep trying it will likely infinite loop
|
||||
// without ever resolving.
|
||||
// TODO: Log a warning?
|
||||
break;
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
if (typeof thenable.status === 'string') {
|
||||
// Only instrument the thenable if the status if not defined. If
|
||||
// it's defined, but an unknown value, assume it's been instrumented by
|
||||
// some custom userspace implementation. We treat it as "pending".
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const pendingThenable: PendingThenable<mixed> = (thenable: any);
|
||||
pendingThenable.status = 'pending';
|
||||
pendingThenable.then(
|
||||
|
@ -113,19 +126,16 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
|
|||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviouslyUsedThenableAtIndex<T>(
|
||||
index: number,
|
||||
): Thenable<T> | null {
|
||||
if (thenableState !== null) {
|
||||
const thenable = thenableState[index];
|
||||
if (thenable !== undefined) {
|
||||
return thenable;
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -25,11 +25,7 @@ import type {ThenableState} from './ReactFizzThenable';
|
|||
|
||||
import {readContext as readContextImpl} from './ReactFizzNewContext';
|
||||
import {getTreeId} from './ReactFizzTreeContext';
|
||||
import {
|
||||
getPreviouslyUsedThenableAtIndex,
|
||||
createThenableState,
|
||||
trackUsedThenable,
|
||||
} from './ReactFizzThenable';
|
||||
import {createThenableState, trackUsedThenable} from './ReactFizzThenable';
|
||||
|
||||
import {makeId} from './ReactServerFormatConfig';
|
||||
|
||||
|
@ -593,62 +589,10 @@ function use<T>(usable: Usable<T>): T {
|
|||
const index = thenableIndexCounter;
|
||||
thenableIndexCounter += 1;
|
||||
|
||||
// TODO: Unify this switch statement with the one in trackUsedThenable.
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
|
||||
thenableState,
|
||||
index,
|
||||
);
|
||||
if (prevThenableAtIndex !== null) {
|
||||
if (thenable !== prevThenableAtIndex) {
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
}
|
||||
switch (prevThenableAtIndex.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = prevThenableAtIndex.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError: mixed = prevThenableAtIndex.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
// The thenable still hasn't resolved. Suspend with the same
|
||||
// thenable as last time to avoid redundant listeners.
|
||||
throw prevThenableAtIndex;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is the first time something has been used at this index.
|
||||
// Stash the thenable at the current index so we can reuse it during
|
||||
// the next attempt.
|
||||
if (thenableState === null) {
|
||||
thenableState = createThenableState();
|
||||
}
|
||||
trackUsedThenable(thenableState, thenable, index);
|
||||
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
}
|
||||
return trackUsedThenable(thenableState, thenable, index);
|
||||
} else if (
|
||||
usable.$$typeof === REACT_CONTEXT_TYPE ||
|
||||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
|
||||
|
|
|
@ -20,8 +20,7 @@ import type {
|
|||
RejectedThenable,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
// TODO: Sparse arrays are bad for performance.
|
||||
export opaque type ThenableState = Array<Thenable<any> | void>;
|
||||
export opaque type ThenableState = Array<Thenable<any>>;
|
||||
|
||||
export function createThenableState(): ThenableState {
|
||||
// The ThenableState is created the first time a component suspends. If it
|
||||
|
@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
|
|||
return [];
|
||||
}
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
export function trackUsedThenable<T>(
|
||||
thenableState: ThenableState,
|
||||
thenable: Thenable<T>,
|
||||
index: number,
|
||||
) {
|
||||
thenableState[index] = thenable;
|
||||
): T {
|
||||
const previous = thenableState[index];
|
||||
if (previous === undefined) {
|
||||
thenableState.push(thenable);
|
||||
} else {
|
||||
if (previous !== thenable) {
|
||||
// Reuse the previous thenable, and drop the new one. We can assume
|
||||
// they represent the same value, because components are idempotent.
|
||||
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
thenable = previous;
|
||||
}
|
||||
}
|
||||
|
||||
// We use an expando to track the status and result of a thenable so that we
|
||||
// can synchronously unwrap the value. Think of this as an extension of the
|
||||
|
@ -43,21 +57,20 @@ export function trackUsedThenable<T>(
|
|||
// If the thenable doesn't have a status, set it to "pending" and attach
|
||||
// a listener that will update its status and result when it resolves.
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled':
|
||||
case 'rejected':
|
||||
// A thenable that already resolved shouldn't have been thrown, so this is
|
||||
// unexpected. Suggests a mistake in a userspace data library. Don't track
|
||||
// this thenable, because if we keep trying it will likely infinite loop
|
||||
// without ever resolving.
|
||||
// TODO: Log a warning?
|
||||
break;
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
if (typeof thenable.status === 'string') {
|
||||
// Only instrument the thenable if the status if not defined. If
|
||||
// it's defined, but an unknown value, assume it's been instrumented by
|
||||
// some custom userspace implementation. We treat it as "pending".
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const pendingThenable: PendingThenable<mixed> = (thenable: any);
|
||||
pendingThenable.status = 'pending';
|
||||
pendingThenable.then(
|
||||
|
@ -76,20 +89,16 @@ export function trackUsedThenable<T>(
|
|||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviouslyUsedThenableAtIndex<T>(
|
||||
thenableState: ThenableState | null,
|
||||
index: number,
|
||||
): Thenable<T> | null {
|
||||
if (thenableState !== null) {
|
||||
const thenable = thenableState[index];
|
||||
if (thenable !== undefined) {
|
||||
return thenable;
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -17,11 +17,7 @@ import {
|
|||
} from 'shared/ReactSymbols';
|
||||
import {readContext as readContextImpl} from './ReactFlightNewContext';
|
||||
import {enableUseHook} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
getPreviouslyUsedThenableAtIndex,
|
||||
createThenableState,
|
||||
trackUsedThenable,
|
||||
} from './ReactFlightThenable';
|
||||
import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
|
||||
|
||||
let currentRequest = null;
|
||||
let thenableIndexCounter = 0;
|
||||
|
@ -121,8 +117,6 @@ function useId(): string {
|
|||
return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':';
|
||||
}
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
function use<T>(usable: Usable<T>): T {
|
||||
if (usable !== null && typeof usable === 'object') {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
|
@ -134,61 +128,10 @@ function use<T>(usable: Usable<T>): T {
|
|||
const index = thenableIndexCounter;
|
||||
thenableIndexCounter += 1;
|
||||
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
|
||||
thenableState,
|
||||
index,
|
||||
);
|
||||
if (prevThenableAtIndex !== null) {
|
||||
if (thenable !== prevThenableAtIndex) {
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
}
|
||||
switch (prevThenableAtIndex.status) {
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = prevThenableAtIndex.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError: mixed = prevThenableAtIndex.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
// The thenable still hasn't resolved. Suspend with the same
|
||||
// thenable as last time to avoid redundant listeners.
|
||||
throw prevThenableAtIndex;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is the first time something has been used at this index.
|
||||
// Stash the thenable at the current index so we can reuse it during
|
||||
// the next attempt.
|
||||
if (thenableState === null) {
|
||||
thenableState = createThenableState();
|
||||
}
|
||||
trackUsedThenable(thenableState, thenable, index);
|
||||
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
}
|
||||
return trackUsedThenable(thenableState, thenable, index);
|
||||
} else if (usable.$$typeof === REACT_SERVER_CONTEXT_TYPE) {
|
||||
const context: ReactServerContext<T> = (usable: any);
|
||||
return readContext(context);
|
||||
|
|
|
@ -20,8 +20,7 @@ import type {
|
|||
RejectedThenable,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
// TODO: Sparse arrays are bad for performance.
|
||||
export opaque type ThenableState = Array<Thenable<any> | void>;
|
||||
export opaque type ThenableState = Array<Thenable<any>>;
|
||||
|
||||
export function createThenableState(): ThenableState {
|
||||
// The ThenableState is created the first time a component suspends. If it
|
||||
|
@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
|
|||
return [];
|
||||
}
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
export function trackUsedThenable<T>(
|
||||
thenableState: ThenableState,
|
||||
thenable: Thenable<T>,
|
||||
index: number,
|
||||
) {
|
||||
thenableState[index] = thenable;
|
||||
): T {
|
||||
const previous = thenableState[index];
|
||||
if (previous === undefined) {
|
||||
thenableState.push(thenable);
|
||||
} else {
|
||||
if (previous !== thenable) {
|
||||
// Reuse the previous thenable, and drop the new one. We can assume
|
||||
// they represent the same value, because components are idempotent.
|
||||
|
||||
// Avoid an unhandled rejection errors for the Promises that we'll
|
||||
// intentionally ignore.
|
||||
thenable.then(noop, noop);
|
||||
thenable = previous;
|
||||
}
|
||||
}
|
||||
|
||||
// We use an expando to track the status and result of a thenable so that we
|
||||
// can synchronously unwrap the value. Think of this as an extension of the
|
||||
|
@ -43,21 +57,20 @@ export function trackUsedThenable<T>(
|
|||
// If the thenable doesn't have a status, set it to "pending" and attach
|
||||
// a listener that will update its status and result when it resolves.
|
||||
switch (thenable.status) {
|
||||
case 'fulfilled':
|
||||
case 'rejected':
|
||||
// A thenable that already resolved shouldn't have been thrown, so this is
|
||||
// unexpected. Suggests a mistake in a userspace data library. Don't track
|
||||
// this thenable, because if we keep trying it will likely infinite loop
|
||||
// without ever resolving.
|
||||
// TODO: Log a warning?
|
||||
break;
|
||||
case 'fulfilled': {
|
||||
const fulfilledValue: T = thenable.value;
|
||||
return fulfilledValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const rejectedError = thenable.reason;
|
||||
throw rejectedError;
|
||||
}
|
||||
default: {
|
||||
if (typeof thenable.status === 'string') {
|
||||
// Only instrument the thenable if the status if not defined. If
|
||||
// it's defined, but an unknown value, assume it's been instrumented by
|
||||
// some custom userspace implementation. We treat it as "pending".
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const pendingThenable: PendingThenable<mixed> = (thenable: any);
|
||||
pendingThenable.status = 'pending';
|
||||
pendingThenable.then(
|
||||
|
@ -76,20 +89,16 @@ export function trackUsedThenable<T>(
|
|||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviouslyUsedThenableAtIndex<T>(
|
||||
thenableState: ThenableState | null,
|
||||
index: number,
|
||||
): Thenable<T> | null {
|
||||
if (thenableState !== null) {
|
||||
const thenable = thenableState[index];
|
||||
if (thenable !== undefined) {
|
||||
return thenable;
|
||||
// Suspend.
|
||||
// TODO: Throwing here is an implementation detail that allows us to
|
||||
// unwind the call stack. But we shouldn't allow it to leak into
|
||||
// userspace. Throw an opaque placeholder value instead of the
|
||||
// actual thenable. If it doesn't get captured by the work loop, log
|
||||
// a warning, because that means something in userspace must have
|
||||
// caught it.
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue