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:
Andrew Clark 2022-10-22 20:39:19 -04:00
parent 7572e4931f
commit fa77f52e74
8 changed files with 222 additions and 407 deletions

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
}