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
}