Skip to content

Commit 60fbb7b

Browse files
authored
[Flight] Implement FlightClient in terms of Thenable/Promises instead of throwing Promises (#25260)
* [Flight] Align Chunks with Thenable used with experimental_use Use the field names used by the Thenable data structure passed to use(). These are considered public in this model. This adds another field since we use a separate field name for "reason". * Implement Thenable Protocol on Chunks This doesn't just ping but resolves/rejects with the value. * Subclass Promises * Pass key through JSON parsing * Wait for preloadModules before resolving module chunks * Initialize lazy resolved values before reading the result * Block a model from initializing if its direct dependencies are pending If a module is blocked, then we can't complete initializing a model. However, we can still let it parse, and then fill in the missing pieces later. We need to block it from resolving until all dependencies have filled in which we can do with a ref count. * Treat blocked modules or models as a special status We currently loop over all chunks at the end to error them if they're still pending. We shouldn't do this if they're pending because they're blocked on an external resource like a module because the module might not resolve before the Flight connection closes and that's not an error. In an alternative solution I had a set that tracked pending chunks and removed one at a time. While the loop at the end is faster it's more work as we go. I figured the extra status might also help debugging. For modules we can probably assume no forward references, and the first async module we can just use the promise as the chunk. So we could probably get away with this only on models that are blocked by modules.
1 parent c91a1e0 commit 60fbb7b

File tree

10 files changed

+384
-165
lines changed

10 files changed

+384
-165
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 309 additions & 98 deletions
Large diffs are not rendered by default.

packages/react-client/src/ReactFlightClientStream.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ function createFromJSONCallback(response: Response) {
114114
return function(key: string, value: JSONValue) {
115115
if (typeof value === 'string') {
116116
// We can't use .bind here because we need the "this" value.
117-
return parseModelString(response, this, value);
117+
return parseModelString(response, this, key, value);
118118
}
119119
if (typeof value === 'object' && value !== null) {
120120
return parseModelTuple(response, value);

packages/react-reconciler/src/ReactFiberWakeable.new.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
6161
// If the thenable doesn't have a status, set it to "pending" and attach
6262
// a listener that will update its status and result when it resolves.
6363
switch (thenable.status) {
64-
case 'pending':
65-
// Since the status is already "pending", we can assume it will be updated
66-
// when it resolves, either by React or something in userspace.
67-
break;
6864
case 'fulfilled':
6965
case 'rejected':
7066
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
7571
suspendedThenable = null;
7672
break;
7773
default: {
78-
// TODO: Only instrument the thenable if the status if not defined. If
79-
// it's defined, but an unknown value, assume it's been instrumented by
80-
// some custom userspace implementation.
74+
if (typeof thenable.status === 'string') {
75+
// Only instrument the thenable if the status if not defined. If
76+
// it's defined, but an unknown value, assume it's been instrumented by
77+
// some custom userspace implementation. We treat it as "pending".
78+
break;
79+
}
8180
const pendingThenable: PendingThenable<mixed> = (thenable: any);
8281
pendingThenable.status = 'pending';
8382
pendingThenable.then(

packages/react-reconciler/src/ReactFiberWakeable.old.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
6161
// If the thenable doesn't have a status, set it to "pending" and attach
6262
// a listener that will update its status and result when it resolves.
6363
switch (thenable.status) {
64-
case 'pending':
65-
// Since the status is already "pending", we can assume it will be updated
66-
// when it resolves, either by React or something in userspace.
67-
break;
6864
case 'fulfilled':
6965
case 'rejected':
7066
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
7571
suspendedThenable = null;
7672
break;
7773
default: {
78-
// TODO: Only instrument the thenable if the status if not defined. If
79-
// it's defined, but an unknown value, assume it's been instrumented by
80-
// some custom userspace implementation.
74+
if (typeof thenable.status === 'string') {
75+
// Only instrument the thenable if the status if not defined. If
76+
// it's defined, but an unknown value, assume it's been instrumented by
77+
// some custom userspace implementation. We treat it as "pending".
78+
break;
79+
}
8180
const pendingThenable: PendingThenable<mixed> = (thenable: any);
8281
pendingThenable.status = 'pending';
8382
pendingThenable.then(

packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ export function resolveModuleReference<T>(
4444
return resolveModuleReferenceImpl(moduleData);
4545
}
4646

47-
function parseModelRecursively(response: Response, parentObj, value) {
47+
function parseModelRecursively(response: Response, parentObj, key, value) {
4848
if (typeof value === 'string') {
49-
return parseModelString(response, parentObj, value);
49+
return parseModelString(response, parentObj, key, value);
5050
}
5151
if (typeof value === 'object' && value !== null) {
5252
if (isArray(value)) {
@@ -55,6 +55,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
5555
(parsedValue: any)[i] = parseModelRecursively(
5656
response,
5757
value,
58+
'' + i,
5859
value[i],
5960
);
6061
}
@@ -65,6 +66,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
6566
(parsedValue: any)[innerKey] = parseModelRecursively(
6667
response,
6768
value,
69+
innerKey,
6870
value[innerKey],
6971
);
7072
}
@@ -77,5 +79,5 @@ function parseModelRecursively(response: Response, parentObj, value) {
7779
const dummy = {};
7880

7981
export function parseModel<T>(response: Response, json: UninitializedModel): T {
80-
return (parseModelRecursively(response, dummy, json): any);
82+
return (parseModelRecursively(response, dummy, '', json): any);
8183
}

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

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

10-
import type {Thenable} from 'shared/ReactTypes';
10+
import type {
11+
Thenable,
12+
FulfilledThenable,
13+
RejectedThenable,
14+
} from 'shared/ReactTypes';
1115

1216
export type WebpackSSRMap = {
1317
[clientId: string]: {
@@ -56,7 +60,9 @@ const asyncModuleCache: Map<string, Thenable<any>> = new Map();
5660

5761
// Start preloading the modules since we might need them soon.
5862
// This function doesn't suspend.
59-
export function preloadModule<T>(moduleData: ModuleReference<T>): void {
63+
export function preloadModule<T>(
64+
moduleData: ModuleReference<T>,
65+
): null | Thenable<any> {
6066
const chunks = moduleData.chunks;
6167
const promises = [];
6268
for (let i = 0; i < chunks.length; i++) {
@@ -72,20 +78,35 @@ export function preloadModule<T>(moduleData: ModuleReference<T>): void {
7278
}
7379
}
7480
if (moduleData.async) {
75-
const modulePromise: any = Promise.all(promises).then(() => {
76-
return __webpack_require__(moduleData.id);
77-
});
78-
modulePromise.then(
79-
value => {
80-
modulePromise.status = 'fulfilled';
81-
modulePromise.value = value;
82-
},
83-
reason => {
84-
modulePromise.status = 'rejected';
85-
modulePromise.reason = reason;
86-
},
87-
);
88-
asyncModuleCache.set(moduleData.id, modulePromise);
81+
const existingPromise = asyncModuleCache.get(moduleData.id);
82+
if (existingPromise) {
83+
if (existingPromise.status === 'fulfilled') {
84+
return null;
85+
}
86+
return existingPromise;
87+
} else {
88+
const modulePromise: Thenable<T> = Promise.all(promises).then(() => {
89+
return __webpack_require__(moduleData.id);
90+
});
91+
modulePromise.then(
92+
value => {
93+
const fulfilledThenable: FulfilledThenable<mixed> = (modulePromise: any);
94+
fulfilledThenable.status = 'fulfilled';
95+
fulfilledThenable.value = value;
96+
},
97+
reason => {
98+
const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
99+
rejectedThenable.status = 'rejected';
100+
rejectedThenable.reason = reason;
101+
},
102+
);
103+
asyncModuleCache.set(moduleData.id, modulePromise);
104+
return modulePromise;
105+
}
106+
} else if (promises.length > 0) {
107+
return Promise.all(promises);
108+
} else {
109+
return null;
89110
}
90111
}
91112

@@ -99,23 +120,10 @@ export function requireModule<T>(moduleData: ModuleReference<T>): T {
99120
const promise: any = asyncModuleCache.get(moduleData.id);
100121
if (promise.status === 'fulfilled') {
101122
moduleExports = promise.value;
102-
} else if (promise.status === 'rejected') {
103-
throw promise.reason;
104123
} else {
105-
throw promise;
124+
throw promise.reason;
106125
}
107126
} else {
108-
const chunks = moduleData.chunks;
109-
for (let i = 0; i < chunks.length; i++) {
110-
const chunkId = chunks[i];
111-
const entry = chunkCache.get(chunkId);
112-
if (entry !== null) {
113-
// We assume that preloadModule has been called before.
114-
// So we don't expect to see entry being undefined here, that's an error.
115-
// Let's throw either an error or the Promise.
116-
throw entry;
117-
}
118-
}
119127
moduleExports = __webpack_require__(moduleData.id);
120128
}
121129
if (moduleData.name === '*') {

packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ export function resolveModuleReference<T>(
4444
return resolveModuleReferenceImpl(moduleData);
4545
}
4646

47-
function parseModelRecursively(response: Response, parentObj, value) {
47+
function parseModelRecursively(response: Response, parentObj, key, value) {
4848
if (typeof value === 'string') {
49-
return parseModelString(response, parentObj, value);
49+
return parseModelString(response, parentObj, key, value);
5050
}
5151
if (typeof value === 'object' && value !== null) {
5252
if (isArray(value)) {
@@ -55,6 +55,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
5555
(parsedValue: any)[i] = parseModelRecursively(
5656
response,
5757
value,
58+
'' + i,
5859
value[i],
5960
);
6061
}
@@ -65,6 +66,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
6566
(parsedValue: any)[innerKey] = parseModelRecursively(
6667
response,
6768
value,
69+
innerKey,
6870
value[innerKey],
6971
);
7072
}
@@ -77,5 +79,5 @@ function parseModelRecursively(response: Response, parentObj, value) {
7779
const dummy = {};
7880

7981
export function parseModel<T>(response: Response, json: UninitializedModel): T {
80-
return (parseModelRecursively(response, dummy, json): any);
82+
return (parseModelRecursively(response, dummy, '', json): any);
8183
}

packages/react-server/src/ReactFizzWakeable.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
4444
// If the thenable doesn't have a status, set it to "pending" and attach
4545
// a listener that will update its status and result when it resolves.
4646
switch (thenable.status) {
47-
case 'pending':
48-
// Since the status is already "pending", we can assume it will be updated
49-
// when it resolves, either by React or something in userspace.
50-
break;
5147
case 'fulfilled':
5248
case 'rejected':
5349
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
5753
// TODO: Log a warning?
5854
break;
5955
default: {
60-
// TODO: Only instrument the thenable if the status if not defined. If
61-
// it's defined, but an unknown value, assume it's been instrumented by
62-
// some custom userspace implementation.
56+
if (typeof thenable.status === 'string') {
57+
// Only instrument the thenable if the status if not defined. If
58+
// it's defined, but an unknown value, assume it's been instrumented by
59+
// some custom userspace implementation. We treat it as "pending".
60+
break;
61+
}
6362
const pendingThenable: PendingThenable<mixed> = (thenable: any);
6463
pendingThenable.status = 'pending';
6564
pendingThenable.then(

packages/react-server/src/ReactFlightWakeable.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
4444
// If the thenable doesn't have a status, set it to "pending" and attach
4545
// a listener that will update its status and result when it resolves.
4646
switch (thenable.status) {
47-
case 'pending':
48-
// Since the status is already "pending", we can assume it will be updated
49-
// when it resolves, either by React or something in userspace.
50-
break;
5147
case 'fulfilled':
5248
case 'rejected':
5349
// A thenable that already resolved shouldn't have been thrown, so this is
@@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
5753
// TODO: Log a warning?
5854
break;
5955
default: {
60-
// TODO: Only instrument the thenable if the status if not defined. If
61-
// it's defined, but an unknown value, assume it's been instrumented by
62-
// some custom userspace implementation.
56+
if (typeof thenable.status === 'string') {
57+
// Only instrument the thenable if the status if not defined. If
58+
// it's defined, but an unknown value, assume it's been instrumented by
59+
// some custom userspace implementation. We treat it as "pending".
60+
break;
61+
}
6362
const pendingThenable: PendingThenable<mixed> = (thenable: any);
6463
pendingThenable.status = 'pending';
6564
pendingThenable.then(

scripts/flow/react-relay-hooks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ declare module 'ReactFlightDOMRelayClientIntegration' {
6262
): JSResourceReference<T>;
6363
declare export function preloadModule<T>(
6464
moduleReference: JSResourceReference<T>,
65-
): void;
65+
): null | Promise<void>;
6666
declare export function requireModule<T>(
6767
moduleReference: JSResourceReference<T>,
6868
): T;
@@ -95,7 +95,7 @@ declare module 'ReactFlightNativeRelayClientIntegration' {
9595
): JSResourceReference<T>;
9696
declare export function preloadModule<T>(
9797
moduleReference: JSResourceReference<T>,
98-
): void;
98+
): null | Promise<void>;
9999
declare export function requireModule<T>(
100100
moduleReference: JSResourceReference<T>,
101101
): T;

0 commit comments

Comments
 (0)