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 { import {
prepareThenableState, prepareThenableState,
trackUsedThenable, trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberThenable.new'; } from './ReactFiberThenable.new';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
}; };
} }
function noop(): void {}
function use<T>(usable: Usable<T>): T { function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') { if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding] // $FlowFixMe[method-unbinding]
@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
// Track the position of the thenable within this fiber. // Track the position of the thenable within this fiber.
const index = thenableIndexCounter; const index = thenableIndexCounter;
thenableIndexCounter += 1; thenableIndexCounter += 1;
return trackUsedThenable(thenable, index);
// 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;
}
}
}
} else if ( } else if (
usable.$$typeof === REACT_CONTEXT_TYPE || usable.$$typeof === REACT_CONTEXT_TYPE ||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE usable.$$typeof === REACT_SERVER_CONTEXT_TYPE

View File

@ -137,7 +137,6 @@ import {now} from './Scheduler';
import { import {
prepareThenableState, prepareThenableState,
trackUsedThenable, trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberThenable.old'; } from './ReactFiberThenable.old';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
}; };
} }
function noop(): void {}
function use<T>(usable: Usable<T>): T { function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') { if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding] // $FlowFixMe[method-unbinding]
@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
// Track the position of the thenable within this fiber. // Track the position of the thenable within this fiber.
const index = thenableIndexCounter; const index = thenableIndexCounter;
thenableIndexCounter += 1; thenableIndexCounter += 1;
return trackUsedThenable(thenable, index);
// 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;
}
}
}
} else if ( } else if (
usable.$$typeof === REACT_CONTEXT_TYPE || usable.$$typeof === REACT_CONTEXT_TYPE ||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE usable.$$typeof === REACT_SERVER_CONTEXT_TYPE

View File

@ -17,8 +17,7 @@ import type {
import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactSharedInternals from 'shared/ReactSharedInternals';
const {ReactCurrentActQueue} = ReactSharedInternals; const {ReactCurrentActQueue} = ReactSharedInternals;
// TODO: Sparse arrays are bad for performance. export opaque type ThenableState = Array<Thenable<any>>;
export opaque type ThenableState = Array<Thenable<any> | void>;
let thenableState: ThenableState | null = null; let thenableState: ThenableState | null = null;
@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
return true; 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) { if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.didUsePromise = true; ReactCurrentActQueue.didUsePromise = true;
} }
@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (thenableState === null) { if (thenableState === null) {
thenableState = [thenable]; thenableState = [thenable];
} else { } 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 // We use an expando to track the status and result of a thenable so that we
@ -80,52 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
// If the thenable doesn't have a status, set it to "pending" and attach // 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. // a listener that will update its status and result when it resolves.
switch (thenable.status) { switch (thenable.status) {
case 'fulfilled': case 'fulfilled': {
case 'rejected': const fulfilledValue: T = thenable.value;
// A thenable that already resolved shouldn't have been thrown, so this is return fulfilledValue;
// unexpected. Suggests a mistake in a userspace data library. Don't track }
// this thenable, because if we keep trying it will likely infinite loop case 'rejected': {
// without ever resolving. const rejectedError = thenable.reason;
// TODO: Log a warning? throw rejectedError;
break; }
default: { default: {
if (typeof thenable.status === 'string') { if (typeof thenable.status === 'string') {
// Only instrument the thenable if the status if not defined. If // Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by // it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending". // some custom userspace implementation. We treat it as "pending".
break; } else {
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
} }
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}
}
export function getPreviouslyUsedThenableAtIndex<T>( // Suspend.
index: number, // TODO: Throwing here is an implementation detail that allows us to
): Thenable<T> | null { // unwind the call stack. But we shouldn't allow it to leak into
if (thenableState !== null) { // userspace. Throw an opaque placeholder value instead of the
const thenable = thenableState[index]; // actual thenable. If it doesn't get captured by the work loop, log
if (thenable !== undefined) { // a warning, because that means something in userspace must have
return thenable; // caught it.
throw thenable;
} }
} }
return null;
} }

View File

@ -17,8 +17,7 @@ import type {
import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactSharedInternals from 'shared/ReactSharedInternals';
const {ReactCurrentActQueue} = ReactSharedInternals; const {ReactCurrentActQueue} = ReactSharedInternals;
// TODO: Sparse arrays are bad for performance. export opaque type ThenableState = Array<Thenable<any>>;
export opaque type ThenableState = Array<Thenable<any> | void>;
let thenableState: ThenableState | null = null; let thenableState: ThenableState | null = null;
@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
return true; 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) { if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.didUsePromise = true; ReactCurrentActQueue.didUsePromise = true;
} }
@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
if (thenableState === null) { if (thenableState === null) {
thenableState = [thenable]; thenableState = [thenable];
} else { } 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 // We use an expando to track the status and result of a thenable so that we
@ -80,52 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
// If the thenable doesn't have a status, set it to "pending" and attach // 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. // a listener that will update its status and result when it resolves.
switch (thenable.status) { switch (thenable.status) {
case 'fulfilled': case 'fulfilled': {
case 'rejected': const fulfilledValue: T = thenable.value;
// A thenable that already resolved shouldn't have been thrown, so this is return fulfilledValue;
// unexpected. Suggests a mistake in a userspace data library. Don't track }
// this thenable, because if we keep trying it will likely infinite loop case 'rejected': {
// without ever resolving. const rejectedError = thenable.reason;
// TODO: Log a warning? throw rejectedError;
break; }
default: { default: {
if (typeof thenable.status === 'string') { if (typeof thenable.status === 'string') {
// Only instrument the thenable if the status if not defined. If // Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by // it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending". // some custom userspace implementation. We treat it as "pending".
break; } else {
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
} }
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}
}
export function getPreviouslyUsedThenableAtIndex<T>( // Suspend.
index: number, // TODO: Throwing here is an implementation detail that allows us to
): Thenable<T> | null { // unwind the call stack. But we shouldn't allow it to leak into
if (thenableState !== null) { // userspace. Throw an opaque placeholder value instead of the
const thenable = thenableState[index]; // actual thenable. If it doesn't get captured by the work loop, log
if (thenable !== undefined) { // a warning, because that means something in userspace must have
return thenable; // caught it.
throw thenable;
} }
} }
return null;
} }

View File

