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 {
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue