Skip to content

Commit

Permalink
Formalize the Wakeable and Thenable types (#18391)
Browse files Browse the repository at this point in the history
* Formalize the Wakeable and Thenable types

We use two subsets of Promises throughout React APIs. This introduces
the smallest subset - Wakeable. It's the thing that you can throw to
suspend. It's something that can ping.

I also use a shared type for Thenable in the cases where we expect a value
so we can be a bit more rigid with our us of them.

* Make Chunks into Wakeables instead of using native Promises

This value is just going from here to React so we can keep it a lighter
abstraction throughout.

* Renamed thenable to wakeable in variable names
  • Loading branch information
sebmarkbage committed Mar 25, 2020
1 parent a6924d7 commit 64ed221
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 159 deletions.
7 changes: 2 additions & 5 deletions packages/react-cache/src/ReactCache.js
Expand Up @@ -7,15 +7,12 @@
* @flow
*/

import type {Thenable} from 'shared/ReactTypes';

import * as React from 'react';

import {createLRU} from './LRU';

type Thenable<T> = {
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
...
};

type Suspender = {then(resolve: () => mixed, reject: () => mixed): mixed, ...};

type PendingResult = {|
Expand Down
152 changes: 78 additions & 74 deletions packages/react-client/src/ReactFlightClient.js
Expand Up @@ -7,6 +7,7 @@
* @flow
*/

import type {Wakeable} from 'shared/ReactTypes';
import type {BlockComponent, BlockRenderFunction} from 'react/src/ReactBlock';
import type {LazyComponent} from 'react/src/ReactLazy';

Expand Down Expand Up @@ -39,48 +40,62 @@ const PENDING = 0;
const RESOLVED = 1;
const ERRORED = 2;

const CHUNK_TYPE = Symbol('flight.chunk');

type PendingChunk = {|
$$typeof: Symbol,
status: 0,
value: Promise<void>,
resolve: () => void,
|};
type ResolvedChunk<T> = {|
$$typeof: Symbol,
status: 1,
value: T,
resolve: null,
|};
type ErroredChunk = {|
$$typeof: Symbol,
status: 2,
value: Error,
resolve: null,
|};
type Chunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
type PendingChunk = {
_status: 0,
_value: null | Array<() => mixed>,
then(resolve: () => mixed): void,
};
type ResolvedChunk<T> = {
_status: 1,
_value: T,
then(resolve: () => mixed): void,
};
type ErroredChunk = {
_status: 2,
_value: Error,
then(resolve: () => mixed): void,
};
type SomeChunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;

function Chunk(status: any, value: any) {
this._status = status;
this._value = value;
}
Chunk.prototype.then = function<T>(resolve: () => mixed) {
let chunk: SomeChunk<T> = this;
if (chunk._status === PENDING) {
if (chunk._value === null) {
chunk._value = [];
}
chunk._value.push(resolve);
} else {
resolve();
}
};

export type Response<T> = {
partialRow: string,
rootChunk: Chunk<T>,
chunks: Map<number, Chunk<any>>,
rootChunk: SomeChunk<T>,
chunks: Map<number, SomeChunk<any>>,
readRoot(): T,
};

function readRoot<T>(): T {
let response: Response<T> = this;
let rootChunk = response.rootChunk;
if (rootChunk.status === RESOLVED) {
return rootChunk.value;
if (rootChunk._status === RESOLVED) {
return rootChunk._value;
} else if (rootChunk._status === PENDING) {
// eslint-disable-next-line no-throw-literal
throw (rootChunk: Wakeable);
} else {
throw rootChunk.value;
throw rootChunk._value;
}
}

export function createResponse<T>(): Response<T> {
let rootChunk: Chunk<any> = createPendingChunk();
let chunks: Map<number, Chunk<any>> = new Map();
let rootChunk: SomeChunk<any> = createPendingChunk();
let chunks: Map<number, SomeChunk<any>> = new Map();
chunks.set(0, rootChunk);
let response = {
partialRow: '',
Expand All @@ -92,58 +107,48 @@ export function createResponse<T>(): Response<T> {
}

function createPendingChunk(): PendingChunk {
let resolve: () => void = (null: any);
let promise = new Promise(r => (resolve = r));
return {
$$typeof: CHUNK_TYPE,
status: PENDING,
value: promise,
resolve: resolve,
};
return new Chunk(PENDING, null);
}

function createErrorChunk(error: Error): ErroredChunk {
return {
$$typeof: CHUNK_TYPE,
status: ERRORED,
value: error,
resolve: null,
};
return new Chunk(ERRORED, error);
}

function wakeChunk(listeners: null | Array<() => mixed>) {
if (listeners !== null) {
for (let i = 0; i < listeners.length; i++) {
let listener = listeners[i];
listener();
}
}
}

function triggerErrorOnChunk<T>(chunk: Chunk<T>, error: Error): void {
if (chunk.status !== PENDING) {
function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: Error): void {
if (chunk._status !== PENDING) {
// We already resolved. We didn't expect to see this.
return;
}
let resolve = chunk.resolve;
let listeners = chunk._value;
let erroredChunk: ErroredChunk = (chunk: any);
erroredChunk.status = ERRORED;
erroredChunk.value = error;
erroredChunk.resolve = null;
resolve();
erroredChunk._status = ERRORED;
erroredChunk._value = error;
wakeChunk(listeners);
}

function createResolvedChunk<T>(value: T): ResolvedChunk<T> {
return {
$$typeof: CHUNK_TYPE,
status: RESOLVED,
value: value,
resolve: null,
};
return new Chunk(RESOLVED, value);
}

function resolveChunk<T>(chunk: Chunk<T>, value: T): void {
if (chunk.status !== PENDING) {
function resolveChunk<T>(chunk: SomeChunk<T>, value: T): void {
if (chunk._status !== PENDING) {
// We already resolved. We didn't expect to see this.
return;
}
let resolve = chunk.resolve;
let listeners = chunk._value;
let resolvedChunk: ResolvedChunk<T> = (chunk: any);
resolvedChunk.status = RESOLVED;
resolvedChunk.value = value;
resolvedChunk.resolve = null;
resolve();
resolvedChunk._status = RESOLVED;
resolvedChunk._value = value;
wakeChunk(listeners);
}

// Report that any missing chunks in the model is now going to throw this
Expand All @@ -160,16 +165,19 @@ export function reportGlobalError<T>(
});
}

function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
if (maybeChunk == null || (maybeChunk: any).$$typeof !== CHUNK_TYPE) {
function readMaybeChunk<T>(maybeChunk: SomeChunk<T> | T): T {
if (maybeChunk == null || !(maybeChunk instanceof Chunk)) {
// $FlowFixMe
return maybeChunk;
}
let chunk: Chunk<T> = (maybeChunk: any);
if (chunk.status === RESOLVED) {
return chunk.value;
let chunk: SomeChunk<T> = (maybeChunk: any);
if (chunk._status === RESOLVED) {
return chunk._value;
} else if (chunk._status === PENDING) {
// eslint-disable-next-line no-throw-literal
throw (chunk: Wakeable);
} else {
throw chunk.value;
throw chunk._value;
}
}

Expand Down Expand Up @@ -216,14 +224,10 @@ function createElement(type, key, props): React$Element<any> {

type UninitializedBlockPayload<Data> = [
mixed,
ModuleMetaData | Chunk<ModuleMetaData>,
Data | Chunk<Data>,
ModuleMetaData | SomeChunk<ModuleMetaData>,
Data | SomeChunk<Data>,
];

type Thenable<T> = {
then(resolve: (T) => mixed, reject?: (mixed) => mixed): Thenable<any>,
};

function initializeBlock<Props, Data>(
tuple: UninitializedBlockPayload<Data>,
): BlockComponent<Props, Data> {
Expand Down
7 changes: 3 additions & 4 deletions packages/react-devtools-shared/src/devtools/cache.js
Expand Up @@ -7,6 +7,8 @@
* @flow
*/

import type {Thenable} from 'shared/ReactTypes';

import * as React from 'react';
import {createContext} from 'react';

Expand All @@ -20,10 +22,7 @@ import {createContext} from 'react';
// The size of this cache is bounded by how many renders were profiled,
// and it will be fully reset between profiling sessions.

export type Thenable<T> = {
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
...
};
export type {Thenable};

type Suspender = {then(resolve: () => mixed, reject: () => mixed): mixed, ...};

Expand Down
8 changes: 4 additions & 4 deletions packages/react-dom/src/test-utils/ReactTestUtilsAct.js
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
import type {Thenable} from 'shared/ReactTypes';

import * as ReactDOM from 'react-dom';
import ReactSharedInternals from 'shared/ReactSharedInternals';
Expand Down Expand Up @@ -73,7 +73,7 @@ function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
let actingUpdatesScopeDepth = 0;
let didWarnAboutUsingActInProd = false;

function act(callback: () => Thenable) {
function act(callback: () => Thenable<mixed>): Thenable<void> {
if (!__DEV__) {
if (didWarnAboutUsingActInProd === false) {
didWarnAboutUsingActInProd = true;
Expand Down Expand Up @@ -146,7 +146,7 @@ function act(callback: () => Thenable) {
// effects and microtasks in a loop until flushPassiveEffects() === false,
// and cleans up
return {
then(resolve: () => void, reject: (?Error) => void) {
then(resolve, reject) {
called = true;
result.then(
() => {
Expand Down Expand Up @@ -206,7 +206,7 @@ function act(callback: () => Thenable) {

// in the sync case, the returned thenable only warns *if* await-ed
return {
then(resolve: () => void) {
then(resolve) {
if (__DEV__) {
console.error(
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
Expand Down
Expand Up @@ -22,16 +22,11 @@ export function resolveModuleReference<T>(
return moduleData;
}

type Thenable = {
then(resolve: (any) => mixed, reject?: (Error) => mixed): Thenable,
...
};

// The chunk cache contains all the chunks we've preloaded so far.
// If they're still pending they're a thenable. This map also exists
// in Webpack but unfortunately it's not exposed so we have to
// replicate it in user space. null means that it has already loaded.
const chunkCache: Map<string, null | Thenable | Error> = new Map();
const chunkCache: Map<string, null | Promise<any> | Error> = new Map();

// Start preloading the modules since we might need them soon.
// This function doesn't suspend.
Expand Down
30 changes: 15 additions & 15 deletions packages/react-reconciler/src/ReactFiberCommitWork.js
Expand Up @@ -21,7 +21,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {CapturedValue, CapturedError} from './ReactCapturedValue';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
import type {Thenable} from './ReactFiberWorkLoop';
import type {Wakeable} from 'shared/ReactTypes';
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';

import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
Expand Down Expand Up @@ -118,7 +118,7 @@ import {
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
resolveRetryThenable,
resolveRetryWakeable,
markCommitTimeOfFallback,
enqueuePendingPassiveHookEffectMount,
enqueuePendingPassiveHookEffectUnmount,
Expand Down Expand Up @@ -1783,9 +1783,9 @@ function commitSuspenseComponent(finishedWork: Fiber) {
if (enableSuspenseCallback && newState !== null) {
const suspenseCallback = finishedWork.memoizedProps.suspenseCallback;
if (typeof suspenseCallback === 'function') {
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
if (thenables !== null) {
suspenseCallback(new Set(thenables));
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
suspenseCallback(new Set(wakeables));
}
} else if (__DEV__) {
if (suspenseCallback !== undefined) {
Expand Down Expand Up @@ -1827,27 +1827,27 @@ function commitSuspenseHydrationCallbacks(
}

function attachSuspenseRetryListeners(finishedWork: Fiber) {
// If this boundary just timed out, then it will have a set of thenables.
// For each thenable, attach a listener so that when it resolves, React
// If this boundary just timed out, then it will have a set of wakeables.
// For each wakeable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
if (thenables !== null) {
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
thenables.forEach(thenable => {
wakeables.forEach(wakeable => {
// Memoize using the boundary fiber to prevent redundant listeners.
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
if (!retryCache.has(thenable)) {
let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
if (enableSchedulerTracing) {
if (thenable.__reactDoNotTraceInteractions !== true) {
if (wakeable.__reactDoNotTraceInteractions !== true) {
retry = Schedule_tracing_wrap(retry);
}
}
retryCache.add(thenable);
thenable.then(retry, retry);
retryCache.add(wakeable);
wakeable.then(retry, retry);
}
});
}
Expand Down

0 comments on commit 64ed221

Please sign in to comment.