@ -25,11 +25,7 @@ import type {ThenableState} from './ReactFizzThenable';
import {readContext as readContextImpl} from './ReactFizzNewContext'; import {readContext as readContextImpl} from './ReactFizzNewContext';
import {getTreeId} from './ReactFizzTreeContext'; import {getTreeId} from './ReactFizzTreeContext';
import { import {createThenableState, trackUsedThenable} from './ReactFizzThenable';
getPreviouslyUsedThenableAtIndex,
createThenableState,
trackUsedThenable,
} from './ReactFizzThenable';
import {makeId} from './ReactServerFormatConfig'; import {makeId} from './ReactServerFormatConfig';
@ -593,62 +589,10 @@ function use<T>(usable: Usable<T>): T {
const index = thenableIndexCounter; const index = thenableIndexCounter;
thenableIndexCounter += 1; thenableIndexCounter += 1;
// TODO: Unify this switch statement with the one in trackUsedThenable. if (thenableState === null) {
switch (thenable.status) { thenableState = createThenableState();
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 ( } else if (
usable.$$typeof === REACT_CONTEXT_TYPE || usable.$$typeof === REACT_CONTEXT_TYPE ||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE usable.$$typeof === REACT_SERVER_CONTEXT_TYPE

View File

@ -20,8 +20,7 @@ import type {
RejectedThenable, RejectedThenable,
} from 'shared/ReactTypes'; } from 'shared/ReactTypes';
// TODO: Sparse arrays are bad for performance. export opaque type ThenableState = Array<Thenable<any>>;
export opaque type ThenableState = Array<Thenable<any> | void>;
export function createThenableState(): ThenableState { export function createThenableState(): ThenableState {
// The ThenableState is created the first time a component suspends. If it // The ThenableState is created the first time a component suspends. If it
@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
return []; return [];
} }
function noop(): void {}
export function trackUsedThenable<T>( export function trackUsedThenable<T>(
thenableState: ThenableState, thenableState: ThenableState,
thenable: Thenable<T>, thenable: Thenable<T>,
index: number, index: number,
) { ): T {
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 // 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 // can synchronously unwrap the value. Think of this as an extension of the
@ -43,53 +57,48 @@ export function trackUsedThenable<T>(
// If the thenable doesn't have a status, set it to "pending" and attach // 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. // a listener that will update its status and result when it resolves.
switch (thenable.status) { switch (thenable.status) {
case 'fulfilled': case 'fulfilled': {
case 'rejected': const fulfilledValue: T = thenable.value;
// A thenable that already resolved shouldn't have been thrown, so this is return fulfilledValue;
// unexpected. Suggests a mistake in a userspace data library. Don't track }
// this thenable, because if we keep trying it will likely infinite loop case 'rejected': {
// without ever resolving. const rejectedError = thenable.reason;
// TODO: Log a warning? throw rejectedError;
break; }
default: { default: {
if (typeof thenable.status === 'string') { if (typeof thenable.status === 'string') {
// Only instrument the thenable if the status if not defined. If // Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by // it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending". // some custom userspace implementation. We treat it as "pending".
break; } else {
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
} }
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}
}
export function getPreviouslyUsedThenableAtIndex<T>( // Suspend.
thenableState: ThenableState | null, // TODO: Throwing here is an implementation detail that allows us to
index: number, // unwind the call stack. But we shouldn't allow it to leak into
): Thenable<T> | null { // userspace. Throw an opaque placeholder value instead of the
if (thenableState !== null) { // actual thenable. If it doesn't get captured by the work loop, log
const thenable = thenableState[index]; // a warning, because that means something in userspace must have
if (thenable !== undefined) { // caught it.
return thenable; throw thenable;
} }
} }
return null;
} }

View File

@ -17,11 +17,7 @@ import {
} from 'shared/ReactSymbols'; } from 'shared/ReactSymbols';
import {readContext as readContextImpl} from './ReactFlightNewContext'; import {readContext as readContextImpl} from './ReactFlightNewContext';
import {enableUseHook} from 'shared/ReactFeatureFlags'; import {enableUseHook} from 'shared/ReactFeatureFlags';
import { import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
getPreviouslyUsedThenableAtIndex,
createThenableState,
trackUsedThenable,
} from './ReactFlightThenable';
let currentRequest = null; let currentRequest = null;
let thenableIndexCounter = 0; let thenableIndexCounter = 0;
@ -121,8 +117,6 @@ function useId(): string {
return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':'; return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':';
} }
function noop(): void {}
function use<T>(usable: Usable<T>): T { function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') { if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding] // $FlowFixMe[method-unbinding]
@ -134,61 +128,10 @@ function use<T>(usable: Usable<T>): T {
const index = thenableIndexCounter; const index = thenableIndexCounter;
thenableIndexCounter += 1; thenableIndexCounter += 1;
switch (thenable.status) { if (thenableState === null) {
case 'fulfilled': { thenableState = createThenableState();
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) { } else if (usable.$$typeof === REACT_SERVER_CONTEXT_TYPE) {
const context: ReactServerContext<T> = (usable: any); const context: ReactServerContext<T> = (usable: any);
return readContext(context); return readContext(context);

View File

@ -20,8 +20,7 @@ import type {
RejectedThenable, RejectedThenable,
} from 'shared/ReactTypes'; } from 'shared/ReactTypes';
// TODO: Sparse arrays are bad for performance. export opaque type ThenableState = Array<Thenable<any>>;
export opaque type ThenableState = Array<Thenable<any> | void>;
export function createThenableState(): ThenableState { export function createThenableState(): ThenableState {
// The ThenableState is created the first time a component suspends. If it // The ThenableState is created the first time a component suspends. If it
@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
return []; return [];
} }
function noop(): void {}
export function trackUsedThenable<T>( export function trackUsedThenable<T>(
thenableState: ThenableState, thenableState: ThenableState,
thenable: Thenable<T>, thenable: Thenable<T>,
index: number, index: number,
) { ): T {
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 // 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 // can synchronously unwrap the value. Think of this as an extension of the
@ -43,53 +57,48 @@ export function trackUsedThenable<T>(
// If the thenable doesn't have a status, set it to "pending" and attach // 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. // a listener that will update its status and result when it resolves.
switch (thenable.status) { switch (thenable.status) {
case 'fulfilled': case 'fulfilled': {
case 'rejected': const fulfilledValue: T = thenable.value;
// A thenable that already resolved shouldn't have been thrown, so this is return fulfilledValue;
// unexpected. Suggests a mistake in a userspace data library. Don't track }
// this thenable, because if we keep trying it will likely infinite loop case 'rejected': {
// without ever resolving. const rejectedError = thenable.reason;
// TODO: Log a warning? throw rejectedError;
break; }
default: { default: {
if (typeof thenable.status === 'string') { if (typeof thenable.status === 'string') {
// Only instrument the thenable if the status if not defined. If // Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by // it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending". // some custom userspace implementation. We treat it as "pending".
break; } else {
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
} }
const pendingThenable: PendingThenable<mixed> = (thenable: any);
pendingThenable.status = 'pending';
pendingThenable.then(
fulfilledValue => {
if (thenable.status === 'pending') {
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === 'pending') {
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
},
);
break;
}
}
}
export function getPreviouslyUsedThenableAtIndex<T>( // Suspend.
thenableState: ThenableState | null, // TODO: Throwing here is an implementation detail that allows us to
index: number, // unwind the call stack. But we shouldn't allow it to leak into
): Thenable<T> | null { // userspace. Throw an opaque placeholder value instead of the
if (thenableState !== null) { // actual thenable. If it doesn't get captured by the work loop, log
const thenable = thenableState[index]; // a warning, because that means something in userspace must have
if (thenable !== undefined) { // caught it.
return thenable; throw thenable;
} }
} }
return null;
} }