@samitouri / QOS-React-2 / commits / 60fbb7b143

[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.

Sebastian Markbåge committed Sep 14, 2022 at 20:13 UTC 60fbb7b1433c5b268a88e0f693f29e6541e1a831
10 files changed +384 -165
packages/react-client/src/ReactFlightClient.js
+309 -98
@@ -7,7 +7,7 @@
7 * @flow
8 */
9
10 -import type {Wakeable} from 'shared/ReactTypes';
10 +import type {Thenable} from 'shared/ReactTypes';
11 import type {LazyComponent} from 'react/src/ReactLazy';
12
13 import type {
@@ -37,63 +37,110 @@ export type JSONValue =
37 | {+[key: string]: JSONValue}
38 | $ReadOnlyArray<JSONValue>;
39
40 -const PENDING = 0;
41 -const RESOLVED_MODEL = 1;
42 -const RESOLVED_MODULE = 2;
43 -const INITIALIZED = 3;
44 -const ERRORED = 4;
45 -
46 -type PendingChunk = {
47 - _status: 0,
48 - _value: null | Array<() => mixed>,
40 +const PENDING = 'pending';
41 +const BLOCKED = 'blocked';
42 +const RESOLVED_MODEL = 'resolved_model';
43 +const RESOLVED_MODULE = 'resolved_module';
44 +const INITIALIZED = 'fulfilled';
45 +const ERRORED = 'rejected';
46 +
47 +type PendingChunk<T> = {
48 + status: 'pending',
49 + value: null | Array<(T) => mixed>,
50 + reason: null | Array<(mixed) => mixed>,
51 + _response: Response,
52 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
53 +};
54 +type BlockedChunk<T> = {
55 + status: 'blocked',
56 + value: null | Array<(T) => mixed>,
57 + reason: null | Array<(mixed) => mixed>,
58 _response: Response,
50 - then(resolve: () => mixed): void,
59 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
60 };
52 -type ResolvedModelChunk = {
53 - _status: 1,
54 - _value: UninitializedModel,
61 +type ResolvedModelChunk<T> = {
62 + status: 'resolved_model',
63 + value: UninitializedModel,
64 + reason: null,
65 _response: Response,
56 - then(resolve: () => mixed): void,
66 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
67 };
68 type ResolvedModuleChunk<T> = {
59 - _status: 2,
60 - _value: ModuleReference<T>,
69 + status: 'resolved_module',
70 + value: ModuleReference<T>,
71 + reason: null,
72 _response: Response,
62 - then(resolve: () => mixed): void,
73 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
74 };
75 type InitializedChunk<T> = {
65 - _status: 3,
66 - _value: T,
76 + status: 'fulfilled',
77 + value: T,
78 + reason: null,
79 _response: Response,
68 - then(resolve: () => mixed): void,
80 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
81 };
70 -type ErroredChunk = {
71 - _status: 4,
72 - _value: Error,
82 +type ErroredChunk<T> = {
83 + status: 'rejected',
84 + value: null,
85 + reason: mixed,
86 _response: Response,
74 - then(resolve: () => mixed): void,
87 + then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
88 };
89 type SomeChunk<T> =
77 - | PendingChunk
78 - | ResolvedModelChunk
90 + | PendingChunk<T>
91 + | BlockedChunk<T>
92 + | ResolvedModelChunk<T>
93 | ResolvedModuleChunk<T>
94 | InitializedChunk<T>
81 - | ErroredChunk;
95 + | ErroredChunk<T>;
96
83 -function Chunk(status: any, value: any, response: Response) {
84 - this._status = status;
85 - this._value = value;
97 +function Chunk(status: any, value: any, reason: any, response: Response) {
98 + this.status = status;
99 + this.value = value;
100 + this.reason = reason;
101 this._response = response;
102 }
88 -Chunk.prototype.then = function<T>(resolve: () => mixed) {
103 +// We subclass Promise.prototype so that we get other methods like .catch
104 +Chunk.prototype = (Object.create(Promise.prototype): any);
105 +// TODO: This doesn't return a new Promise chain unlike the real .then
106 +Chunk.prototype.then = function<T>(
107 + resolve: (value: T) => mixed,
108 + reject: (reason: mixed) => mixed,
109 +) {
110 const chunk: SomeChunk<T> = this;
90 - if (chunk._status === PENDING) {
91 - if (chunk._value === null) {
92 - chunk._value = [];
93 - }
94 - chunk._value.push(resolve);
95 - } else {
96 - resolve();
111 + // If we have resolved content, we try to initialize it first which
112 + // might put us back into one of the other states.
113 + switch (chunk.status) {
114 + case RESOLVED_MODEL:
115 + initializeModelChunk(chunk);
116 + break;
117 + case RESOLVED_MODULE:
118 + initializeModuleChunk(chunk);
119 + break;
120 + }
121 + // The status might have changed after initialization.
122 + switch (chunk.status) {
123 + case INITIALIZED:
124 + resolve(chunk.value);
125 + break;
126 + case PENDING:
127 + case BLOCKED:
128 + if (resolve) {
129 + if (chunk.value === null) {
130 + chunk.value = [];
131 + }
132 + chunk.value.push(resolve);
133 + }
134 + if (reject) {
135 + if (chunk.reason === null) {
136 + chunk.reason = [];
137 + }
138 + chunk.reason.push(reject);
139 + }
140 + break;
141 + default:
142 + reject(chunk.reason);
143 + break;
144 }
145 };
146
@@ -107,18 +154,26 @@ export type ResponseBase = {
154 export type {Response};
155
156 function readChunk<T>(chunk: SomeChunk<T>): T {
110 - switch (chunk._status) {
111 - case INITIALIZED:
112 - return chunk._value;
157 + // If we have resolved content, we try to initialize it first which
158 + // might put us back into one of the other states.
159 + switch (chunk.status) {
160 case RESOLVED_MODEL:
114 - return initializeModelChunk(chunk);
161 + initializeModelChunk(chunk);
162 + break;
163 case RESOLVED_MODULE:
116 - return initializeModuleChunk(chunk);
164 + initializeModuleChunk(chunk);
165 + break;
166 + }
167 + // The status might have changed after initialization.
168 + switch (chunk.status) {
169 + case INITIALIZED:
170 + return chunk.value;
171 case PENDING:
172 + case BLOCKED:
173 // eslint-disable-next-line no-throw-literal
119 - throw (chunk: Wakeable);
174 + throw ((chunk: any): Thenable<T>);
175 default:
121 - throw chunk._value;
176 + throw chunk.reason;
177 }
178 }
179
@@ -128,14 +183,22 @@ function readRoot<T>(): T {
183 return readChunk(chunk);
184 }
185
131 -function createPendingChunk(response: Response): PendingChunk {
186 +function createPendingChunk<T>(response: Response): PendingChunk<T> {
187 + // $FlowFixMe Flow doesn't support functions as constructors
188 + return new Chunk(PENDING, null, null, response);
189 +}
190 +
191 +function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
192 // $FlowFixMe Flow doesn't support functions as constructors
133 - return new Chunk(PENDING, null, response);
193 + return new Chunk(BLOCKED, null, null, response);
194 }
195
136 -function createErrorChunk(response: Response, error: Error): ErroredChunk {
196 +function createErrorChunk<T>(
197 + response: Response,
198 + error: Error,
199 +): ErroredChunk<T> {
200 // $FlowFixMe Flow doesn't support functions as constructors
138 - return new Chunk(ERRORED, error, response);
201 + return new Chunk(ERRORED, null, error, response);
202 }
203
204 function createInitializedChunk<T>(
@@ -143,36 +206,58 @@ function createInitializedChunk<T>(
206 value: T,
207 ): InitializedChunk<T> {
208 // $FlowFixMe Flow doesn't support functions as constructors
146 - return new Chunk(INITIALIZED, value, response);
209 + return new Chunk(INITIALIZED, value, null, response);
210 }
211
149 -function wakeChunk(listeners: null | Array<() => mixed>) {
150 - if (listeners !== null) {
151 - for (let i = 0; i < listeners.length; i++) {
152 - const listener = listeners[i];
153 - listener();
154 - }
212 +function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
213 + for (let i = 0; i < listeners.length; i++) {
214 + const listener = listeners[i];
215 + listener(value);
216 }
217 }
218
158 -function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: Error): void {
159 - if (chunk._status !== PENDING) {
219 +function wakeChunkIfInitialized<T>(
220 + chunk: SomeChunk<T>,
221 + resolveListeners: Array<(T) => mixed>,
222 + rejectListeners: null | Array<(mixed) => mixed>,
223 +): void {
224 + switch (chunk.status) {
225 + case INITIALIZED:
226 + wakeChunk(resolveListeners, chunk.value);
227 + break;
228 + case PENDING:
229 + case BLOCKED:
230 + chunk.value = resolveListeners;
231 + chunk.reason = rejectListeners;
232 + break;
233 + case ERRORED:
234 + if (rejectListeners) {
235 + wakeChunk(rejectListeners, chunk.reason);
236 + }
237 + break;
238 + }
239 +}
240 +
241 +function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
242 + if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
243 // We already resolved. We didn't expect to see this.
244 return;
245 }
163 - const listeners = chunk._value;
164 - const erroredChunk: ErroredChunk = (chunk: any);
165 - erroredChunk._status = ERRORED;
166 - erroredChunk._value = error;
167 - wakeChunk(listeners);
246 + const listeners = chunk.reason;
247 + const erroredChunk: ErroredChunk<T> = (chunk: any);
248 + erroredChunk.status = ERRORED;
249 + erroredChunk.reason = error;
250 + if (listeners !== null) {
251 + wakeChunk(listeners, error);
252 + }
253 }
254
170 -function createResolvedModelChunk(
255 +function createResolvedModelChunk<T>(
256 response: Response,
257 value: UninitializedModel,
173 -): ResolvedModelChunk {
258 +): ResolvedModelChunk<T> {
259 // $FlowFixMe Flow doesn't support functions as constructors
175 - return new Chunk(RESOLVED_MODEL, value, response);
260 + return new Chunk(RESOLVED_MODEL, value, null, response);
261 }
262
263 function createResolvedModuleChunk<T>(
@@ -180,53 +265,97 @@ function createResolvedModuleChunk<T>(
265 value: ModuleReference<T>,
266 ): ResolvedModuleChunk<T> {
267 // $FlowFixMe Flow doesn't support functions as constructors
183 - return new Chunk(RESOLVED_MODULE, value, response);
268 + return new Chunk(RESOLVED_MODULE, value, null, response);
269 }
270
271 function resolveModelChunk<T>(
272 chunk: SomeChunk<T>,
273 value: UninitializedModel,
274 ): void {
190 - if (chunk._status !== PENDING) {
275 + if (chunk.status !== PENDING) {
276 // We already resolved. We didn't expect to see this.
277 return;
278 }
194 - const listeners = chunk._value;
195 - const resolvedChunk: ResolvedModelChunk = (chunk: any);
196 - resolvedChunk._status = RESOLVED_MODEL;
197 - resolvedChunk._value = value;
198 - wakeChunk(listeners);
279 + const resolveListeners = chunk.value;
280 + const rejectListeners = chunk.reason;
281 + const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
282 + resolvedChunk.status = RESOLVED_MODEL;
283 + resolvedChunk.value = value;
284 + if (resolveListeners !== null) {
285 + // This is unfortunate that we're reading this eagerly if
286 + // we already have listeners attached since they might no
287 + // longer be rendered or might not be the highest pri.
288 + initializeModelChunk(resolvedChunk);
289 + // The status might have changed after initialization.
290 + wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
291 + }
292 }
293
294 function resolveModuleChunk<T>(
295 chunk: SomeChunk<T>,
296 value: ModuleReference<T>,
297 ): void {
205 - if (chunk._status !== PENDING) {
298 + if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
299 // We already resolved. We didn't expect to see this.
300 return;
301 }
209 - const listeners = chunk._value;
302 + const resolveListeners = chunk.value;
303 + const rejectListeners = chunk.reason;
304 const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
211 - resolvedChunk._status = RESOLVED_MODULE;
212 - resolvedChunk._value = value;
213 - wakeChunk(listeners);
305 + resolvedChunk.status = RESOLVED_MODULE;
306 + resolvedChunk.value = value;
307 + if (resolveListeners !== null) {
308 + initializeModuleChunk(resolvedChunk);
309 + wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
310 + }
311 }
312
216 -function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
217 - const value: T = parseModel(chunk._response, chunk._value);
218 - const initializedChunk: InitializedChunk<T> = (chunk: any);
219 - initializedChunk._status = INITIALIZED;
220 - initializedChunk._value = value;
221 - return value;
313 +let initializingChunk: ResolvedModelChunk<any> = (null: any);
314 +let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
315 +function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
316 + const prevChunk = initializingChunk;
317 + const prevBlocked = initializingChunkBlockedModel;
318 + initializingChunk = chunk;
319 + initializingChunkBlockedModel = null;
320 + try {
321 + const value: T = parseModel(chunk._response, chunk.value);
322 + if (
323 + initializingChunkBlockedModel !== null &&
324 + initializingChunkBlockedModel.deps > 0
325 + ) {
326 + initializingChunkBlockedModel.value = value;
327 + // We discovered new dependencies on modules that are not yet resolved.
328 + // We have to go the BLOCKED state until they're resolved.
329 + const blockedChunk: BlockedChunk<T> = (chunk: any);
330 + blockedChunk.status = BLOCKED;
331 + blockedChunk.value = null;
332 + blockedChunk.reason = null;
333 + } else {
334 + const initializedChunk: InitializedChunk<T> = (chunk: any);
335 + initializedChunk.status = INITIALIZED;
336 + initializedChunk.value = value;
337 + }
338 + } catch (error) {
339 + const erroredChunk: ErroredChunk<T> = (chunk: any);
340 + erroredChunk.status = ERRORED;
341 + erroredChunk.reason = error;
342 + } finally {
343 + initializingChunk = prevChunk;
344 + initializingChunkBlockedModel = prevBlocked;
345 + }
346 }
347
224 -function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): T {
225 - const value: T = requireModule(chunk._value);
226 - const initializedChunk: InitializedChunk<T> = (chunk: any);
227 - initializedChunk._status = INITIALIZED;
228 - initializedChunk._value = value;
229 - return value;
348 +function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): void {
349 + try {
350 + const value: T = requireModule(chunk.value);
351 + const initializedChunk: InitializedChunk<T> = (chunk: any);
352 + initializedChunk.status = INITIALIZED;
353 + initializedChunk.value = value;
354 + } catch (error) {
355 + const erroredChunk: ErroredChunk<T> = (chunk: any);
356 + erroredChunk.status = ERRORED;
357 + erroredChunk.reason = error;
358 + }
359 }
360
361 // Report that any missing chunks in the model is now going to throw this
@@ -236,7 +365,9 @@ export function reportGlobalError(response: Response, error: Error): void {
365 // If this chunk was already resolved or errored, it won't
366 // trigger an error but if it wasn't then we need to
367 // because we won't be getting any new data to resolve it.
239 - triggerErrorOnChunk(chunk, error);
368 + if (chunk.status === PENDING) {
369 + triggerErrorOnChunk(chunk, error);
370 + }
371 });
372 }
373
@@ -302,9 +433,47 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
433 return chunk;
434 }
435
436 +function createModelResolver<T>(
437 + chunk: SomeChunk<T>,
438 + parentObject: Object,
439 + key: string,
440 +) {
441 + let blocked;
442 + if (initializingChunkBlockedModel) {
443 + blocked = initializingChunkBlockedModel;
444 + blocked.deps++;
445 + } else {
446 + blocked = initializingChunkBlockedModel = {
447 + deps: 1,
448 + value: null,
449 + };
450 + }
451 + return value => {
452 + parentObject[key] = value;
453 + blocked.deps--;
454 + if (blocked.deps === 0) {
455 + if (chunk.status !== BLOCKED) {
456 + return;
457 + }
458 + const resolveListeners = chunk.value;
459 + const initializedChunk: InitializedChunk<T> = (chunk: any);
460 + initializedChunk.status = INITIALIZED;
461 + initializedChunk.value = blocked.value;
462 + if (resolveListeners !== null) {
463 + wakeChunk(resolveListeners, blocked.value);
464 + }
465 + }
466 + };
467 +}
468 +
469 +function createModelReject<T>(chunk: SomeChunk<T>) {
470 + return error => triggerErrorOnChunk(chunk, error);
471 +}
472 +
473 export function parseModelString(
474 response: Response,
475 parentObject: Object,
476 + key: string,
477 value: string,
478 ): any {
479 switch (value[0]) {
@@ -317,7 +486,29 @@ export function parseModelString(
486 } else {
487 const id = parseInt(value.substring(1), 16);
488 const chunk = getChunk(response, id);
320 - return readChunk(chunk);
489 + switch (chunk.status) {
490 + case RESOLVED_MODEL:
491 + initializeModelChunk(chunk);
492 + break;
493 + case RESOLVED_MODULE:
494 + initializeModuleChunk(chunk);
495 + break;
496 + }
497 + // The status might have changed after initialization.
498 + switch (chunk.status) {
499 + case INITIALIZED:
500 + return chunk.value;
501 + case PENDING:
502 + case BLOCKED:
503 + const parentChunk = initializingChunk;
504 + chunk.then(
505 + createModelResolver(parentChunk, parentObject, key),
506 + createModelReject(parentChunk),
507 + );
508 + return null;
509 + default:
510 + throw chunk.reason;
511 + }
512 }
513 }
514 case '@': {
@@ -400,12 +591,32 @@ export function resolveModule(
591 // TODO: Add an option to encode modules that are lazy loaded.
592 // For now we preload all modules as early as possible since it's likely
593 // that we'll need them.
403 - preloadModule(moduleReference);
404 -
405 - if (!chunk) {
406 - chunks.set(id, createResolvedModuleChunk(response, moduleReference));
594 + const promise = preloadModule(moduleReference);
595 + if (promise) {
596 + let blockedChunk: BlockedChunk<any>;
597 + if (!chunk) {
598 + // Technically, we should just treat promise as the chunk in this
599 + // case. Because it'll just behave as any other promise.
600 + blockedChunk = createBlockedChunk(response);
601 + chunks.set(id, blockedChunk);
602 + } else {
603 + // This can't actually happen because we don't have any forward
604 + // references to modules.
605 + blockedChunk = (chunk: any);
606 + blockedChunk.status = BLOCKED;
607 + }
608 + promise.then(
609 + () => resolveModuleChunk(blockedChunk, moduleReference),
610 + error => triggerErrorOnChunk(blockedChunk, error),
611 + );
612 } else {
408 - resolveModuleChunk(chunk, moduleReference);
613 + if (!chunk) {
614 + chunks.set(id, createResolvedModuleChunk(response, moduleReference));
615 + } else {
616 + // This can't actually happen because we don't have any forward
617 + // references to modules.
618 + resolveModuleChunk(chunk, moduleReference);
619 + }
620 }
621 }
622
packages/react-client/src/ReactFlightClientStream.js
+1 -1
@@ -114,7 +114,7 @@ function createFromJSONCallback(response: Response) {
114 return function(key: string, value: JSONValue) {
115 if (typeof value === 'string') {
116 // 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);
118 }
119 if (typeof value === 'object' && value !== null) {
120 return parseModelTuple(response, value);
packages/react-reconciler/src/ReactFiberWakeable.new.js
+6 -7
@@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
61 // If the thenable doesn't have a status, set it to "pending" and attach
62 // a listener that will update its status and result when it resolves.
63 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;
64 case 'fulfilled':
65 case 'rejected':
66 // A thenable that already resolved shouldn't have been thrown, so this is
@@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
71 suspendedThenable = null;
72 break;
73 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 + }
80 const pendingThenable: PendingThenable<mixed> = (thenable: any);
81 pendingThenable.status = 'pending';
82 pendingThenable.then(
packages/react-reconciler/src/ReactFiberWakeable.old.js
+6 -7
@@ -61,10 +61,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
61 // If the thenable doesn't have a status, set it to "pending" and attach
62 // a listener that will update its status and result when it resolves.
63 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;
64 case 'fulfilled':
65 case 'rejected':
66 // A thenable that already resolved shouldn't have been thrown, so this is
@@ -75,9 +71,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
71 suspendedThenable = null;
72 break;
73 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 + }
80 const pendingThenable: PendingThenable<mixed> = (thenable: any);
81 pendingThenable.status = 'pending';
82 pendingThenable.then(
packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js
+5 -3
@@ -44,9 +44,9 @@ export function resolveModuleReference<T>(
44 return resolveModuleReferenceImpl(moduleData);
45 }
46
47 -function parseModelRecursively(response: Response, parentObj, value) {
47 +function parseModelRecursively(response: Response, parentObj, key, value) {
48 if (typeof value === 'string') {
49 - return parseModelString(response, parentObj, value);
49 + return parseModelString(response, parentObj, key, value);
50 }
51 if (typeof value === 'object' && value !== null) {
52 if (isArray(value)) {
@@ -55,6 +55,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
55 (parsedValue: any)[i] = parseModelRecursively(
56 response,
57 value,
58 + '' + i,
59 value[i],
60 );
61 }
@@ -65,6 +66,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
66 (parsedValue: any)[innerKey] = parseModelRecursively(
67 response,
68 value,
69 + innerKey,
70 value[innerKey],
71 );
72 }
@@ -77,5 +79,5 @@ function parseModelRecursively(response: Response, parentObj, value) {
79 const dummy = {};
80
81 export function parseModel<T>(response: Response, json: UninitializedModel): T {
80 - return (parseModelRecursively(response, dummy, json): any);
82 + return (parseModelRecursively(response, dummy, '', json): any);
83 }
packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js
+38 -30
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {Thenable} from 'shared/ReactTypes';
10 +import type {
11 + Thenable,
12 + FulfilledThenable,
13 + RejectedThenable,
14 +} from 'shared/ReactTypes';
15
16 export type WebpackSSRMap = {
17 [clientId: string]: {
@@ -56,7 +60,9 @@ const asyncModuleCache: Map<string, Thenable<any>> = new Map();
60
61 // Start preloading the modules since we might need them soon.
62 // 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> {
66 const chunks = moduleData.chunks;
67 const promises = [];
68 for (let i = 0; i < chunks.length; i++) {
@@ -72,20 +78,35 @@ export function preloadModule<T>(moduleData: ModuleReference<T>): void {
78 }
79 }
80 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;
110 }
111 }
112
@@ -99,23 +120,10 @@ export function requireModule<T>(moduleData: ModuleReference<T>): T {
120 const promise: any = asyncModuleCache.get(moduleData.id);
121 if (promise.status === 'fulfilled') {
122 moduleExports = promise.value;
102 - } else if (promise.status === 'rejected') {
103 - throw promise.reason;
123 } else {
105 - throw promise;
124 + throw promise.reason;
125 }
126 } 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 - }
127 moduleExports = __webpack_require__(moduleData.id);
128 }
129 if (moduleData.name === '*') {
packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js
+5 -3
@@ -44,9 +44,9 @@ export function resolveModuleReference<T>(
44 return resolveModuleReferenceImpl(moduleData);
45 }
46
47 -function parseModelRecursively(response: Response, parentObj, value) {
47 +function parseModelRecursively(response: Response, parentObj, key, value) {
48 if (typeof value === 'string') {
49 - return parseModelString(response, parentObj, value);
49 + return parseModelString(response, parentObj, key, value);
50 }
51 if (typeof value === 'object' && value !== null) {
52 if (isArray(value)) {
@@ -55,6 +55,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
55 (parsedValue: any)[i] = parseModelRecursively(
56 response,
57 value,
58 + '' + i,
59 value[i],
60 );
61 }
@@ -65,6 +66,7 @@ function parseModelRecursively(response: Response, parentObj, value) {
66 (parsedValue: any)[innerKey] = parseModelRecursively(
67 response,
68 value,
69 + innerKey,
70 value[innerKey],
71 );
72 }
@@ -77,5 +79,5 @@ function parseModelRecursively(response: Response, parentObj, value) {
79 const dummy = {};
80
81 export function parseModel<T>(response: Response, json: UninitializedModel): T {
80 - return (parseModelRecursively(response, dummy, json): any);
82 + return (parseModelRecursively(response, dummy, '', json): any);
83 }
packages/react-server/src/ReactFizzWakeable.js
+6 -7
@@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
44 // If the thenable doesn't have a status, set it to "pending" and attach
45 // a listener that will update its status and result when it resolves.
46 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;
47 case 'fulfilled':
48 case 'rejected':
49 // A thenable that already resolved shouldn't have been thrown, so this is
@@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
53 // TODO: Log a warning?
54 break;
55 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 + }
62 const pendingThenable: PendingThenable<mixed> = (thenable: any);
63 pendingThenable.status = 'pending';
64 pendingThenable.then(
packages/react-server/src/ReactFlightWakeable.js
+6 -7
@@ -44,10 +44,6 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
44 // If the thenable doesn't have a status, set it to "pending" and attach
45 // a listener that will update its status and result when it resolves.
46 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;
47 case 'fulfilled':
48 case 'rejected':
49 // A thenable that already resolved shouldn't have been thrown, so this is
@@ -57,9 +53,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
53 // TODO: Log a warning?
54 break;
55 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 + }
62 const pendingThenable: PendingThenable<mixed> = (thenable: any);
63 pendingThenable.status = 'pending';
64 pendingThenable.then(
scripts/flow/react-relay-hooks.js
+2 -2
@@ -62,7 +62,7 @@ declare module 'ReactFlightDOMRelayClientIntegration' {
62 ): JSResourceReference<T>;
63 declare export function preloadModule<T>(
64 moduleReference: JSResourceReference<T>,
65 - ): void;
65 + ): null | Promise<void>;
66 declare export function requireModule<T>(
67 moduleReference: JSResourceReference<T>,
68 ): T;
@@ -95,7 +95,7 @@ declare module 'ReactFlightNativeRelayClientIntegration' {
95 ): JSResourceReference<T>;
96 declare export function preloadModule<T>(
97 moduleReference: JSResourceReference<T>,
98 - ): void;
98 + ): null | Promise<void>;
99 declare export function requireModule<T>(
100 moduleReference: JSResourceReference<T>,
101 ): T;