Skip to content

Commit 64ed221

Browse files
authored
Formalize the Wakeable and Thenable types (facebook#18391)
* 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
1 parent a6924d7 commit 64ed221

File tree

13 files changed

+160
-159
lines changed

13 files changed

+160
-159
lines changed

packages/react-cache/src/ReactCache.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77
* @flow
88
*/
99

10+
import type {Thenable} from 'shared/ReactTypes';
11+
1012
import * as React from 'react';
1113

1214
import {createLRU} from './LRU';
1315

14-
type Thenable<T> = {
15-
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
16-
...
17-
};
18-
1916
type Suspender = {then(resolve: () => mixed, reject: () => mixed): mixed, ...};
2017

2118
type PendingResult = {|

packages/react-client/src/ReactFlightClient.js

Lines changed: 78 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import type {Wakeable} from 'shared/ReactTypes';
1011
import type {BlockComponent, BlockRenderFunction} from 'react/src/ReactBlock';
1112
import type {LazyComponent} from 'react/src/ReactLazy';
1213

@@ -39,48 +40,62 @@ const PENDING = 0;
3940
const RESOLVED = 1;
4041
const ERRORED = 2;
4142

42-
const CHUNK_TYPE = Symbol('flight.chunk');
43-
44-
type PendingChunk = {|
45-
$$typeof: Symbol,
46-
status: 0,
47-
value: Promise<void>,
48-
resolve: () => void,
49-
|};
50-
type ResolvedChunk<T> = {|
51-
$$typeof: Symbol,
52-
status: 1,
53-
value: T,
54-
resolve: null,
55-
|};
56-
type ErroredChunk = {|
57-
$$typeof: Symbol,
58-
status: 2,
59-
value: Error,
60-
resolve: null,
61-
|};
62-
type Chunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
43+
type PendingChunk = {
44+
_status: 0,
45+
_value: null | Array<() => mixed>,
46+
then(resolve: () => mixed): void,
47+
};
48+
type ResolvedChunk<T> = {
49+
_status: 1,
50+
_value: T,
51+
then(resolve: () => mixed): void,
52+
};
53+
type ErroredChunk = {
54+
_status: 2,
55+
_value: Error,
56+
then(resolve: () => mixed): void,
57+
};
58+
type SomeChunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
59+
60+
function Chunk(status: any, value: any) {
61+
this._status = status;
62+
this._value = value;
63+
}
64+
Chunk.prototype.then = function<T>(resolve: () => mixed) {
65+
let chunk: SomeChunk<T> = this;
66+
if (chunk._status === PENDING) {
67+
if (chunk._value === null) {
68+
chunk._value = [];
69+
}
70+
chunk._value.push(resolve);
71+
} else {
72+
resolve();
73+
}
74+
};
6375

6476
export type Response<T> = {
6577
partialRow: string,
66-
rootChunk: Chunk<T>,
67-
chunks: Map<number, Chunk<any>>,
78+
rootChunk: SomeChunk<T>,
79+
chunks: Map<number, SomeChunk<any>>,
6880
readRoot(): T,
6981
};
7082

7183
function readRoot<T>(): T {
7284
let response: Response<T> = this;
7385
let rootChunk = response.rootChunk;
74-
if (rootChunk.status === RESOLVED) {
75-
return rootChunk.value;
86+
if (rootChunk._status === RESOLVED) {
87+
return rootChunk._value;
88+
} else if (rootChunk._status === PENDING) {
89+
// eslint-disable-next-line no-throw-literal
90+
throw (rootChunk: Wakeable);
7691
} else {
77-
throw rootChunk.value;
92+
throw rootChunk._value;
7893
}
7994
}
8095

8196
export function createResponse<T>(): Response<T> {
82-
let rootChunk: Chunk<any> = createPendingChunk();
83-
let chunks: Map<number, Chunk<any>> = new Map();
97+
let rootChunk: SomeChunk<any> = createPendingChunk();
98+
let chunks: Map<number, SomeChunk<any>> = new Map();
8499
chunks.set(0, rootChunk);
85100
let response = {
86101
partialRow: '',
@@ -92,58 +107,48 @@ export function createResponse<T>(): Response<T> {
92107
}
93108

94109
function createPendingChunk(): PendingChunk {
95-
let resolve: () => void = (null: any);
96-
let promise = new Promise(r => (resolve = r));
97-
return {
98-
$$typeof: CHUNK_TYPE,
99-
status: PENDING,
100-
value: promise,
101-
resolve: resolve,
102-
};
110+
return new Chunk(PENDING, null);
103111
}
104112

105113
function createErrorChunk(error: Error): ErroredChunk {
106-
return {
107-
$$typeof: CHUNK_TYPE,
108-
status: ERRORED,
109-
value: error,
110-
resolve: null,
111-
};
114+
return new Chunk(ERRORED, error);
115+
}
116+
117+
function wakeChunk(listeners: null | Array<() => mixed>) {
118+
if (listeners !== null) {
119+
for (let i = 0; i < listeners.length; i++) {
120+
let listener = listeners[i];
121+
listener();
122+
}
123+
}
112124
}
113125

114-
function triggerErrorOnChunk<T>(chunk: Chunk<T>, error: Error): void {
115-
if (chunk.status !== PENDING) {
126+
function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: Error): void {
127+
if (chunk._status !== PENDING) {
116128
// We already resolved. We didn't expect to see this.
117129
return;
118130
}
119-
let resolve = chunk.resolve;
131+
let listeners = chunk._value;
120132
let erroredChunk: ErroredChunk = (chunk: any);
121-
erroredChunk.status = ERRORED;
122-
erroredChunk.value = error;
123-
erroredChunk.resolve = null;
124-
resolve();
133+
erroredChunk._status = ERRORED;
134+
erroredChunk._value = error;
135+
wakeChunk(listeners);
125136
}
126137

127138
function createResolvedChunk<T>(value: T): ResolvedChunk<T> {
128-
return {
129-
$$typeof: CHUNK_TYPE,
130-
status: RESOLVED,
131-
value: value,
132-
resolve: null,
133-
};
139+
return new Chunk(RESOLVED, value);
134140
}
135141

136-
function resolveChunk<T>(chunk: Chunk<T>, value: T): void {
137-
if (chunk.status !== PENDING) {
142+
function resolveChunk<T>(chunk: SomeChunk<T>, value: T): void {
143+
if (chunk._status !== PENDING) {
138144
// We already resolved. We didn't expect to see this.
139145
return;
140146
}
141-
let resolve = chunk.resolve;
147+
let listeners = chunk._value;
142148
let resolvedChunk: ResolvedChunk<T> = (chunk: any);
143-
resolvedChunk.status = RESOLVED;
144-
resolvedChunk.value = value;
145-
resolvedChunk.resolve = null;
146-
resolve();
149+
resolvedChunk._status = RESOLVED;
150+
resolvedChunk._value = value;
151+
wakeChunk(listeners);
147152
}
148153

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

163-
function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
164-
if (maybeChunk == null || (maybeChunk: any).$$typeof !== CHUNK_TYPE) {
168+
function readMaybeChunk<T>(maybeChunk: SomeChunk<T> | T): T {
169+
if (maybeChunk == null || !(maybeChunk instanceof Chunk)) {
165170
// $FlowFixMe
166171
return maybeChunk;
167172
}
168-
let chunk: Chunk<T> = (maybeChunk: any);
169-
if (chunk.status === RESOLVED) {
170-
return chunk.value;
173+
let chunk: SomeChunk<T> = (maybeChunk: any);
174+
if (chunk._status === RESOLVED) {
175+
return chunk._value;
176+
} else if (chunk._status === PENDING) {
177+
// eslint-disable-next-line no-throw-literal
178+
throw (chunk: Wakeable);
171179
} else {
172-
throw chunk.value;
180+
throw chunk._value;
173181
}
174182
}
175183

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

217225
type UninitializedBlockPayload<Data> = [
218226
mixed,
219-
ModuleMetaData | Chunk<ModuleMetaData>,
220-
Data | Chunk<Data>,
227+
ModuleMetaData | SomeChunk<ModuleMetaData>,
228+
Data | SomeChunk<Data>,
221229
];
222230

223-
type Thenable<T> = {
224-
then(resolve: (T) => mixed, reject?: (mixed) => mixed): Thenable<any>,
225-
};
226-
227231
function initializeBlock<Props, Data>(
228232
tuple: UninitializedBlockPayload<Data>,
229233
): BlockComponent<Props, Data> {

packages/react-devtools-shared/src/devtools/cache.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @flow
88
*/
99

10+
import type {Thenable} from 'shared/ReactTypes';
11+
1012
import * as React from 'react';
1113
import {createContext} from 'react';
1214

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

23-
export type Thenable<T> = {
24-
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
25-
...
26-
};
25+
export type {Thenable};
2726

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

packages/react-dom/src/test-utils/ReactTestUtilsAct.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

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

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

76-
function act(callback: () => Thenable) {
76+
function act(callback: () => Thenable<mixed>): Thenable<void> {
7777
if (!__DEV__) {
7878
if (didWarnAboutUsingActInProd === false) {
7979
didWarnAboutUsingActInProd = true;
@@ -146,7 +146,7 @@ function act(callback: () => Thenable) {
146146
// effects and microtasks in a loop until flushPassiveEffects() === false,
147147
// and cleans up
148148
return {
149-
then(resolve: () => void, reject: (?Error) => void) {
149+
then(resolve, reject) {
150150
called = true;
151151
result.then(
152152
() => {
@@ -206,7 +206,7 @@ function act(callback: () => Thenable) {
206206

207207
// in the sync case, the returned thenable only warns *if* await-ed
208208
return {
209-
then(resolve: () => void) {
209+
then(resolve) {
210210
if (__DEV__) {
211211
console.error(
212212
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',

packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,11 @@ export function resolveModuleReference<T>(
2222
return moduleData;
2323
}
2424

25-
type Thenable = {
26-
then(resolve: (any) => mixed, reject?: (Error) => mixed): Thenable,
27-
...
28-
};
29-
3025
// The chunk cache contains all the chunks we've preloaded so far.
3126
// If they're still pending they're a thenable. This map also exists
3227
// in Webpack but unfortunately it's not exposed so we have to
3328
// replicate it in user space. null means that it has already loaded.
34-
const chunkCache: Map<string, null | Thenable | Error> = new Map();
29+
const chunkCache: Map<string, null | Promise<any> | Error> = new Map();
3530

3631
// Start preloading the modules since we might need them soon.
3732
// This function doesn't suspend.

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
2121
import type {CapturedValue, CapturedError} from './ReactCapturedValue';
2222
import type {SuspenseState} from './ReactFiberSuspenseComponent';
2323
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
24-
import type {Thenable} from './ReactFiberWorkLoop';
24+
import type {Wakeable} from 'shared/ReactTypes';
2525
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
2626

2727
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
@@ -118,7 +118,7 @@ import {
118118
} from './ReactFiberHostConfig';
119119
import {
120120
captureCommitPhaseError,
121-
resolveRetryThenable,
121+
resolveRetryWakeable,
122122
markCommitTimeOfFallback,
123123
enqueuePendingPassiveHookEffectMount,
124124
enqueuePendingPassiveHookEffectUnmount,
@@ -1783,9 +1783,9 @@ function commitSuspenseComponent(finishedWork: Fiber) {
17831783
if (enableSuspenseCallback && newState !== null) {
17841784
const suspenseCallback = finishedWork.memoizedProps.suspenseCallback;
17851785
if (typeof suspenseCallback === 'function') {
1786-
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
1787-
if (thenables !== null) {
1788-
suspenseCallback(new Set(thenables));
1786+
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
1787+
if (wakeables !== null) {
1788+
suspenseCallback(new Set(wakeables));
17891789
}
17901790
} else if (__DEV__) {
17911791
if (suspenseCallback !== undefined) {
@@ -1827,27 +1827,27 @@ function commitSuspenseHydrationCallbacks(
18271827
}
18281828

18291829
function attachSuspenseRetryListeners(finishedWork: Fiber) {
1830-
// If this boundary just timed out, then it will have a set of thenables.
1831-
// For each thenable, attach a listener so that when it resolves, React
1830+
// If this boundary just timed out, then it will have a set of wakeables.
1831+
// For each wakeable, attach a listener so that when it resolves, React
18321832
// attempts to re-render the boundary in the primary (pre-timeout) state.
1833-
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
1834-
if (thenables !== null) {
1833+
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
1834+
if (wakeables !== null) {
18351835
finishedWork.updateQueue = null;
18361836
let retryCache = finishedWork.stateNode;
18371837
if (retryCache === null) {
18381838
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
18391839
}
1840-
thenables.forEach(thenable => {
1840+
wakeables.forEach(wakeable => {
18411841
// Memoize using the boundary fiber to prevent redundant listeners.
1842-
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
1843-
if (!retryCache.has(thenable)) {
1842+
let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
1843+
if (!retryCache.has(wakeable)) {
18441844
if (enableSchedulerTracing) {
1845-
if (thenable.__reactDoNotTraceInteractions !== true) {
1845+
if (wakeable.__reactDoNotTraceInteractions !== true) {
18461846
retry = Schedule_tracing_wrap(retry);
18471847
}
18481848
}
1849-
retryCache.add(thenable);
1850-
thenable.then(retry, retry);
1849+
retryCache.add(wakeable);
1850+
wakeable.then(retry, retry);
18511851
}
18521852
});
18531853
}

0 commit comments

Comments
 (0)