@samitouri / QOS-React / commits / 7dc903cd29

Patch FlightReplyServer with fixes from ReactFlightClient (#35277)

FlightReplyServer are for client->server and ReactFlightClient is for server->client. They're not 100% symmetrical. We did a number of refactors to ReactFlightClient in PRs like #29823 and #33664 to change the structure of the resolution. This PR brings those changes to synchronize the two approaches. Which addresses deep resolution of cycles and deferred error handling. This also fixes a critical security vulnerability.

Sebastian Markbåge committed Dec 3, 2025 at 10:41 UTC 7dc903cd29dac55efb4424853fd0442fef3a8700
9 files changed +712 -278
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+23 -12
@@ -344,16 +344,23 @@ function decodeReplyFromBusboy<T>(
344 // we queue any fields we receive until the previous file is done.
345 queuedFields.push(name, value);
346 } else {
347 - resolveField(response, name, value);
347 + try {
348 + resolveField(response, name, value);
349 + } catch (error) {
350 + busboyStream.destroy(error);
351 + }
352 }
353 });
354 busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
355 if (encoding.toLowerCase() === 'base64') {
352 - throw new Error(
353 - "React doesn't accept base64 encoded file uploads because we don't expect " +
354 - "form data passed from a browser to ever encode data that way. If that's " +
355 - 'the wrong assumption, we can easily fix it.',
356 + busboyStream.destroy(
357 + new Error(
358 + "React doesn't accept base64 encoded file uploads because we don't expect " +
359 + "form data passed from a browser to ever encode data that way. If that's " +
360 + 'the wrong assumption, we can easily fix it.',
361 + ),
362 );
363 + return;
364 }
365 pendingFiles++;
366 const file = resolveFileInfo(response, name, filename, mimeType);
@@ -361,14 +368,18 @@ function decodeReplyFromBusboy<T>(
368 resolveFileChunk(response, file, chunk);
369 });
370 value.on('end', () => {
364 - resolveFileComplete(response, name, file);
365 - pendingFiles--;
366 - if (pendingFiles === 0) {
367 - // Release any queued fields
368 - for (let i = 0; i < queuedFields.length; i += 2) {
369 - resolveField(response, queuedFields[i], queuedFields[i + 1]);
371 + try {
372 + resolveFileComplete(response, name, file);
373 + pendingFiles--;
374 + if (pendingFiles === 0) {
375 + // Release any queued fields
376 + for (let i = 0; i < queuedFields.length; i += 2) {
377 + resolveField(response, queuedFields[i], queuedFields[i + 1]);
378 + }
379 + queuedFields.length = 0;
380 }
371 - queuedFields.length = 0;
381 + } catch (error) {
382 + busboyStream.destroy(error);
383 }
384 });
385 });
packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js
+6 -1
@@ -19,6 +19,8 @@ import {
19 } from '../shared/ReactFlightImportMetadata';
20 import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
21
22 +import hasOwnProperty from 'shared/hasOwnProperty';
23 +
24 export type ServerManifest = {
25 [string]: Array<string>,
26 };
@@ -78,7 +80,10 @@ export function preloadModule<T>(
80
81 export function requireModule<T>(metadata: ClientReference<T>): T {
82 const moduleExports = parcelRequire(metadata[ID]);
81 - return moduleExports[metadata[NAME]];
83 + if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
84 + return moduleExports[metadata[NAME]];
85 + }
86 + return (undefined: any);
87 }
88
89 export function getModuleDebugInfo<T>(
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
+23 -12
@@ -572,16 +572,23 @@ export function decodeReplyFromBusboy<T>(
572 // we queue any fields we receive until the previous file is done.
573 queuedFields.push(name, value);
574 } else {
575 - resolveField(response, name, value);
575 + try {
576 + resolveField(response, name, value);
577 + } catch (error) {
578 + busboyStream.destroy(error);
579 + }
580 }
581 });
582 busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
583 if (encoding.toLowerCase() === 'base64') {
580 - throw new Error(
581 - "React doesn't accept base64 encoded file uploads because we don't expect " +
582 - "form data passed from a browser to ever encode data that way. If that's " +
583 - 'the wrong assumption, we can easily fix it.',
584 + busboyStream.destroy(
585 + new Error(
586 + "React doesn't accept base64 encoded file uploads because we don't expect " +
587 + "form data passed from a browser to ever encode data that way. If that's " +
588 + 'the wrong assumption, we can easily fix it.',
589 + ),
590 );
591 + return;
592 }
593 pendingFiles++;
594 const file = resolveFileInfo(response, name, filename, mimeType);
@@ -589,14 +596,18 @@ export function decodeReplyFromBusboy<T>(
596 resolveFileChunk(response, file, chunk);
597 });
598 value.on('end', () => {
592 - resolveFileComplete(response, name, file);
593 - pendingFiles--;
594 - if (pendingFiles === 0) {
595 - // Release any queued fields
596 - for (let i = 0; i < queuedFields.length; i += 2) {
597 - resolveField(response, queuedFields[i], queuedFields[i + 1]);
599 + try {
600 + resolveFileComplete(response, name, file);
601 + pendingFiles--;
602 + if (pendingFiles === 0) {
603 + // Release any queued fields
604 + for (let i = 0; i < queuedFields.length; i += 2) {
605 + resolveField(response, queuedFields[i], queuedFields[i + 1]);
606 + }
607 + queuedFields.length = 0;
608 }
599 - queuedFields.length = 0;
609 + } catch (error) {
610 + busboyStream.destroy(error);
611 }
612 });
613 });
packages/react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js
+6 -1
@@ -34,6 +34,8 @@ import {
34 addChunkDebugInfo,
35 } from 'react-client/src/ReactFlightClientConfig';
36
37 +import hasOwnProperty from 'shared/hasOwnProperty';
38 +
39 export type ServerConsumerModuleMap = null | {
40 [clientId: string]: {
41 [clientExportName: string]: ClientReferenceManifestEntry,
@@ -245,7 +247,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T {
247 // default property of this if it was an ESM interop module.
248 return moduleExports.__esModule ? moduleExports.default : moduleExports;
249 }
248 - return moduleExports[metadata[NAME]];
250 + if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
251 + return moduleExports[metadata[NAME]];
252 + }
253 + return (undefined: any);
254 }
255
256 export function getModuleDebugInfo<T>(
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+23 -12
@@ -564,16 +564,23 @@ function decodeReplyFromBusboy<T>(
564 // we queue any fields we receive until the previous file is done.
565 queuedFields.push(name, value);
566 } else {
567 - resolveField(response, name, value);
567 + try {
568 + resolveField(response, name, value);
569 + } catch (error) {
570 + busboyStream.destroy(error);
571 + }
572 }
573 });
574 busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
575 if (encoding.toLowerCase() === 'base64') {
572 - throw new Error(
573 - "React doesn't accept base64 encoded file uploads because we don't expect " +
574 - "form data passed from a browser to ever encode data that way. If that's " +
575 - 'the wrong assumption, we can easily fix it.',
576 + busboyStream.destroy(
577 + new Error(
578 + "React doesn't accept base64 encoded file uploads because we don't expect " +
579 + "form data passed from a browser to ever encode data that way. If that's " +
580 + 'the wrong assumption, we can easily fix it.',
581 + ),
582 );
583 + return;
584 }
585 pendingFiles++;
586 const file = resolveFileInfo(response, name, filename, mimeType);
@@ -581,14 +588,18 @@ function decodeReplyFromBusboy<T>(
588 resolveFileChunk(response, file, chunk);
589 });
590 value.on('end', () => {
584 - resolveFileComplete(response, name, file);
585 - pendingFiles--;
586 - if (pendingFiles === 0) {
587 - // Release any queued fields
588 - for (let i = 0; i < queuedFields.length; i += 2) {
589 - resolveField(response, queuedFields[i], queuedFields[i + 1]);
591 + try {
592 + resolveFileComplete(response, name, file);
593 + pendingFiles--;
594 + if (pendingFiles === 0) {
595 + // Release any queued fields
596 + for (let i = 0; i < queuedFields.length; i += 2) {
597 + resolveField(response, queuedFields[i], queuedFields[i + 1]);
598 + }
599 + queuedFields.length = 0;
600 }
591 - queuedFields.length = 0;
601 + } catch (error) {
602 + busboyStream.destroy(error);
603 }
604 });
605 });
packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js
+6 -1
@@ -24,6 +24,8 @@ import {
24 } from '../shared/ReactFlightImportMetadata';
25 import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
26
27 +import hasOwnProperty from 'shared/hasOwnProperty';
28 +
29 export type ServerConsumerModuleMap = {
30 [clientId: string]: {
31 [clientExportName: string]: ClientReference<any>,
@@ -158,7 +160,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T {
160 // default property of this if it was an ESM interop module.
161 return moduleExports.default;
162 }
161 - return moduleExports[metadata.name];
163 + if (hasOwnProperty.call(moduleExports, metadata.name)) {
164 + return moduleExports[metadata.name];
165 + }
166 + return (undefined: any);
167 }
168
169 export function getModuleDebugInfo<T>(metadata: ClientReference<T>): null {
packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js
+6 -1
@@ -34,6 +34,8 @@ import {
34 addChunkDebugInfo,
35 } from 'react-client/src/ReactFlightClientConfig';
36
37 +import hasOwnProperty from 'shared/hasOwnProperty';
38 +
39 export type ServerConsumerModuleMap = null | {
40 [clientId: string]: {
41 [clientExportName: string]: ClientReferenceManifestEntry,
@@ -253,7 +255,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T {
255 // default property of this if it was an ESM interop module.
256 return moduleExports.__esModule ? moduleExports.default : moduleExports;
257 }
256 - return moduleExports[metadata[NAME]];
258 + if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
259 + return moduleExports[metadata[NAME]];
260 + }
261 + return (undefined: any);
262 }
263
264 export function getModuleDebugInfo<T>(
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+23 -12
@@ -564,16 +564,23 @@ function decodeReplyFromBusboy<T>(
564 // we queue any fields we receive until the previous file is done.
565 queuedFields.push(name, value);
566 } else {
567 - resolveField(response, name, value);
567 + try {
568 + resolveField(response, name, value);
569 + } catch (error) {
570 + busboyStream.destroy(error);
571 + }
572 }
573 });
574 busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => {
575 if (encoding.toLowerCase() === 'base64') {
572 - throw new Error(
573 - "React doesn't accept base64 encoded file uploads because we don't expect " +
574 - "form data passed from a browser to ever encode data that way. If that's " +
575 - 'the wrong assumption, we can easily fix it.',
576 + busboyStream.destroy(
577 + new Error(
578 + "React doesn't accept base64 encoded file uploads because we don't expect " +
579 + "form data passed from a browser to ever encode data that way. If that's " +
580 + 'the wrong assumption, we can easily fix it.',
581 + ),
582 );
583 + return;
584 }
585 pendingFiles++;
586 const file = resolveFileInfo(response, name, filename, mimeType);
@@ -581,14 +588,18 @@ function decodeReplyFromBusboy<T>(
588 resolveFileChunk(response, file, chunk);
589 });
590 value.on('end', () => {
584 - resolveFileComplete(response, name, file);
585 - pendingFiles--;
586 - if (pendingFiles === 0) {
587 - // Release any queued fields
588 - for (let i = 0; i < queuedFields.length; i += 2) {
589 - resolveField(response, queuedFields[i], queuedFields[i + 1]);
591 + try {
592 + resolveFileComplete(response, name, file);
593 + pendingFiles--;
594 + if (pendingFiles === 0) {
595 + // Release any queued fields
596 + for (let i = 0; i < queuedFields.length; i += 2) {
597 + resolveField(response, queuedFields[i], queuedFields[i + 1]);
598 + }
599 + queuedFields.length = 0;
600 }
591 - queuedFields.length = 0;
601 + } catch (error) {
602 + busboyStream.destroy(error);
603 }
604 });
605 });
packages/react-server/src/ReactFlightReplyServer.js
+596 -226
@@ -50,44 +50,35 @@ export type JSONValue =
50
51 const PENDING = 'pending';
52 const BLOCKED = 'blocked';
53 -const CYCLIC = 'cyclic';
53 const RESOLVED_MODEL = 'resolved_model';
54 const INITIALIZED = 'fulfilled';
55 const ERRORED = 'rejected';
56
57 +type RESPONSE_SYMBOL_TYPE = 'RESPONSE_SYMBOL'; // Fake symbol type.
58 +const RESPONSE_SYMBOL: RESPONSE_SYMBOL_TYPE = (Symbol(): any);
59 +
60 type PendingChunk<T> = {
61 status: 'pending',
60 - value: null | Array<(T) => mixed>,
61 - reason: null | Array<(mixed) => mixed>,
62 - _response: Response,
62 + value: null | Array<InitializationReference | (T => mixed)>,
63 + reason: null | Array<InitializationReference | (mixed => mixed)>,
64 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
65 };
66 type BlockedChunk<T> = {
67 status: 'blocked',
67 - value: null | Array<(T) => mixed>,
68 - reason: null | Array<(mixed) => mixed>,
69 - _response: Response,
70 - then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
71 -};
72 -type CyclicChunk<T> = {
73 - status: 'cyclic',
74 - value: null | Array<(T) => mixed>,
75 - reason: null | Array<(mixed) => mixed>,
76 - _response: Response,
68 + value: null | Array<InitializationReference | (T => mixed)>,
69 + reason: null | Array<InitializationReference | (mixed => mixed)>,
70 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
71 };
72 type ResolvedModelChunk<T> = {
73 status: 'resolved_model',
74 value: string,
82 - reason: number,
83 - _response: Response,
75 + reason: {id: number, [RESPONSE_SYMBOL_TYPE]: Response},
76 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
77 };
78 type InitializedChunk<T> = {
79 status: 'fulfilled',
80 value: T,
81 reason: null,
90 - _response: Response,
82 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
83 };
84 type InitializedStreamChunk<
@@ -96,38 +87,34 @@ type InitializedStreamChunk<
87 status: 'fulfilled',
88 value: T,
89 reason: FlightStreamController,
99 - _response: Response,
90 then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
91 };
92 type ErroredChunk<T> = {
93 status: 'rejected',
94 value: null,
95 reason: mixed,
106 - _response: Response,
96 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
97 };
98 type SomeChunk<T> =
99 | PendingChunk<T>
100 | BlockedChunk<T>
112 - | CyclicChunk<T>
101 | ResolvedModelChunk<T>
102 | InitializedChunk<T>
103 | ErroredChunk<T>;
104
105 // $FlowFixMe[missing-this-annot]
118 -function Chunk(status: any, value: any, reason: any, response: Response) {
106 +function ReactPromise(status: any, value: any, reason: any) {
107 this.status = status;
108 this.value = value;
109 this.reason = reason;
122 - this._response = response;
110 }
111 // We subclass Promise.prototype so that we get other methods like .catch
125 -Chunk.prototype = (Object.create(Promise.prototype): any);
112 +ReactPromise.prototype = (Object.create(Promise.prototype): any);
113 // TODO: This doesn't return a new Promise chain unlike the real .then
127 -Chunk.prototype.then = function <T>(
114 +ReactPromise.prototype.then = function <T>(
115 this: SomeChunk<T>,
116 resolve: (value: T) => mixed,
130 - reject: (reason: mixed) => mixed,
117 + reject: ?(reason: mixed) => mixed,
118 ) {
119 const chunk: SomeChunk<T> = this;
120 // If we have resolved content, we try to initialize it first which
@@ -140,26 +127,31 @@ Chunk.prototype.then = function <T>(
127 // The status might have changed after initialization.
128 switch (chunk.status) {
129 case INITIALIZED:
143 - resolve(chunk.value);
130 + if (typeof resolve === 'function') {
131 + resolve(chunk.value);
132 + }
133 break;
134 case PENDING:
135 case BLOCKED:
147 - case CYCLIC:
148 - if (resolve) {
136 + if (typeof resolve === 'function') {
137 if (chunk.value === null) {
150 - chunk.value = ([]: Array<(T) => mixed>);
138 + chunk.value = ([]: Array<InitializationReference | (T => mixed)>);
139 }
140 chunk.value.push(resolve);
141 }
154 - if (reject) {
142 + if (typeof reject === 'function') {
143 if (chunk.reason === null) {
156 - chunk.reason = ([]: Array<(mixed) => mixed>);
144 + chunk.reason = ([]: Array<
145 + InitializationReference | (mixed => mixed),
146 + >);
147 }
148 chunk.reason.push(reject);
149 }
150 break;
151 default:
162 - reject(chunk.reason);
152 + if (typeof reject === 'function') {
153 + reject(chunk.reason);
154 + }
155 break;
156 }
157 };
@@ -181,28 +173,114 @@ export function getRoot<T>(response: Response): Thenable<T> {
173
174 function createPendingChunk<T>(response: Response): PendingChunk<T> {
175 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
184 - return new Chunk(PENDING, null, null, response);
176 + return new ReactPromise(PENDING, null, null);
177 }
178
187 -function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void {
179 +function wakeChunk<T>(
180 + response: Response,
181 + listeners: Array<InitializationReference | (T => mixed)>,
182 + value: T,
183 +): void {
184 for (let i = 0; i < listeners.length; i++) {
185 const listener = listeners[i];
190 - listener(value);
186 + if (typeof listener === 'function') {
187 + listener(value);
188 + } else {
189 + fulfillReference(response, listener, value);
190 + }
191 }
192 }
193
194 +function rejectChunk(
195 + response: Response,
196 + listeners: Array<InitializationReference | (mixed => mixed)>,
197 + error: mixed,
198 +): void {
199 + for (let i = 0; i < listeners.length; i++) {
200 + const listener = listeners[i];
201 + if (typeof listener === 'function') {
202 + listener(error);
203 + } else {
204 + rejectReference(response, listener.handler, error);
205 + }
206 + }
207 +}
208 +
209 +function resolveBlockedCycle<T>(
210 + resolvedChunk: SomeChunk<T>,
211 + reference: InitializationReference,
212 +): null | InitializationHandler {
213 + const referencedChunk = reference.handler.chunk;
214 + if (referencedChunk === null) {
215 + return null;
216 + }
217 + if (referencedChunk === resolvedChunk) {
218 + // We found the cycle. We can resolve the blocked cycle now.
219 + return reference.handler;
220 + }
221 + const resolveListeners = referencedChunk.value;
222 + if (resolveListeners !== null) {
223 + for (let i = 0; i < resolveListeners.length; i++) {
224 + const listener = resolveListeners[i];
225 + if (typeof listener !== 'function') {
226 + const foundHandler = resolveBlockedCycle(resolvedChunk, listener);
227 + if (foundHandler !== null) {
228 + return foundHandler;
229 + }
230 + }
231 + }
232 + }
233 + return null;
234 +}
235 +
236 function wakeChunkIfInitialized<T>(
237 + response: Response,
238 chunk: SomeChunk<T>,
196 - resolveListeners: Array<(T) => mixed>,
197 - rejectListeners: null | Array<(mixed) => mixed>,
239 + resolveListeners: Array<InitializationReference | (T => mixed)>,
240 + rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,
241 ): void {
242 switch (chunk.status) {
243 case INITIALIZED:
201 - wakeChunk(resolveListeners, chunk.value);
244 + wakeChunk(response, resolveListeners, chunk.value);
245 break;
203 - case PENDING:
246 case BLOCKED:
205 - case CYCLIC:
247 + // It is possible that we're blocked on our own chunk if it's a cycle.
248 + // Before adding back the listeners to the chunk, let's check if it would
249 + // result in a cycle.
250 + for (let i = 0; i < resolveListeners.length; i++) {
251 + const listener = resolveListeners[i];
252 + if (typeof listener !== 'function') {
253 + const reference: InitializationReference = listener;
254 + const cyclicHandler = resolveBlockedCycle(chunk, reference);
255 + if (cyclicHandler !== null) {
256 + // This reference points back to this chunk. We can resolve the cycle by
257 + // using the value from that handler.
258 + fulfillReference(response, reference, cyclicHandler.value);
259 + resolveListeners.splice(i, 1);
260 + i--;
261 + if (rejectListeners !== null) {
262 + const rejectionIdx = rejectListeners.indexOf(reference);
263 + if (rejectionIdx !== -1) {
264 + rejectListeners.splice(rejectionIdx, 1);
265 + }
266 + }
267 + // The status might have changed after fulfilling the reference.
268 + switch ((chunk: SomeChunk<T>).status) {
269 + case INITIALIZED:
270 + const initializedChunk: InitializedChunk<T> = (chunk: any);
271 + wakeChunk(response, resolveListeners, initializedChunk.value);
272 + return;
273 + case ERRORED:
274 + if (rejectListeners !== null) {
275 + rejectChunk(response, rejectListeners, chunk.reason);
276 + }
277 + return;
278 + }
279 + }
280 + }
281 + }
282 + // Fallthrough
283 + case PENDING:
284 if (chunk.value) {
285 for (let i = 0; i < resolveListeners.length; i++) {
286 chunk.value.push(resolveListeners[i]);
@@ -223,13 +301,17 @@ function wakeChunkIfInitialized<T>(
301 break;
302 case ERRORED:
303 if (rejectListeners) {
226 - wakeChunk(rejectListeners, chunk.reason);
304 + wakeChunk(response, rejectListeners, chunk.reason);
305 }
306 break;
307 }
308 }
309
232 -function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
310 +function triggerErrorOnChunk<T>(
311 + response: Response,
312 + chunk: SomeChunk<T>,
313 + error: mixed,
314 +): void {
315 if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
316 // If we get more data to an already resolved ID, we assume that it's
317 // a stream chunk since any other row shouldn't have more than one entry.
@@ -244,7 +326,7 @@ function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
326 erroredChunk.status = ERRORED;
327 erroredChunk.reason = error;
328 if (listeners !== null) {
247 - wakeChunk(listeners, error);
329 + rejectChunk(response, listeners, error);
330 }
331 }
332
@@ -254,7 +336,10 @@ function createResolvedModelChunk<T>(
336 id: number,
337 ): ResolvedModelChunk<T> {
338 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
257 - return new Chunk(RESOLVED_MODEL, value, id, response);
339 + return new ReactPromise(RESOLVED_MODEL, value, {
340 + id,
341 + [RESPONSE_SYMBOL]: response,
342 + });
343 }
344
345 function createErroredChunk<T>(
@@ -262,10 +347,11 @@ function createErroredChunk<T>(
347 reason: mixed,
348 ): ErroredChunk<T> {
349 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
265 - return new Chunk(ERRORED, null, reason, response);
350 + return new ReactPromise(ERRORED, null, reason);
351 }
352
353 function resolveModelChunk<T>(
354 + response: Response,
355 chunk: SomeChunk<T>,
356 value: string,
357 id: number,
@@ -287,14 +373,14 @@ function resolveModelChunk<T>(
373 const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
374 resolvedChunk.status = RESOLVED_MODEL;
375 resolvedChunk.value = value;
290 - resolvedChunk.reason = id;
376 + resolvedChunk.reason = {id, [RESPONSE_SYMBOL]: response};
377 if (resolveListeners !== null) {
378 // This is unfortunate that we're reading this eagerly if
379 // we already have listeners attached since they might no
380 // longer be rendered or might not be the highest pri.
381 initializeModelChunk(resolvedChunk);
382 // The status might have changed after initialization.
297 - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
383 + wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
384 }
385 }
386
@@ -308,7 +394,7 @@ function createInitializedStreamChunk<
394 // We use the reason field to stash the controller since we already have that
395 // field. It's a bit of a hack but efficient.
396 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
311 - return new Chunk(INITIALIZED, value, controller, response);
397 + return new ReactPromise(INITIALIZED, value, controller);
398 }
399
400 function createResolvedIteratorResultChunk<T>(
@@ -320,10 +406,14 @@ function createResolvedIteratorResultChunk<T>(
406 const iteratorResultJSON =
407 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
408 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
323 - return new Chunk(RESOLVED_MODEL, iteratorResultJSON, -1, response);
409 + return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, {
410 + id: -1,
411 + [RESPONSE_SYMBOL]: response,
412 + });
413 }
414
415 function resolveIteratorResultChunk<T>(
416 + response: Response,
417 chunk: SomeChunk<IteratorResult<T, T>>,
418 value: string,
419 done: boolean,
@@ -331,55 +421,112 @@ function resolveIteratorResultChunk<T>(
421 // To reuse code as much code as possible we add the wrapper element as part of the JSON.
422 const iteratorResultJSON =
423 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
334 - resolveModelChunk(chunk, iteratorResultJSON, -1);
335 -}
336 -
337 -function bindArgs(fn: any, args: any) {
338 - return fn.bind.apply(fn, [null].concat(args));
424 + resolveModelChunk(response, chunk, iteratorResultJSON, -1);
425 }
426
341 -function loadServerReference<T>(
427 +function loadServerReference<A: Iterable<any>, T>(
428 response: Response,
343 - id: ServerReferenceId,
344 - bound: null | Thenable<Array<any>>,
345 - parentChunk: SomeChunk<T>,
429 + metaData: {
430 + id: any,
431 + bound: null | Thenable<Array<any>>,
432 + },
433 parentObject: Object,
434 key: string,
348 -): T {
435 +): (...A) => Promise<T> {
436 + const id: ServerReferenceId = metaData.id;
437 + if (typeof id !== 'string') {
438 + return (null: any);
439 + }
440 const serverReference: ServerReference<T> =
441 resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
442 // We expect most servers to not really need this because you'd just have all
443 // the relevant modules already loaded but it allows for lazy loading of code
444 // if needed.
354 - const preloadPromise = preloadModule(serverReference);
355 - let promise: Promise<T>;
356 - if (bound) {
357 - promise = Promise.all([(bound: any), preloadPromise]).then(
358 - ([args]: Array<any>) => bindArgs(requireModule(serverReference), args),
359 - );
360 - } else {
361 - if (preloadPromise) {
362 - promise = Promise.resolve(preloadPromise).then(() =>
363 - requireModule(serverReference),
364 - );
445 + const bound = metaData.bound;
446 + let promise: null | Thenable<any> = preloadModule(serverReference);
447 + if (!promise) {
448 + if (bound instanceof ReactPromise) {
449 + promise = Promise.resolve(bound);
450 } else {
366 - // Synchronously available
367 - return requireModule(serverReference);
451 + const resolvedValue = (requireModule(serverReference): any);
452 + return resolvedValue;
453 }
454 + } else if (bound instanceof ReactPromise) {
455 + promise = Promise.all([promise, bound]);
456 }
370 - promise.then(
371 - createModelResolver(
372 - parentChunk,
373 - parentObject,
374 - key,
375 - false,
376 - response,
377 - createModel,
378 - [],
379 - ),
380 - createModelReject(parentChunk),
381 - );
382 - // We need a placeholder value that will be replaced later.
457 +
458 + let handler: InitializationHandler;
459 + if (initializingHandler) {
460 + handler = initializingHandler;
461 + handler.deps++;
462 + } else {
463 + handler = initializingHandler = {
464 + chunk: null,
465 + value: null,
466 + reason: null,
467 + deps: 1,
468 + errored: false,
469 + };
470 + }
471 +
472 + function fulfill(): void {
473 + let resolvedValue = (requireModule(serverReference): any);
474 +
475 + if (metaData.bound) {
476 + // This promise is coming from us and should have initilialized by now.
477 + const promiseValue = (metaData.bound: any).value;
478 + const boundArgs: Array<any> = Array.isArray(promiseValue)
479 + ? promiseValue.slice(0)
480 + : [];
481 + boundArgs.unshift(null); // this
482 + resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);
483 + }
484 +
485 + parentObject[key] = resolvedValue;
486 +
487 + // If this is the root object for a model reference, where `handler.value`
488 + // is a stale `null`, the resolved value can be used directly.
489 + if (key === '' && handler.value === null) {
490 + handler.value = resolvedValue;
491 + }
492 +
493 + handler.deps--;
494 +
495 + if (handler.deps === 0) {
496 + const chunk = handler.chunk;
497 + if (chunk === null || chunk.status !== BLOCKED) {
498 + return;
499 + }
500 + const resolveListeners = chunk.value;
501 + const initializedChunk: InitializedChunk<T> = (chunk: any);
502 + initializedChunk.status = INITIALIZED;
503 + initializedChunk.value = handler.value;
504 + if (resolveListeners !== null) {
505 + wakeChunk(response, resolveListeners, handler.value);
506 + }
507 + }
508 + }
509 +
510 + function reject(error: mixed): void {
511 + if (handler.errored) {
512 + // We've already errored. We could instead build up an AggregateError
513 + // but if there are multiple errors we just take the first one like
514 + // Promise.all.
515 + return;
516 + }
517 + handler.errored = true;
518 + handler.value = null;
519 + handler.reason = error;
520 + const chunk = handler.chunk;
521 + if (chunk === null || chunk.status !== BLOCKED) {
522 + return;
523 + }
524 + triggerErrorOnChunk(response, chunk, error);
525 + }
526 +
527 + promise.then(fulfill, reject);
528 +
529 + // Return a place holder value for now.
530 return (null: any);
531 }
532
@@ -427,7 +574,7 @@ function reviveModel(
574 value[key],
575 childRef,
576 );
430 - if (newValue !== undefined) {
577 + if (newValue !== undefined || key === '__proto__') {
578 // $FlowFixMe[cannot-write]
579 value[key] = newValue;
580 } else {
@@ -441,24 +588,42 @@ function reviveModel(
588 return value;
589 }
590
444 -let initializingChunk: ResolvedModelChunk<any> = (null: any);
445 -let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
591 +type InitializationReference = {
592 + handler: InitializationHandler,
593 + parentObject: Object,
594 + key: string,
595 + map: (
596 + response: Response,
597 + model: any,
598 + parentObject: Object,
599 + key: string,
600 + ) => any,
601 + path: Array<string>,
602 +};
603 +type InitializationHandler = {
604 + chunk: null | BlockedChunk<any>,
605 + value: any,
606 + reason: any,
607 + deps: number,
608 + errored: boolean,
609 +};
610 +let initializingHandler: null | InitializationHandler = null;
611 +
612 function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
447 - const prevChunk = initializingChunk;
448 - const prevBlocked = initializingChunkBlockedModel;
449 - initializingChunk = chunk;
450 - initializingChunkBlockedModel = null;
613 + const prevHandler = initializingHandler;
614 + initializingHandler = null;
615
452 - const rootReference =
453 - chunk.reason === -1 ? undefined : chunk.reason.toString(16);
616 + const {[RESPONSE_SYMBOL]: response, id} = chunk.reason;
617 +
618 + const rootReference = id === -1 ? undefined : id.toString(16);
619
620 const resolvedModel = chunk.value;
621
457 - // We go to the CYCLIC state until we've fully resolved this.
622 + // We go to the BLOCKED state until we've fully resolved this.
623 // We do this before parsing in case we try to initialize the same chunk
624 // while parsing the model. Such as in a cyclic reference.
460 - const cyclicChunk: CyclicChunk<T> = (chunk: any);
461 - cyclicChunk.status = CYCLIC;
625 + const cyclicChunk: BlockedChunk<T> = (chunk: any);
626 + cyclicChunk.status = BLOCKED;
627 cyclicChunk.value = null;
628 cyclicChunk.reason = null;
629
@@ -466,37 +631,50 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
631 const rawModel = JSON.parse(resolvedModel);
632
633 const value: T = reviveModel(
469 - chunk._response,
634 + response,
635 {'': rawModel},
636 '',
637 rawModel,
638 rootReference,
639 );
475 - if (
476 - initializingChunkBlockedModel !== null &&
477 - initializingChunkBlockedModel.deps > 0
478 - ) {
479 - initializingChunkBlockedModel.value = value;
480 - // We discovered new dependencies on modules that are not yet resolved.
481 - // We have to go the BLOCKED state until they're resolved.
482 - const blockedChunk: BlockedChunk<T> = (chunk: any);
483 - blockedChunk.status = BLOCKED;
484 - } else {
485 - const resolveListeners = cyclicChunk.value;
486 - const initializedChunk: InitializedChunk<T> = (chunk: any);
487 - initializedChunk.status = INITIALIZED;
488 - initializedChunk.value = value;
489 - if (resolveListeners !== null) {
490 - wakeChunk(resolveListeners, value);
640 +
641 + // Invoke any listeners added while resolving this model. I.e. cyclic
642 + // references. This may or may not fully resolve the model depending on
643 + // if they were blocked.
644 + const resolveListeners = cyclicChunk.value;
645 + if (resolveListeners !== null) {
646 + cyclicChunk.value = null;
647 + cyclicChunk.reason = null;
648 + for (let i = 0; i < resolveListeners.length; i++) {
649 + const listener = resolveListeners[i];
650 + if (typeof listener === 'function') {
651 + listener(value);
652 + } else {
653 + fulfillReference(response, listener, value);
654 + }
655 }
656 }
657 + if (initializingHandler !== null) {
658 + if (initializingHandler.errored) {
659 + throw initializingHandler.reason;
660 + }
661 + if (initializingHandler.deps > 0) {
662 + // We discovered new dependencies on modules that are not yet resolved.
663 + // We have to keep the BLOCKED state until they're resolved.
664 + initializingHandler.value = value;
665 + initializingHandler.chunk = cyclicChunk;
666 + return;
667 + }
668 + }
669 + const initializedChunk: InitializedChunk<T> = (chunk: any);
670 + initializedChunk.status = INITIALIZED;
671 + initializedChunk.value = value;
672 } catch (error) {
673 const erroredChunk: ErroredChunk<T> = (chunk: any);
674 erroredChunk.status = ERRORED;
675 erroredChunk.reason = error;
676 } finally {
498 - initializingChunk = prevChunk;
499 - initializingChunkBlockedModel = prevBlocked;
677 + initializingHandler = prevHandler;
678 }
679 }
680
@@ -510,7 +688,7 @@ export function reportGlobalError(response: Response, error: Error): void {
688 // trigger an error but if it wasn't then we need to
689 // because we won't be getting any new data to resolve it.
690 if (chunk.status === PENDING) {
513 - triggerErrorOnChunk(chunk, error);
691 + triggerErrorOnChunk(response, chunk, error);
692 }
693 });
694 }
@@ -523,9 +701,8 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
701 const key = prefix + id;
702 // Check if we have this field in the backing store already.
703 const backingEntry = response._formData.get(key);
526 - if (backingEntry != null) {
527 - // We assume that this is a string entry for now.
528 - chunk = createResolvedModelChunk(response, (backingEntry: any), id);
704 + if (typeof backingEntry === 'string') {
705 + chunk = createResolvedModelChunk(response, backingEntry, id);
706 } else if (response._closed) {
707 // We have already errored the response and we're not going to get
708 // anything more streaming in so this will immediately error.
@@ -539,57 +716,152 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
716 return chunk;
717 }
718
542 -function createModelResolver<T>(
543 - chunk: SomeChunk<T>,
719 +function fulfillReference(
720 + response: Response,
721 + reference: InitializationReference,
722 + value: any,
723 +): void {
724 + const {handler, parentObject, key, map, path} = reference;
725 +
726 + for (let i = 1; i < path.length; i++) {
727 + // The server doesn't have any lazy references but we unwrap Chunks here in the same way as the client.
728 + while (value instanceof ReactPromise) {
729 + const referencedChunk: SomeChunk<any> = value;
730 + switch (referencedChunk.status) {
731 + case RESOLVED_MODEL:
732 + initializeModelChunk(referencedChunk);
733 + break;
734 + }
735 + switch (referencedChunk.status) {
736 + case INITIALIZED: {
737 + value = referencedChunk.value;
738 + continue;
739 + }
740 + case BLOCKED:
741 + case PENDING: {
742 + // If we're not yet initialized we need to skip what we've already drilled
743 + // through and then wait for the next value to become available.
744 + path.splice(0, i - 1);
745 + // Add "listener" to our new chunk dependency.
746 + if (referencedChunk.value === null) {
747 + referencedChunk.value = [reference];
748 + } else {
749 + referencedChunk.value.push(reference);
750 + }
751 + if (referencedChunk.reason === null) {
752 + referencedChunk.reason = [reference];
753 + } else {
754 + referencedChunk.reason.push(reference);
755 + }
756 + return;
757 + }
758 + default: {
759 + rejectReference(response, reference.handler, referencedChunk.reason);
760 + return;
761 + }
762 + }
763 + }
764 + const name = path[i];
765 + if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
766 + value = value[name];
767 + }
768 + }
769 +
770 + const mappedValue = map(response, value, parentObject, key);
771 + parentObject[key] = mappedValue;
772 +
773 + // If this is the root object for a model reference, where `handler.value`
774 + // is a stale `null`, the resolved value can be used directly.
775 + if (key === '' && handler.value === null) {
776 + handler.value = mappedValue;
777 + }
778 +
779 + // There are no Elements or Debug Info to transfer here.
780 +
781 + handler.deps--;
782 +
783 + if (handler.deps === 0) {
784 + const chunk = handler.chunk;
785 + if (chunk === null || chunk.status !== BLOCKED) {
786 + return;
787 + }
788 + const resolveListeners = chunk.value;
789 + const initializedChunk: InitializedChunk<any> = (chunk: any);
790 + initializedChunk.status = INITIALIZED;
791 + initializedChunk.value = handler.value;
792 + initializedChunk.reason = handler.reason; // Used by streaming chunks
793 + if (resolveListeners !== null) {
794 + wakeChunk(response, resolveListeners, handler.value);
795 + }
796 + }
797 +}
798 +
799 +function rejectReference(
800 + response: Response,
801 + handler: InitializationHandler,
802 + error: mixed,
803 +): void {
804 + if (handler.errored) {
805 + // We've already errored. We could instead build up an AggregateError
806 + // but if there are multiple errors we just take the first one like
807 + // Promise.all.
808 + return;
809 + }
810 + handler.errored = true;
811 + handler.value = null;
812 + handler.reason = error;
813 + const chunk = handler.chunk;
814 + if (chunk === null || chunk.status !== BLOCKED) {
815 + return;
816 + }
817 + // There's no debug info to forward in this direction.
818 + triggerErrorOnChunk(response, chunk, error);
819 +}
820 +
821 +function waitForReference<T>(
822 + referencedChunk: PendingChunk<T> | BlockedChunk<T>,
823 parentObject: Object,
824 key: string,
546 - cyclic: boolean,
825 response: Response,
548 - map: (response: Response, model: any) => T,
826 + map: (response: Response, model: any, parentObject: Object, key: string) => T,
827 path: Array<string>,
550 -): (value: any) => void {
551 - let blocked;
552 - if (initializingChunkBlockedModel) {
553 - blocked = initializingChunkBlockedModel;
554 - if (!cyclic) {
555 - blocked.deps++;
556 - }
828 +): T {
829 + let handler: InitializationHandler;
830 + if (initializingHandler) {
831 + handler = initializingHandler;
832 + handler.deps++;
833 } else {
558 - blocked = initializingChunkBlockedModel = {
559 - deps: (cyclic ? 0 : 1) as number,
560 - value: (null: any),
834 + handler = initializingHandler = {
835 + chunk: null,
836 + value: null,
837 + reason: null,
838 + deps: 1,
839 + errored: false,
840 };
841 }
563 - return value => {
564 - for (let i = 1; i < path.length; i++) {
565 - value = value[path[i]];
566 - }
567 - parentObject[key] = map(response, value);
842
569 - // If this is the root object for a model reference, where `blocked.value`
570 - // is a stale `null`, the resolved value can be used directly.
571 - if (key === '' && blocked.value === null) {
572 - blocked.value = parentObject[key];
573 - }
574 -
575 - blocked.deps--;
576 - if (blocked.deps === 0) {
577 - if (chunk.status !== BLOCKED) {
578 - return;
579 - }
580 - const resolveListeners = chunk.value;
581 - const initializedChunk: InitializedChunk<T> = (chunk: any);
582 - initializedChunk.status = INITIALIZED;
583 - initializedChunk.value = blocked.value;
584 - if (resolveListeners !== null) {
585 - wakeChunk(resolveListeners, blocked.value);
586 - }
587 - }
843 + const reference: InitializationReference = {
844 + handler,
845 + parentObject,
846 + key,
847 + map,
848 + path,
849 };
589 -}
850
591 -function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
592 - return (error: mixed) => triggerErrorOnChunk(chunk, error);
851 + // Add "listener".
852 + if (referencedChunk.value === null) {
853 + referencedChunk.value = [reference];
854 + } else {
855 + referencedChunk.value.push(reference);
856 + }
857 + if (referencedChunk.reason === null) {
858 + referencedChunk.reason = [reference];
859 + } else {
860 + referencedChunk.reason.push(reference);
861 + }
862 +
863 + // Return a place holder value for now.
864 + return (null: any);
865 }
866
867 function getOutlinedModel<T>(
@@ -597,7 +869,7 @@ function getOutlinedModel<T>(
869 reference: string,
870 parentObject: Object,
871 key: string,
600 - map: (response: Response, model: any) => T,
872 + map: (response: Response, model: any, parentObject: Object, key: string) => T,
873 ): T {
874 const path = reference.split(':');
875 const id = parseInt(path[0], 16);
@@ -612,28 +884,79 @@ function getOutlinedModel<T>(
884 case INITIALIZED:
885 let value = chunk.value;
886 for (let i = 1; i < path.length; i++) {
615 - value = value[path[i]];
887 + // The server doesn't have any lazy references but we unwrap Chunks here in the same way as the client.
888 + while (value instanceof ReactPromise) {
889 + const referencedChunk: SomeChunk<any> = value;
890 + switch (referencedChunk.status) {
891 + case RESOLVED_MODEL:
892 + initializeModelChunk(referencedChunk);
893 + break;
894 + }
895 + switch (referencedChunk.status) {
896 + case INITIALIZED: {
897 + value = referencedChunk.value;
898 + break;
899 + }
900 + case BLOCKED:
901 + case PENDING: {
902 + return waitForReference(
903 + referencedChunk,
904 + parentObject,
905 + key,
906 + response,
907 + map,
908 + path.slice(i - 1),
909 + );
910 + }
911 + default: {
912 + // This is an error. Instead of erroring directly, we're going to encode this on
913 + // an initialization handler so that we can catch it at the nearest Element.
914 + if (initializingHandler) {
915 + initializingHandler.errored = true;
916 + initializingHandler.value = null;
917 + initializingHandler.reason = referencedChunk.reason;
918 + } else {
919 + initializingHandler = {
920 + chunk: null,
921 + value: null,
922 + reason: referencedChunk.reason,
923 + deps: 0,
924 + errored: true,
925 + };
926 + }
927 + return (null: any);
928 + }
929 + }
930 + }
931 + const name = path[i];
932 + if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
933 + value = value[name];
934 + }
935 }
617 - return map(response, value);
936 + const chunkValue = map(response, value, parentObject, key);
937 + // There's no Element nor Debug Info in the ReplyServer so we don't have to check those here.
938 + return chunkValue;
939 case PENDING:
940 case BLOCKED:
620 - case CYCLIC:
621 - const parentChunk = initializingChunk;
622 - chunk.then(
623 - createModelResolver(
624 - parentChunk,
625 - parentObject,
626 - key,
627 - chunk.status === CYCLIC,
628 - response,
629 - map,
630 - path,
631 - ),
632 - createModelReject(parentChunk),
633 - );
634 - return (null: any);
941 + return waitForReference(chunk, parentObject, key, response, map, path);
942 default:
636 - throw chunk.reason;
943 + // This is an error. Instead of erroring directly, we're going to encode this on
944 + // an initialization handler.
945 + if (initializingHandler) {
946 + initializingHandler.errored = true;
947 + initializingHandler.value = null;
948 + initializingHandler.reason = chunk.reason;
949 + } else {
950 + initializingHandler = {
951 + chunk: null,
952 + value: null,
953 + reason: chunk.reason,
954 + deps: 0,
955 + errored: true,
956 + };
957 + }
958 + // Placeholder
959 + return (null: any);
960 }
961 }
962
@@ -657,7 +980,7 @@ function createModel(response: Response, model: any): any {
980 return model;
981 }
982
660 -function parseTypedArray(
983 +function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
984 response: Response,
985 reference: string,
986 constructor: any,
@@ -670,30 +993,78 @@ function parseTypedArray(
993 const key = prefix + id;
994 // We should have this backingEntry in the store already because we emitted
995 // it before referencing it. It should be a Blob.
996 + // TODO: Use getOutlinedModel to allow us to emit the Blob later. We should be able to do that now.
997 const backingEntry: Blob = (response._formData.get(key): any);
998
675 - const promise =
676 - constructor === ArrayBuffer
677 - ? backingEntry.arrayBuffer()
678 - : backingEntry.arrayBuffer().then(function (buffer) {
679 - return new constructor(buffer);
680 - });
999 + const promise: Promise<ArrayBuffer> = backingEntry.arrayBuffer();
1000
1001 // Since loading the buffer is an async operation we'll be blocking the parent
1002 // chunk.
684 - const parentChunk = initializingChunk;
685 - promise.then(
686 - createModelResolver(
687 - parentChunk,
688 - parentObject,
689 - parentKey,
690 - false,
691 - response,
692 - createModel,
693 - [],
694 - ),
695 - createModelReject(parentChunk),
696 - );
1003 +
1004 + let handler: InitializationHandler;
1005 + if (initializingHandler) {
1006 + handler = initializingHandler;
1007 + handler.deps++;
1008 + } else {
1009 + handler = initializingHandler = {
1010 + chunk: null,
1011 + value: null,
1012 + reason: null,
1013 + deps: 1,
1014 + errored: false,
1015 + };
1016 + }
1017 +
1018 + function fulfill(buffer: ArrayBuffer): void {
1019 + const resolvedValue: T =
1020 + constructor === ArrayBuffer
1021 + ? (buffer: any)
1022 + : (new constructor(buffer): any);
1023 +
1024 + parentObject[parentKey] = resolvedValue;
1025 +
1026 + // If this is the root object for a model reference, where `handler.value`
1027 + // is a stale `null`, the resolved value can be used directly.
1028 + if (parentKey === '' && handler.value === null) {
1029 + handler.value = resolvedValue;
1030 + }
1031 +
1032 + handler.deps--;
1033 +
1034 + if (handler.deps === 0) {
1035 + const chunk = handler.chunk;
1036 + if (chunk === null || chunk.status !== BLOCKED) {
1037 + return;
1038 + }
1039 + const resolveListeners = chunk.value;
1040 + const initializedChunk: InitializedChunk<T> = (chunk: any);
1041 + initializedChunk.status = INITIALIZED;
1042 + initializedChunk.value = handler.value;
1043 + if (resolveListeners !== null) {
1044 + wakeChunk(response, resolveListeners, handler.value);
1045 + }
1046 + }
1047 + }
1048 +
1049 + function reject(error: mixed): void {
1050 + if (handler.errored) {
1051 + // We've already errored. We could instead build up an AggregateError
1052 + // but if there are multiple errors we just take the first one like
1053 + // Promise.all.
1054 + return;
1055 + }
1056 + handler.errored = true;
1057 + handler.value = null;
1058 + handler.reason = error;
1059 + const chunk = handler.chunk;
1060 + if (chunk === null || chunk.status !== BLOCKED) {
1061 + return;
1062 + }
1063 + triggerErrorOnChunk(response, chunk, error);
1064 + }
1065 +
1066 + promise.then(fulfill, reject);
1067 +
1068 return null;
1069 }
1070
@@ -711,12 +1082,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
1082 const key = prefix + id;
1083 const existingEntries = response._formData.getAll(key);
1084 for (let i = 0; i < existingEntries.length; i++) {
714 - // We assume that this is a string entry for now.
715 - const value: string = (existingEntries[i]: any);
716 - if (value[0] === 'C') {
717 - controller.close(value === 'C' ? '"$undefined"' : value.slice(1));
718 - } else {
719 - controller.enqueueModel(value);
1085 + const value = existingEntries[i];
1086 + if (typeof value === 'string') {
1087 + if (value[0] === 'C') {
1088 + controller.close(value === 'C' ? '"$undefined"' : value.slice(1));
1089 + } else {
1090 + controller.enqueueModel(value);
1091 + }
1092 }
1093 }
1094 }
@@ -774,7 +1146,7 @@ function parseReadableStream<T>(
1146 // to synchronous emitting.
1147 previousBlockedChunk = null;
1148 }
777 - resolveModelChunk(chunk, json, -1);
1149 + resolveModelChunk(response, chunk, json, -1);
1150 });
1151 }
1152 },
@@ -844,7 +1216,12 @@ function parseAsyncIterable<T>(
1216 false,
1217 );
1218 } else {
847 - resolveIteratorResultChunk(buffer[nextWriteIndex], value, false);
1219 + resolveIteratorResultChunk(
1220 + response,
1221 + buffer[nextWriteIndex],
1222 + value,
1223 + false,
1224 + );
1225 }
1226 nextWriteIndex++;
1227 },
@@ -857,12 +1234,18 @@ function parseAsyncIterable<T>(
1234 true,
1235 );
1236 } else {
860 - resolveIteratorResultChunk(buffer[nextWriteIndex], value, true);
1237 + resolveIteratorResultChunk(
1238 + response,
1239 + buffer[nextWriteIndex],
1240 + value,
1241 + true,
1242 + );
1243 }
1244 nextWriteIndex++;
1245 while (nextWriteIndex < buffer.length) {
1246 // In generators, any extra reads from the iterator have the value undefined.
1247 resolveIteratorResultChunk(
1248 + response,
1249 buffer[nextWriteIndex++],
1250 '"$undefined"',
1251 true,
@@ -876,7 +1259,7 @@ function parseAsyncIterable<T>(
1259 createPendingChunk<IteratorResult<T, T>>(response);
1260 }
1261 while (nextWriteIndex < buffer.length) {
879 - triggerErrorOnChunk(buffer[nextWriteIndex++], error);
1262 + triggerErrorOnChunk(response, buffer[nextWriteIndex++], error);
1263 }
1264 },
1265 };
@@ -892,11 +1275,10 @@ function parseAsyncIterable<T>(
1275 if (nextReadIndex === buffer.length) {
1276 if (closed) {
1277 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
895 - return new Chunk(
1278 + return new ReactPromise(
1279 INITIALIZED,
1280 {done: true, value: undefined},
1281 null,
899 - response,
1282 );
1283 }
1284 buffer[nextReadIndex] =
@@ -935,19 +1317,7 @@ function parseModelString(
1317 case 'F': {
1318 // Server Reference
1319 const ref = value.slice(2);
938 - // TODO: Just encode this in the reference inline instead of as a model.
939 - const metaData: {
940 - id: ServerReferenceId,
941 - bound: null | Thenable<Array<any>>,
942 - } = getOutlinedModel(response, ref, obj, key, createModel);
943 - return loadServerReference(
944 - response,
945 - metaData.id,
946 - metaData.bound,
947 - initializingChunk,
948 - obj,
949 - key,
950 - );
1320 + return getOutlinedModel(response, ref, obj, key, loadServerReference);
1321 }
1322 case 'T': {
1323 // Temporary Reference
@@ -1121,7 +1491,7 @@ export function resolveField(
1491 const chunk = chunks.get(id);
1492 if (chunk) {
1493 // We were waiting on this key so now we can resolve it.
1124 - resolveModelChunk(chunk, value, id);
1494 + resolveModelChunk(response, chunk, value, id);
1495 }
1496 }
1497 }