Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formalize the Wakeable and Thenable types #18391

Merged
merged 3 commits into from Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw this seems like a bug in the no-throw-literal lint. It gets confused by the Flow annotation.

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