main
js 2,016 lines 60 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {Thenable} from 'shared/ReactTypes';
11
12 // The server acts as a Client of itself when resolving Server References.
13 // That's why we import the Client configuration from the Server.
14 // Everything is aliased as their Server equivalence for clarity.
15 import type {
16 ServerReferenceId,
17 ServerManifest,
18 ClientReference as ServerReference,
19 } from 'react-client/src/ReactFlightClientConfig';
20
21 import type {BackingFormData} from './ReactFlightReplyBackingFormData';
22 import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences';
23
24 import {
25 resolveServerReference,
26 preloadModule,
27 requireModule,
28 } from 'react-client/src/ReactFlightClientConfig';
29
30 import {
31 createBackingFormData,
32 advanceBackingEntryIterator,
33 appendBackingEntry,
34 appendBackingFile,
35 consumeBackingEntry,
36 getBackingEntry,
37 getAllBackingEntries,
38 peekBackingEntry,
39 } from './ReactFlightReplyBackingFormData';
40 import {
41 createTemporaryReference,
42 registerTemporaryReference,
43 } from './ReactFlightServerTemporaryReferences';
44 import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
45
46 import hasOwnProperty from 'shared/hasOwnProperty';
47 import getPrototypeOf from 'shared/getPrototypeOf';
48 import isArray from 'shared/isArray';
49
50 interface FlightStreamController {
51 enqueueModel(json: string): void;
52 close(json: string): void;
53 error(error: Error): void;
54 }
55
56 export type JSONValue =
57 | number
58 | null
59 | boolean
60 | string
61 | {+[key: string]: JSONValue}
62 | $ReadOnlyArray<JSONValue>;
63
64 const PENDING = 'pending';
65 const BLOCKED = 'blocked';
66 const RESOLVED_MODEL = 'resolved_model';
67 const INITIALIZED = 'fulfilled';
68 const ERRORED = 'rejected';
69
70 const __PROTO__ = '__proto__';
71
72 type RESPONSE_SYMBOL_TYPE = 'RESPONSE_SYMBOL'; // Fake symbol type.
73 const RESPONSE_SYMBOL: RESPONSE_SYMBOL_TYPE = Symbol() as any;
74
75 type PendingChunk<T> = {
76 status: 'pending',
77 value: null | Array<InitializationReference | (T => mixed)>,
78 reason: null | Array<InitializationReference | (mixed => mixed)>,
79 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
80 };
81 type BlockedChunk<T> = {
82 status: 'blocked',
83 value: null | Array<InitializationReference | (T => mixed)>,
84 reason: null | Array<InitializationReference | (mixed => mixed)>,
85 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
86 };
87 type ResolvedModelChunk<T> = {
88 status: 'resolved_model',
89 value: string,
90 reason: {id: number, [RESPONSE_SYMBOL_TYPE]: Response},
91 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
92 };
93 type InitializedChunk<T> = {
94 status: 'fulfilled',
95 value: T,
96 reason: null | NestedArrayContext,
97 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
98 };
99 type InitializedStreamChunk<
100 T: ReadableStream | $AsyncIterable<any, any, void>,
101 > = {
102 status: 'fulfilled',
103 value: T,
104 reason: FlightStreamController,
105 then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
106 };
107 type ErroredChunk<T> = {
108 status: 'rejected',
109 value: null,
110 reason: mixed,
111 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
112 };
113 type SomeChunk<T> =
114 | PendingChunk<T>
115 | BlockedChunk<T>
116 | ResolvedModelChunk<T>
117 | InitializedChunk<T>
118 | ErroredChunk<T>;
119
120 // $FlowFixMe[missing-this-annot]
121 function ReactPromise(status: any, value: any, reason: any) {
122 this.status = status;
123 this.value = value;
124 this.reason = reason;
125 }
126 // We subclass Promise.prototype so that we get other methods like .catch
127 ReactPromise.prototype = Object.create(Promise.prototype) as any;
128 // TODO: This doesn't return a new Promise chain unlike the real .then
129 function reactPromiseThen<T>(
130 this: SomeChunk<T>,
131 resolve: (value: T) => mixed,
132 reject: ?(reason: mixed) => mixed,
133 ) {
134 const chunk: SomeChunk<T> = this;
135 // If we have resolved content, we try to initialize it first which
136 // might put us back into one of the other states.
137 switch (chunk.status) {
138 case RESOLVED_MODEL:
139 initializeModelChunk(chunk);
140 break;
141 }
142 // The status might have changed after initialization.
143 switch (chunk.status) {
144 case INITIALIZED:
145 if (typeof resolve === 'function') {
146 let inspectedValue = chunk.value;
147 // Recursively check if the value is itself a ReactPromise and if so if it points
148 // back to itself. This helps catch recursive thenables early error.
149 let cycleProtection = 0;
150 const visited = new Set<typeof ReactPromise>();
151 while (inspectedValue instanceof ReactPromise) {
152 cycleProtection++;
153 if (
154 // $FlowFixMe[invalid-compare]
155 inspectedValue === chunk ||
156 visited.has(inspectedValue) ||
157 cycleProtection > 1000
158 ) {
159 if (typeof reject === 'function') {
160 reject(new Error('Cannot have cyclic thenables.'));
161 }
162 return;
163 }
164 visited.add(inspectedValue);
165 // $FlowFixMe[invalid-compare]
166 if (inspectedValue.status === INITIALIZED) {
167 inspectedValue = inspectedValue.value;
168 } else {
169 // If this is lazily resolved, pending or blocked, it'll eventually become
170 // initialized and break the loop. Rejected also breaks it.
171 break;
172 }
173 }
174 resolve(chunk.value);
175 }
176 break;
177 case PENDING:
178 case BLOCKED:
179 if (typeof resolve === 'function') {
180 if (chunk.value === null) {
181 chunk.value = [] as Array<InitializationReference | (T => mixed)>;
182 }
183 chunk.value.push(resolve);
184 }
185 if (typeof reject === 'function') {
186 if (chunk.reason === null) {
187 chunk.reason = [] as Array<
188 InitializationReference | (mixed => mixed),
189 >;
190 }
191 chunk.reason.push(reject);
192 }
193 break;
194 default:
195 if (typeof reject === 'function') {
196 reject(chunk.reason);
197 }
198 break;
199 }
200 }
201 // The shadowing `then` must be defined with `Object.defineProperty` instead of
202 // assignment. Assignment would throw when `Promise.prototype` is frozen (e.g.
203 // by SES lockdown) because assigning over an inherited non-writable property
204 // is rejected.
205 Object.defineProperty(ReactPromise.prototype, 'then', {
206 writable: true,
207 enumerable: true,
208 configurable: true,
209 value: reactPromiseThen,
210 });
211
212 const ObjectPrototype = Object.prototype;
213 const ArrayPrototype = Array.prototype;
214
215 export type Response = {
216 _bundlerConfig: ServerManifest,
217 _prefix: string,
218 _formData: BackingFormData,
219 _chunks: Map<number, SomeChunk<any>>,
220 _closed: boolean,
221 _closedReason: mixed,
222 _temporaryReferences: void | TemporaryReferenceSet,
223 _rootArrayContexts: WeakMap<$ReadOnlyArray<mixed>, NestedArrayContext>,
224 _arraySizeLimit: number,
225 };
226
227 export function getRoot<T>(response: Response): Thenable<T> {
228 const chunk = getChunk(response, 0);
229 return chunk as any;
230 }
231
232 function createPendingChunk<T>(response: Response): PendingChunk<T> {
233 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
234 return new ReactPromise(PENDING, null, null);
235 }
236
237 function wakeChunk<T>(
238 response: Response,
239 listeners: Array<InitializationReference | (T => mixed)>,
240 value: T,
241 chunk: InitializedChunk<T>,
242 ): void {
243 for (let i = 0; i < listeners.length; i++) {
244 const listener = listeners[i];
245 if (typeof listener === 'function') {
246 listener(value);
247 } else {
248 fulfillReference(response, listener, value, chunk.reason);
249 }
250 }
251 }
252
253 function rejectChunk(
254 response: Response,
255 listeners: Array<InitializationReference | (mixed => mixed)>,
256 error: mixed,
257 ): void {
258 for (let i = 0; i < listeners.length; i++) {
259 const listener = listeners[i];
260 if (typeof listener === 'function') {
261 listener(error);
262 } else {
263 rejectReference(response, listener.handler, error);
264 }
265 }
266 }
267
268 function wakeChunkIfInitialized<T>(
269 response: Response,
270 chunk: SomeChunk<T>,
271 resolveListeners: Array<InitializationReference | (T => mixed)>,
272 rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,
273 ): void {
274 switch (chunk.status) {
275 case INITIALIZED:
276 wakeChunk(response, resolveListeners, chunk.value, chunk);
277 break;
278 case BLOCKED:
279 case PENDING:
280 if (chunk.value) {
281 for (let i = 0; i < resolveListeners.length; i++) {
282 chunk.value.push(resolveListeners[i]);
283 }
284 } else {
285 chunk.value = resolveListeners;
286 }
287
288 if (chunk.reason) {
289 if (rejectListeners) {
290 for (let i = 0; i < rejectListeners.length; i++) {
291 chunk.reason.push(rejectListeners[i]);
292 }
293 }
294 } else {
295 chunk.reason = rejectListeners;
296 }
297 break;
298 case ERRORED:
299 if (rejectListeners) {
300 rejectChunk(response, rejectListeners, chunk.reason);
301 }
302 break;
303 }
304 }
305
306 function triggerErrorOnChunk<T>(
307 response: Response,
308 chunk: SomeChunk<T>,
309 error: mixed,
310 ): void {
311 if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
312 // If we get more data to an already resolved ID, we assume that it's
313 // a stream chunk since any other row shouldn't have more than one entry.
314 const streamChunk: InitializedStreamChunk<any> = chunk as any;
315 const controller = streamChunk.reason;
316 // $FlowFixMe[incompatible-type]: The error method should accept mixed.
317 controller.error(error);
318 return;
319 }
320 const listeners = chunk.reason;
321 const erroredChunk: ErroredChunk<T> = chunk as any;
322 erroredChunk.status = ERRORED;
323 erroredChunk.reason = error;
324 if (listeners !== null) {
325 rejectChunk(response, listeners, error);
326 }
327 }
328
329 function createResolvedModelChunk<T>(
330 response: Response,
331 value: string,
332 id: number,
333 ): ResolvedModelChunk<T> {
334 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
335 return new ReactPromise(RESOLVED_MODEL, value, {
336 id,
337 [RESPONSE_SYMBOL]: response,
338 });
339 }
340
341 function createErroredChunk<T>(
342 response: Response,
343 reason: mixed,
344 ): ErroredChunk<T> {
345 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
346 return new ReactPromise(ERRORED, null, reason);
347 }
348
349 function resolveModelChunk<T>(
350 response: Response,
351 chunk: SomeChunk<T>,
352 value: string,
353 id: number,
354 ): void {
355 if (chunk.status !== PENDING) {
356 // If we get more data to an already resolved ID, we assume that it's
357 // a stream chunk since any other row shouldn't have more than one entry.
358 const streamChunk: InitializedStreamChunk<any> = chunk as any;
359 const controller = streamChunk.reason;
360 if (value[0] === 'C') {
361 controller.close(value === 'C' ? '"$undefined"' : value.slice(1));
362 } else {
363 controller.enqueueModel(value);
364 }
365 return;
366 }
367 const resolveListeners = chunk.value;
368 const rejectListeners = chunk.reason;
369 const resolvedChunk: ResolvedModelChunk<T> = chunk as any;
370 resolvedChunk.status = RESOLVED_MODEL;
371 resolvedChunk.value = value;
372 resolvedChunk.reason = {id, [RESPONSE_SYMBOL]: response};
373 if (resolveListeners !== null) {
374 // This is unfortunate that we're reading this eagerly if
375 // we already have listeners attached since they might no
376 // longer be rendered or might not be the highest pri.
377 initializeModelChunk(resolvedChunk);
378 // The status might have changed after initialization.
379 wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
380 }
381 }
382
383 function createInitializedStreamChunk<
384 T: ReadableStream | $AsyncIterable<any, any, void>,
385 >(
386 response: Response,
387 value: T,
388 controller: FlightStreamController,
389 ): InitializedChunk<T> {
390 // We use the reason field to stash the controller since we already have that
391 // field. It's a bit of a hack but efficient.
392 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
393 return new ReactPromise(INITIALIZED, value, controller);
394 }
395
396 function createResolvedIteratorResultChunk<T>(
397 response: Response,
398 value: string,
399 done: boolean,
400 ): ResolvedModelChunk<IteratorResult<T, T>> {
401 // To reuse code as much code as possible we add the wrapper element as part of the JSON.
402 const iteratorResultJSON =
403 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
404 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
405 return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, {
406 id: -1,
407 [RESPONSE_SYMBOL]: response,
408 });
409 }
410
411 function resolveIteratorResultChunk<T>(
412 response: Response,
413 chunk: SomeChunk<IteratorResult<T, T>>,
414 value: string,
415 done: boolean,
416 ): void {
417 // To reuse code as much code as possible we add the wrapper element as part of the JSON.
418 const iteratorResultJSON =
419 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
420 resolveModelChunk(response, chunk, iteratorResultJSON, -1);
421 }
422
423 function loadServerReference<A: Iterable<any>, T>(
424 response: Response,
425 metaData: {
426 id: any,
427 bound: null | Thenable<Array<any>>,
428 },
429 parentObject: Object,
430 key: string,
431 ): (...A) => Promise<T> {
432 const id: ServerReferenceId = metaData.id;
433 if (typeof id !== 'string') {
434 return null as any;
435 }
436 if (key === 'then') {
437 // This should never happen because we always serialize objects with then-functions
438 // as "thenable" which reduces to ReactPromise with no other fields.
439 return null as any;
440 }
441
442 // Check for a cached promise from a previous call with the same metadata.
443 // This handles deduplication when the same server reference appears multiple
444 // times in the payload.
445 const cachedPromise: SomeChunk<T> | void = (metaData as any).$$promise;
446 if (cachedPromise !== undefined) {
447 if (cachedPromise.status === INITIALIZED) {
448 // The value was already resolved by a previous call.
449 const resolvedValue: T = cachedPromise.value;
450 if (key === __PROTO__) {
451 return null as any;
452 }
453 parentObject[key] = resolvedValue;
454 return resolvedValue as any;
455 }
456
457 // The promise is still blocked. Increment the handler dependency count ...
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 // ... and register resolve and reject listeners on the promise.
472 cachedPromise.then(
473 resolveReference.bind(null, response, handler, parentObject, key),
474 rejectReference.bind(null, response, handler),
475 );
476
477 // Return a place holder value for now.
478 return null as any;
479 }
480
481 // This is the first call for this server reference metadata. Create a cached
482 // promise to be used for subsequent calls.
483 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
484 const blockedPromise: BlockedChunk<T> = new ReactPromise(BLOCKED, null, null);
485 (metaData as any).$$promise = blockedPromise;
486
487 const serverReference: ServerReference<T> =
488 resolveServerReference<$FlowFixMe>(response._bundlerConfig, id);
489 // We expect most servers to not really need this because you'd just have all
490 // the relevant modules already loaded but it allows for lazy loading of code
491 // if needed.
492 const bound = metaData.bound;
493 let serverReferencePromise: null | Thenable<any> =
494 preloadModule(serverReference);
495 if (!serverReferencePromise) {
496 if (bound instanceof ReactPromise) {
497 serverReferencePromise = Promise.resolve(bound);
498 } else {
499 const resolvedValue = requireModule(serverReference) as any;
500 // Resolve the cached promise synchronously.
501 const initializedPromise: InitializedChunk<T> = blockedPromise as any;
502 initializedPromise.status = INITIALIZED;
503 initializedPromise.value = resolvedValue;
504 initializedPromise.reason = null;
505 return resolvedValue;
506 }
507 } else if (bound instanceof ReactPromise) {
508 serverReferencePromise = Promise.all([serverReferencePromise, bound]);
509 }
510
511 let handler: InitializationHandler;
512 if (initializingHandler) {
513 handler = initializingHandler;
514 handler.deps++;
515 } else {
516 handler = initializingHandler = {
517 chunk: null,
518 value: null,
519 reason: null,
520 deps: 1,
521 errored: false,
522 };
523 }
524
525 function fulfill(): void {
526 let resolvedValue = requireModule(serverReference) as any;
527
528 if (metaData.bound) {
529 // This promise is coming from us and should have initialized by now.
530 const promiseValue = (metaData.bound as any).value;
531 const boundArgs: Array<any> = isArray(promiseValue)
532 ? promiseValue.slice(0)
533 : [];
534 if (boundArgs.length > MAX_BOUND_ARGS) {
535 reject(
536 new Error(
537 'Server Function has too many bound arguments. Received ' +
538 boundArgs.length +
539 ' but the limit is ' +
540 MAX_BOUND_ARGS +
541 '.',
542 ),
543 );
544 return;
545 }
546 boundArgs.unshift(null); // this
547 resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);
548 }
549
550 // Resolve the cached promise so subsequent references can use the value.
551 const resolveListeners = blockedPromise.value;
552 const initializedPromise: InitializedChunk<T> = blockedPromise as any;
553 initializedPromise.status = INITIALIZED;
554 initializedPromise.value = resolvedValue;
555 initializedPromise.reason = null;
556 if (resolveListeners !== null) {
557 // Notify any resolve listeners that were added via .then() from
558 // subsequent loadServerReference calls for the same reference.
559 wakeChunk(response, resolveListeners, resolvedValue, initializedPromise);
560 }
561
562 resolveReference(response, handler, parentObject, key, resolvedValue);
563 }
564
565 function reject(error: mixed): void {
566 // Mark the cached promise as errored so subsequent references fail too.
567 const rejectListeners = blockedPromise.reason;
568 const erroredPromise: ErroredChunk<T> = blockedPromise as any;
569 erroredPromise.status = ERRORED;
570 erroredPromise.value = null;
571 erroredPromise.reason = error;
572 if (rejectListeners !== null) {
573 // Notify any reject listeners that were added via .then() from subsequent
574 // loadServerReference calls for the same reference.
575 rejectChunk(response, rejectListeners, error);
576 }
577
578 rejectReference(response, handler, error);
579 }
580
581 serverReferencePromise.then(fulfill, reject);
582
583 // Return a place holder value for now.
584 return null as any;
585 }
586
587 function reviveModel(
588 response: Response,
589 parentObj: any,
590 parentKey: string,
591 value: JSONValue,
592 reference: void | string,
593 arrayRoot: null | NestedArrayContext,
594 ): any {
595 if (typeof value === 'string') {
596 // We can't use .bind here because we need the "this" value.
597 return parseModelString(
598 response,
599 parentObj,
600 parentKey,
601 value,
602 reference,
603 arrayRoot,
604 );
605 }
606 if (typeof value === 'object' && value !== null) {
607 if (
608 reference !== undefined &&
609 response._temporaryReferences !== undefined
610 ) {
611 // Store this object's reference in case it's returned later.
612 registerTemporaryReference(
613 response._temporaryReferences,
614 value,
615 reference,
616 );
617 }
618 if (isArray(value)) {
619 let childContext: NestedArrayContext;
620 if (arrayRoot === null) {
621 childContext = {
622 count: 0,
623 fork: false,
624 } as NestedArrayContext;
625 response._rootArrayContexts.set(value, childContext);
626 } else {
627 childContext = arrayRoot;
628 }
629 if (value.length > 1) {
630 childContext.fork = true;
631 }
632 bumpArrayCount(
633 childContext,
634 // Number of commas + square brackets
635 // value.length - 1 + 2
636 value.length + 1,
637 response,
638 );
639 for (let i = 0; i < value.length; i++) {
640 const childRef =
641 reference !== undefined ? reference + ':' + i : undefined;
642 // $FlowFixMe[cannot-write]
643 value[i] = reviveModel(
644 response,
645 value,
646 '' + i,
647 value[i],
648 childRef,
649 childContext,
650 );
651 }
652 } else {
653 for (const key in value) {
654 if (hasOwnProperty.call(value, key)) {
655 if (key === __PROTO__) {
656 // $FlowFixMe[cannot-write]
657 delete value[key];
658 continue;
659 }
660 const childRef =
661 reference !== undefined && key.indexOf(':') === -1
662 ? reference + ':' + key
663 : undefined;
664 const newValue = reviveModel(
665 response,
666 value,
667 key,
668 value[key],
669 childRef,
670 null, // The array context resets when we're entering a non-array
671 );
672 if (newValue !== undefined) {
673 // $FlowFixMe[cannot-write]
674 value[key] = newValue;
675 } else {
676 // $FlowFixMe[cannot-write]
677 delete value[key];
678 }
679 }
680 }
681 }
682 }
683 return value;
684 }
685
686 type NestedArrayContext = {
687 // Keeps track of how many slots, bytes or characters are in nested arrays/strings/typed arrays.
688 count: number,
689 // A single child is itself not harmful. There needs to be at least one parent array with more
690 // than one child.
691 fork: boolean,
692 };
693
694 function bumpArrayCount(
695 arrayContext: NestedArrayContext,
696 slots: number,
697 response: Response,
698 ): void {
699 const newCount = (arrayContext.count += slots);
700 if (newCount > response._arraySizeLimit && arrayContext.fork) {
701 throw new Error(
702 'Maximum array nesting exceeded. Large nested arrays can be dangerous. Try adding intermediate objects.',
703 );
704 }
705 }
706
707 type InitializationReference = {
708 handler: InitializationHandler,
709 parentObject: Object,
710 key: string,
711 map: (
712 response: Response,
713 model: any,
714 parentObject: Object,
715 key: string,
716 ) => any,
717 path: Array<string>,
718 arrayRoot: null | NestedArrayContext,
719 };
720 type InitializationHandler = {
721 chunk: null | BlockedChunk<any>,
722 value: any,
723 // TODO: Split type to make it impossible to treat a thrown value as NestedArrayContext.
724 // thrown value if errored, otherwise array context
725 reason: mixed | NestedArrayContext,
726 deps: number,
727 errored: boolean,
728 };
729 let initializingHandler: null | InitializationHandler = null;
730
731 function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
732 const prevHandler = initializingHandler;
733 initializingHandler = null;
734
735 const {[RESPONSE_SYMBOL]: response, id} = chunk.reason;
736
737 const rootReference = id === -1 ? undefined : id.toString(16);
738
739 const resolvedModel = chunk.value;
740
741 // We go to the BLOCKED state until we've fully resolved this.
742 // We do this before parsing in case we try to initialize the same chunk
743 // while parsing the model. Such as in a cyclic reference.
744 const cyclicChunk: BlockedChunk<T> = chunk as any;
745 cyclicChunk.status = BLOCKED;
746 cyclicChunk.value = null;
747 cyclicChunk.reason = null;
748
749 try {
750 const rawModel = JSON.parse(resolvedModel);
751
752 // The root might not be an array but if it is we want to track the count of entries.
753 const arrayRoot: NestedArrayContext = {
754 count: 0,
755 fork: false,
756 };
757
758 const value: T = reviveModel(
759 response,
760 {'': rawModel},
761 '',
762 rawModel,
763 rootReference,
764 arrayRoot,
765 );
766
767 // Invoke any listeners added while resolving this model. I.e. cyclic
768 // references. This may or may not fully resolve the model depending on
769 // if they were blocked.
770 const resolveListeners = cyclicChunk.value;
771 if (resolveListeners !== null) {
772 cyclicChunk.value = null;
773 cyclicChunk.reason = null;
774 for (let i = 0; i < resolveListeners.length; i++) {
775 const listener = resolveListeners[i];
776 if (typeof listener === 'function') {
777 listener(value);
778 } else {
779 fulfillReference(response, listener, value, arrayRoot);
780 }
781 }
782 }
783 if (initializingHandler !== null) {
784 if (initializingHandler.errored) {
785 throw initializingHandler.reason;
786 }
787 if (initializingHandler.deps > 0) {
788 // We discovered new dependencies on modules that are not yet resolved.
789 // We have to keep the BLOCKED state until they're resolved.
790 initializingHandler.value = value;
791 initializingHandler.reason = arrayRoot;
792 initializingHandler.chunk = cyclicChunk;
793 return;
794 }
795 }
796 const initializedChunk: InitializedChunk<T> = chunk as any;
797 initializedChunk.status = INITIALIZED;
798 initializedChunk.value = value;
799 initializedChunk.reason = arrayRoot;
800 } catch (error) {
801 const erroredChunk: ErroredChunk<T> = chunk as any;
802 erroredChunk.status = ERRORED;
803 erroredChunk.reason = error;
804 } finally {
805 initializingHandler = prevHandler;
806 }
807 }
808
809 // Report that any missing chunks in the model is now going to throw this
810 // error upon read. Also notify any pending promises.
811 export function reportGlobalError(response: Response, error: Error): void {
812 response._closed = true;
813 response._closedReason = error;
814 response._chunks.forEach(chunk => {
815 // If this chunk was already resolved or errored, it won't
816 // trigger an error but if it wasn't then we need to
817 // because we won't be getting any new data to resolve it.
818 if (chunk.status === PENDING) {
819 triggerErrorOnChunk(response, chunk, error);
820 } else if (chunk.status === INITIALIZED) {
821 const initializedChunk:
822 | InitializedChunk<any>
823 | InitializedStreamChunk<any> = chunk as any;
824 if (initializedChunk.reason !== null) {
825 const maybeController = initializedChunk.reason;
826 // $FlowFixMe[method-unbinding] Just doing a typeof check
827 if (typeof maybeController.error === 'function') {
828 maybeController.error(error);
829 }
830 }
831 }
832 });
833 }
834
835 function getChunk(response: Response, id: number): SomeChunk<any> {
836 const chunks = response._chunks;
837 let chunk = chunks.get(id);
838 if (!chunk) {
839 const prefix = response._prefix;
840 const key = prefix + id;
841 // Check if we have this field in the backing store already.
842 const backingEntry = getBackingEntry(response._formData, key);
843 if (typeof backingEntry === 'string') {
844 chunk = createResolvedModelChunk(response, backingEntry, id);
845 } else if (response._closed) {
846 // We have already errored the response and we're not going to get
847 // anything more streaming in so this will immediately error.
848 chunk = createErroredChunk(response, response._closedReason);
849 } else {
850 // We're still waiting on this entry to stream in.
851 chunk = createPendingChunk(response);
852 }
853 chunks.set(id, chunk);
854 }
855 return chunk;
856 }
857
858 function fulfillReference(
859 response: Response,
860 reference: InitializationReference,
861 value: any,
862 arrayRoot: null | NestedArrayContext,
863 ): void {
864 const {handler, parentObject, key, map, path} = reference;
865
866 let resolvedValue;
867 try {
868 let localLength: number = 0;
869 const rootArrayContexts = response._rootArrayContexts;
870 for (let i = 1; i < path.length; i++) {
871 // The server doesn't have any lazy references so we don't expect to go through a Promise.
872 const name = path[i];
873 if (
874 typeof value === 'object' &&
875 value !== null &&
876 (getPrototypeOf(value) === ObjectPrototype ||
877 getPrototypeOf(value) === ArrayPrototype) &&
878 hasOwnProperty.call(value, name)
879 ) {
880 value = value[name];
881 if (isArray(value)) {
882 localLength = 0;
883 arrayRoot = rootArrayContexts.get(value) || arrayRoot;
884 } else {
885 arrayRoot = null;
886 if (typeof value === 'string') {
887 localLength = value.length;
888 } else if (typeof value === 'bigint') {
889 // Estimate the length to avoid expensive toString() calls on large
890 // BigInt values. If the value is too large, we get Infinity, which
891 // will trigger the array size limit error.
892 // eslint-disable-next-line react-internal/no-primitive-constructors
893 const n = Math.abs(Number(value));
894 if (n === 0) {
895 localLength = 1;
896 } else {
897 localLength = Math.floor(Math.log10(n)) + 1;
898 }
899 } else if (ArrayBuffer.isView(value)) {
900 localLength = value.byteLength;
901 } else {
902 localLength = 0;
903 }
904 }
905 } else {
906 throw new Error('Invalid reference.');
907 }
908 }
909
910 resolvedValue = map(response, value, parentObject, key);
911
912 // Add any array counts to the reference's array root. The value that we're
913 // resolving might have deep nesting that we need to resolve.
914 const referenceArrayRoot = reference.arrayRoot;
915 if (referenceArrayRoot !== null) {
916 if (arrayRoot !== null) {
917 if (arrayRoot.fork) {
918 referenceArrayRoot.fork = true;
919 }
920 bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);
921 } else if (localLength > 0) {
922 bumpArrayCount(referenceArrayRoot, localLength, response);
923 }
924 }
925 } catch (error) {
926 rejectReference(response, handler, error);
927 return;
928 }
929
930 // There are no Elements or Debug Info to transfer here.
931
932 resolveReference(response, handler, parentObject, key, resolvedValue);
933 }
934
935 function resolveReference(
936 response: Response,
937 handler: InitializationHandler,
938 parentObject: Object,
939 key: string,
940 resolvedValue: mixed,
941 ): void {
942 if (key !== __PROTO__) {
943 parentObject[key] = resolvedValue;
944 }
945
946 // If this is the root object for a model reference, where `handler.value`
947 // is a stale `null`, the resolved value can be used directly.
948 if (key === '' && handler.value === null) {
949 handler.value = resolvedValue;
950 }
951
952 handler.deps--;
953
954 if (handler.deps === 0) {
955 const chunk = handler.chunk;
956 if (chunk === null || chunk.status !== BLOCKED) {
957 return;
958 }
959 const resolveListeners = chunk.value;
960 const initializedChunk: InitializedChunk<any> = chunk as any;
961 initializedChunk.status = INITIALIZED;
962 initializedChunk.value = handler.value;
963 initializedChunk.reason =
964 // $FlowFixMe[incompatible-type] Assuming handler.errored is false.
965 handler.reason;
966 if (resolveListeners !== null) {
967 wakeChunk(response, resolveListeners, handler.value, initializedChunk);
968 }
969 }
970 }
971
972 function rejectReference(
973 response: Response,
974 handler: InitializationHandler,
975 error: mixed,
976 ): void {
977 if (handler.errored) {
978 // We've already errored. We could instead build up an AggregateError
979 // but if there are multiple errors we just take the first one like
980 // Promise.all.
981 return;
982 }
983 handler.errored = true;
984 handler.value = null;
985 handler.reason = error;
986 const chunk = handler.chunk;
987 if (chunk === null || chunk.status !== BLOCKED) {
988 return;
989 }
990 // There's no debug info to forward in this direction.
991 triggerErrorOnChunk(response, chunk, error);
992 }
993
994 function waitForReference<T>(
995 response: Response,
996 referencedChunk: BlockedChunk<T>,
997 parentObject: Object,
998 key: string,
999 arrayRoot: null | NestedArrayContext,
1000 map: (response: Response, model: any, parentObject: Object, key: string) => T,
1001 path: Array<string>,
1002 ): T {
1003 let handler: InitializationHandler;
1004 if (initializingHandler) {
1005 handler = initializingHandler;
1006 handler.deps++;
1007 } else {
1008 handler = initializingHandler = {
1009 chunk: null,
1010 value: null,
1011 reason: null,
1012 deps: 1,
1013 errored: false,
1014 };
1015 }
1016
1017 const reference: InitializationReference = {
1018 handler,
1019 parentObject,
1020 key,
1021 map,
1022 path,
1023 arrayRoot,
1024 };
1025
1026 // Add "listener".
1027 if (referencedChunk.value === null) {
1028 referencedChunk.value = [reference];
1029 } else {
1030 referencedChunk.value.push(reference);
1031 }
1032 if (referencedChunk.reason === null) {
1033 referencedChunk.reason = [reference];
1034 } else {
1035 referencedChunk.reason.push(reference);
1036 }
1037
1038 // Return a place holder value for now.
1039 return null as any;
1040 }
1041
1042 function getOutlinedModel<T>(
1043 response: Response,
1044 reference: string,
1045 parentObject: Object,
1046 key: string,
1047 referenceArrayRoot: null | NestedArrayContext,
1048 map: (response: Response, model: any, parentObject: Object, key: string) => T,
1049 ): T {
1050 const path = reference.split(':');
1051 const id = parseInt(path[0], 16);
1052 let chunk = getChunk(response, id);
1053 switch (chunk.status) {
1054 case RESOLVED_MODEL:
1055 initializeModelChunk(chunk);
1056 // $FlowFixMe[incompatible-type] We just initialized this chunk so it can't be a ResolvedModelChunk anymore.
1057 chunk = chunk as Exclude<SomeChunk<T>, ResolvedModelChunk<T>>;
1058 break;
1059 }
1060 // The status might have changed after initialization.
1061 switch (chunk.status) {
1062 case INITIALIZED:
1063 let value = chunk.value;
1064 const arrayRootOrController:
1065 | null
1066 | NestedArrayContext
1067 | FlightStreamController = chunk.reason;
1068 if (arrayRootOrController !== null && 'error' in arrayRootOrController) {
1069 throw new Error(
1070 'Expected an initialized chunk but got an initialized stream chunk instead. ' +
1071 'This payload may have been submitted by an older version of React.',
1072 );
1073 }
1074 let arrayRoot = arrayRootOrController;
1075
1076 let localLength: number = 0;
1077 const rootArrayContexts = response._rootArrayContexts;
1078 for (let i = 1; i < path.length; i++) {
1079 const name = path[i];
1080 if (
1081 typeof value === 'object' &&
1082 // $FlowFixMe[invalid-compare] This check is still needed at runtime.
1083 value !== null &&
1084 (getPrototypeOf(value) === ObjectPrototype ||
1085 getPrototypeOf(value) === ArrayPrototype) &&
1086 hasOwnProperty.call(value, name)
1087 ) {
1088 value = value[name];
1089 if (isArray(value)) {
1090 localLength = 0;
1091 arrayRoot =
1092 rootArrayContexts.get(
1093 // $FlowFixMe[incompatible-type] Our `isArray` typing can't narrow `mixed`
1094 value as $ReadOnlyArray<mixed>,
1095 ) || arrayRoot;
1096 } else {
1097 arrayRoot = null;
1098 if (typeof value === 'string') {
1099 localLength = value.length;
1100 } else if (typeof value === 'bigint') {
1101 // Estimate the length to avoid expensive toString() calls on large
1102 // BigInt values. If the value is too large, we get Infinity, which
1103 // will trigger the array size limit error.
1104 // eslint-disable-next-line react-internal/no-primitive-constructors
1105 const n = Math.abs(Number(value));
1106 if (n === 0) {
1107 localLength = 1;
1108 } else {
1109 localLength = Math.floor(Math.log10(n)) + 1;
1110 }
1111 } else if (ArrayBuffer.isView(value)) {
1112 localLength = value.byteLength;
1113 } else {
1114 localLength = 0;
1115 }
1116 }
1117 } else {
1118 throw new Error('Invalid reference.');
1119 }
1120 }
1121 const chunkValue = map(response, value, parentObject, key);
1122
1123 // Add any array counts to the reference's array root. The value that we're
1124 // resolving might have deep nesting that we need to resolve.
1125 if (referenceArrayRoot !== null) {
1126 if (arrayRoot !== null) {
1127 if (arrayRoot.fork) {
1128 referenceArrayRoot.fork = true;
1129 }
1130 bumpArrayCount(referenceArrayRoot, arrayRoot.count, response);
1131 } else if (localLength > 0) {
1132 bumpArrayCount(referenceArrayRoot, localLength, response);
1133 }
1134 }
1135 // There's no Element nor Debug Info in the ReplyServer so we don't have to check those here.
1136 return chunkValue;
1137 case BLOCKED:
1138 return waitForReference(
1139 response,
1140 chunk,
1141 parentObject,
1142 key,
1143 referenceArrayRoot,
1144 map,
1145 path,
1146 );
1147 case PENDING:
1148 // If we don't have the referenced chunk yet, then this must be a forward reference,
1149 // which is not allowed.
1150 throw new Error('Invalid forward reference.');
1151 default:
1152 // This is an error. Instead of erroring directly, we're going to encode this on
1153 // an initialization handler.
1154 if (initializingHandler) {
1155 initializingHandler.errored = true;
1156 initializingHandler.value = null;
1157 initializingHandler.reason = chunk.reason;
1158 } else {
1159 initializingHandler = {
1160 chunk: null,
1161 value: null,
1162 reason: chunk.reason,
1163 deps: 0,
1164 errored: true,
1165 };
1166 }
1167 // Placeholder
1168 return null as any;
1169 }
1170 }
1171
1172 function createMap(
1173 response: Response,
1174 model: Array<[any, any]>,
1175 ): Map<any, any> {
1176 if (!isArray(model)) {
1177 throw new Error('Invalid Map initializer.');
1178 }
1179 if ((model as any).$$consumed === true) {
1180 throw new Error('Already initialized Map.');
1181 }
1182 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.
1183 (model as any).$$consumed = true;
1184 const map = new Map(model);
1185 return map;
1186 }
1187
1188 function createSet(response: Response, model: Array<any>): Set<any> {
1189 if (!isArray(model)) {
1190 throw new Error('Invalid Set initializer.');
1191 }
1192 if ((model as any).$$consumed === true) {
1193 throw new Error('Already initialized Set.');
1194 }
1195 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.
1196 (model as any).$$consumed = true;
1197 const set = new Set(model);
1198 return set;
1199 }
1200
1201 function extractIterator(response: Response, model: Array<any>): Iterator<any> {
1202 if (!isArray(model)) {
1203 throw new Error('Invalid Iterator initializer.');
1204 }
1205 if ((model as any).$$consumed === true) {
1206 throw new Error('Already initialized Iterator.');
1207 }
1208 // This needs to come first to prevent the model from being consumed again in case of a cyclic reference.
1209 (model as any).$$consumed = true;
1210 // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array.
1211 const iterator = model[Symbol.iterator]();
1212 return iterator;
1213 }
1214
1215 function createModel(
1216 response: Response,
1217 model: any,
1218 parentObject: Object,
1219 key: string,
1220 ): any {
1221 if (key === 'then' && typeof model === 'function') {
1222 // This should never happen because we always serialize objects with then-functions
1223 // as "thenable" which reduces to ReactPromise with no other fields.
1224 return null;
1225 }
1226 return model;
1227 }
1228
1229 function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>(
1230 response: Response,
1231 reference: string,
1232 constructor: any,
1233 bytesPerElement: number,
1234 parentObject: Object,
1235 parentKey: string,
1236 referenceArrayRoot: null | NestedArrayContext,
1237 ): null {
1238 const id = parseInt(reference.slice(2), 16);
1239 const prefix = response._prefix;
1240 const key = prefix + id;
1241 const chunks = response._chunks;
1242 if (chunks.has(id)) {
1243 throw new Error('Already initialized typed array.');
1244 }
1245 chunks.set(
1246 id,
1247 // We don't need to put the actual Blob in the chunk,
1248 // because it shouldn't be accessed by anything else.
1249 createErroredChunk(response, new Error('Already initialized typed array.')),
1250 );
1251
1252 // We should have this backingEntry in the store already because we emitted
1253 // it before referencing it. It should be a Blob.
1254 const backingEntry: Blob = getBackingEntry(response._formData, key) as any;
1255
1256 const promise: Promise<ArrayBuffer> = backingEntry.arrayBuffer();
1257
1258 // Since loading the buffer is an async operation we'll be blocking the parent
1259 // chunk.
1260
1261 let handler: InitializationHandler;
1262 if (initializingHandler) {
1263 handler = initializingHandler;
1264 handler.deps++;
1265 } else {
1266 handler = initializingHandler = {
1267 chunk: null,
1268 value: null,
1269 reason: null,
1270 deps: 1,
1271 errored: false,
1272 };
1273 }
1274
1275 function fulfill(buffer: ArrayBuffer): void {
1276 try {
1277 if (referenceArrayRoot !== null) {
1278 bumpArrayCount(referenceArrayRoot, buffer.byteLength, response);
1279 }
1280
1281 const resolvedValue: T =
1282 constructor === ArrayBuffer
1283 ? (buffer as any)
1284 : (new constructor(buffer) as any);
1285
1286 if (key !== __PROTO__) {
1287 parentObject[parentKey] = resolvedValue;
1288 }
1289
1290 // If this is the root object for a model reference, where `handler.value`
1291 // is a stale `null`, the resolved value can be used directly.
1292 if (parentKey === '' && handler.value === null) {
1293 handler.value = resolvedValue;
1294 }
1295 } catch (x) {
1296 reject(x);
1297 return;
1298 }
1299
1300 handler.deps--;
1301
1302 if (handler.deps === 0) {
1303 const chunk = handler.chunk;
1304 if (chunk === null || chunk.status !== BLOCKED) {
1305 return;
1306 }
1307 const resolveListeners = chunk.value;
1308 const initializedChunk: InitializedChunk<T> = chunk as any;
1309 initializedChunk.status = INITIALIZED;
1310 initializedChunk.value = handler.value;
1311 // We don't keep an array count for this since it won't be referenced again.
1312 // In fact, we don't really need to store this chunk at all.
1313 initializedChunk.reason = null;
1314 if (resolveListeners !== null) {
1315 wakeChunk(response, resolveListeners, handler.value, initializedChunk);
1316 }
1317 }
1318 }
1319
1320 function reject(error: mixed): void {
1321 if (handler.errored) {
1322 // We've already errored. We could instead build up an AggregateError
1323 // but if there are multiple errors we just take the first one like
1324 // Promise.all.
1325 return;
1326 }
1327 handler.errored = true;
1328 handler.value = null;
1329 handler.reason = error;
1330 const chunk = handler.chunk;
1331 if (chunk === null || chunk.status !== BLOCKED) {
1332 return;
1333 }
1334 triggerErrorOnChunk(response, chunk, error);
1335 }
1336
1337 promise.then(fulfill, reject);
1338
1339 return null;
1340 }
1341
1342 function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
1343 response: Response,
1344 id: number,
1345 stream: T,
1346 controller: FlightStreamController,
1347 ): void {
1348 const chunks = response._chunks;
1349 const chunk = createInitializedStreamChunk(response, stream, controller);
1350 chunks.set(id, chunk);
1351
1352 const prefix = response._prefix;
1353 const key = prefix + id;
1354 const existingEntries = getAllBackingEntries(response._formData, key);
1355 for (let i = 0; i < existingEntries.length; i++) {
1356 const value = existingEntries[i];
1357 if (typeof value === 'string') {
1358 if (value[0] === 'C') {
1359 controller.close(value === 'C' ? '"$undefined"' : value.slice(1));
1360 } else {
1361 controller.enqueueModel(value);
1362 }
1363 }
1364 }
1365 }
1366
1367 function parseReadableStream<T>(
1368 response: Response,
1369 reference: string,
1370 type: void | 'bytes',
1371 parentObject: Object,
1372 parentKey: string,
1373 ): ReadableStream {
1374 const id = parseInt(reference.slice(2), 16);
1375 const chunks = response._chunks;
1376 if (chunks.has(id)) {
1377 throw new Error('Already initialized stream.');
1378 }
1379
1380 let controller: ReadableStreamController = null as any;
1381 let closed = false;
1382 const stream = new ReadableStream({
1383 type: type,
1384 start(c) {
1385 controller = c;
1386 },
1387 });
1388 let previousBlockedChunk: SomeChunk<T> | null = null;
1389 function enqueue(value: T): void {
1390 if (type === 'bytes' && !ArrayBuffer.isView(value)) {
1391 flightController.error(new Error('Invalid data for bytes stream.'));
1392 return;
1393 }
1394 controller.enqueue(value);
1395 }
1396 const flightController = {
1397 enqueueModel(json: string): void {
1398 if (previousBlockedChunk === null) {
1399 // If we're not blocked on any other chunks, we can try to eagerly initialize
1400 // this as a fast-path to avoid awaiting them.
1401 const chunk: ResolvedModelChunk<T> = createResolvedModelChunk(
1402 response,
1403 json,
1404 -1,
1405 );
1406 initializeModelChunk(chunk);
1407 const initializedChunk: SomeChunk<T> = chunk;
1408 if (initializedChunk.status === INITIALIZED) {
1409 enqueue(initializedChunk.value);
1410 } else {
1411 chunk.then(enqueue, flightController.error);
1412 previousBlockedChunk = chunk;
1413 }
1414 } else {
1415 // We're still waiting on a previous chunk so we can't enqueue quite yet.
1416 const blockedChunk = previousBlockedChunk;
1417 const chunk: SomeChunk<T> = createPendingChunk(response);
1418 chunk.then(enqueue, flightController.error);
1419 previousBlockedChunk = chunk;
1420 blockedChunk.then(function () {
1421 if (previousBlockedChunk === chunk) {
1422 // We were still the last chunk so we can now clear the queue and return
1423 // to synchronous emitting.
1424 previousBlockedChunk = null;
1425 }
1426 resolveModelChunk(response, chunk, json, -1);
1427 });
1428 }
1429 },
1430 close(json: string): void {
1431 if (closed) {
1432 return;
1433 }
1434 closed = true;
1435 if (previousBlockedChunk === null) {
1436 controller.close();
1437 } else {
1438 const blockedChunk = previousBlockedChunk;
1439 // We shouldn't get any more enqueues after this so we can set it back to null.
1440 previousBlockedChunk = null;
1441 blockedChunk.then(() => controller.close());
1442 }
1443 },
1444 error(error: mixed): void {
1445 if (closed) {
1446 return;
1447 }
1448 closed = true;
1449 if (previousBlockedChunk === null) {
1450 // $FlowFixMe[incompatible-type]
1451 controller.error(error);
1452 } else {
1453 const blockedChunk = previousBlockedChunk;
1454 // We shouldn't get any more enqueues after this so we can set it back to null.
1455 previousBlockedChunk = null;
1456 blockedChunk.then(() => controller.error(error as any));
1457 }
1458 },
1459 };
1460 resolveStream(response, id, stream, flightController);
1461 return stream;
1462 }
1463
1464 function FlightIterator(
1465 this: {next: (arg: void) => SomeChunk<IteratorResult<any, any>>, ...},
1466 next: (arg: void) => SomeChunk<IteratorResult<any, any>>,
1467 ) {
1468 this.next = next;
1469 // TODO: Add return/throw as options for aborting.
1470 }
1471 // TODO: The iterator could inherit the AsyncIterator prototype which is not exposed as
1472 // a global but exists as a prototype of an AsyncGenerator. However, it's not needed
1473 // to satisfy the iterable protocol.
1474 FlightIterator.prototype = {} as any;
1475 FlightIterator.prototype[ASYNC_ITERATOR] = function asyncIterator(
1476 this: $AsyncIterator<any, any, void>,
1477 ) {
1478 // Self referencing iterator.
1479 return this;
1480 };
1481
1482 function parseAsyncIterable<T>(
1483 response: Response,
1484 reference: string,
1485 iterator: boolean,
1486 parentObject: Object,
1487 parentKey: string,
1488 ): $AsyncIterable<T, T, void> | $AsyncIterator<T, T, void> {
1489 const id = parseInt(reference.slice(2), 16);
1490 const chunks = response._chunks;
1491 if (chunks.has(id)) {
1492 throw new Error('Already initialized stream.');
1493 }
1494
1495 const buffer: Array<SomeChunk<IteratorResult<T, T>>> = [];
1496 let closed = false;
1497 let nextWriteIndex = 0;
1498 const flightController = {
1499 enqueueModel(value: string): void {
1500 if (nextWriteIndex === buffer.length) {
1501 buffer[nextWriteIndex] = createResolvedIteratorResultChunk(
1502 response,
1503 value,
1504 false,
1505 );
1506 } else {
1507 resolveIteratorResultChunk(
1508 response,
1509 buffer[nextWriteIndex],
1510 value,
1511 false,
1512 );
1513 }
1514 nextWriteIndex++;
1515 },
1516 close(value: string): void {
1517 if (closed) {
1518 return;
1519 }
1520 closed = true;
1521 if (nextWriteIndex === buffer.length) {
1522 buffer[nextWriteIndex] = createResolvedIteratorResultChunk(
1523 response,
1524 value,
1525 true,
1526 );
1527 } else {
1528 resolveIteratorResultChunk(
1529 response,
1530 buffer[nextWriteIndex],
1531 value,
1532 true,
1533 );
1534 }
1535 nextWriteIndex++;
1536 while (nextWriteIndex < buffer.length) {
1537 // In generators, any extra reads from the iterator have the value undefined.
1538 resolveIteratorResultChunk(
1539 response,
1540 buffer[nextWriteIndex++],
1541 '"$undefined"',
1542 true,
1543 );
1544 }
1545 },
1546 error(error: Error): void {
1547 if (closed) {
1548 return;
1549 }
1550 closed = true;
1551 if (nextWriteIndex === buffer.length) {
1552 buffer[nextWriteIndex] =
1553 createPendingChunk<IteratorResult<T, T>>(response);
1554 }
1555 while (nextWriteIndex < buffer.length) {
1556 triggerErrorOnChunk(response, buffer[nextWriteIndex++], error);
1557 }
1558 },
1559 };
1560 const iterable: $AsyncIterable<T, T, void> = {
1561 [ASYNC_ITERATOR](): $AsyncIterator<T, T, void> {
1562 let nextReadIndex = 0;
1563 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
1564 return new FlightIterator((arg: void) => {
1565 if (arg !== undefined) {
1566 throw new Error(
1567 'Values cannot be passed to next() of AsyncIterables passed to Client Components.',
1568 );
1569 }
1570 if (nextReadIndex === buffer.length) {
1571 if (closed) {
1572 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
1573 return new ReactPromise(
1574 INITIALIZED,
1575 {done: true, value: undefined},
1576 null,
1577 );
1578 }
1579 buffer[nextReadIndex] =
1580 createPendingChunk<IteratorResult<T, T>>(response);
1581 }
1582 return buffer[nextReadIndex++];
1583 });
1584 },
1585 };
1586 // TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after
1587 // reading through the end, but currently we favor code size over this optimization.
1588 const stream = iterator ? iterable[ASYNC_ITERATOR]() : iterable;
1589 resolveStream(response, id, stream, flightController);
1590 return stream;
1591 }
1592
1593 function parseModelString(
1594 response: Response,
1595 obj: Object,
1596 key: string,
1597 value: string,
1598 reference: void | string,
1599 arrayRoot: null | NestedArrayContext,
1600 ): any {
1601 if (value[0] === '$') {
1602 switch (value[1]) {
1603 case '$': {
1604 // This was an escaped string value.
1605 if (arrayRoot !== null) {
1606 bumpArrayCount(arrayRoot, value.length - 1, response);
1607 }
1608 return value.slice(1);
1609 }
1610 case '@': {
1611 // Promise
1612 const id = parseInt(value.slice(2), 16);
1613 const chunk = getChunk(response, id);
1614 return chunk;
1615 }
1616 case 'h': {
1617 // Server Reference
1618 const ref = value.slice(2);
1619 return getOutlinedModel(
1620 response,
1621 ref,
1622 obj,
1623 key,
1624 null,
1625 loadServerReference,
1626 );
1627 }
1628 case 'T': {
1629 // Temporary Reference
1630 if (
1631 reference === undefined ||
1632 response._temporaryReferences === undefined
1633 ) {
1634 throw new Error(
1635 'Could not reference an opaque temporary reference. ' +
1636 'This is likely due to misconfiguring the temporaryReferences options ' +
1637 'on the server.',
1638 );
1639 }
1640 return createTemporaryReference(
1641 response._temporaryReferences,
1642 reference,
1643 );
1644 }
1645 case 'Q': {
1646 // Map
1647 const ref = value.slice(2);
1648 return getOutlinedModel(response, ref, obj, key, null, createMap);
1649 }
1650 case 'W': {
1651 // Set
1652 const ref = value.slice(2);
1653 return getOutlinedModel(response, ref, obj, key, null, createSet);
1654 }
1655 case 'K': {
1656 // FormData
1657 const stringId = value.slice(2);
1658
1659 const responsePrefix = response._prefix;
1660 // Use the special marker from the Client to distinguish keys that should
1661 // be consumed by referenced FormData.
1662 const anyFormPrefix = responsePrefix + '_';
1663 const formPrefix = anyFormPrefix + stringId + '_';
1664
1665 const data = new FormData();
1666 const backingFormData = response._formData;
1667 // We're still transpiling for-of loops, so we have to use the iterator directly instead of a for-of loop.
1668 while (true) {
1669 const formDataKey = peekBackingEntry(backingFormData);
1670 if (formDataKey === undefined) {
1671 break;
1672 }
1673 if (formDataKey.startsWith(formPrefix)) {
1674 const referencedFormDataValue = getAllBackingEntries(
1675 backingFormData,
1676 formDataKey,
1677 );
1678 const referencedFormDataKey = formDataKey.slice(formPrefix.length);
1679 for (let i = 0; i < referencedFormDataValue.length; i++) {
1680 // $FlowFixMe[incompatible-type]
1681 data.append(referencedFormDataKey, referencedFormDataValue[i]);
1682 }
1683 consumeBackingEntry(backingFormData, formDataKey);
1684 } else if (formDataKey.startsWith(anyFormPrefix)) {
1685 // The FormData values are continuous and before the FormData reference.
1686 // If we see something that doesn't look like a value for a referenced
1687 // FormData, we can assume we're past the values for this FormData
1688 // reference and stop iterating.
1689 break;
1690 } else {
1691 // Either an outlined value or something not owned by this Reply.
1692 advanceBackingEntryIterator(backingFormData);
1693 }
1694 }
1695 return data;
1696 }
1697 case 'i': {
1698 // Iterator
1699 const ref = value.slice(2);
1700 return getOutlinedModel(response, ref, obj, key, null, extractIterator);
1701 }
1702 case 'I': {
1703 // $Infinity
1704 return Infinity;
1705 }
1706 case '-': {
1707 // $-0 or $-Infinity
1708 if (value === '$-0') {
1709 return -0;
1710 } else {
1711 return -Infinity;
1712 }
1713 }
1714 case 'N': {
1715 // $NaN
1716 return NaN;
1717 }
1718 case 'u': {
1719 // matches "$undefined"
1720 // Special encoding for `undefined` which can't be serialized as JSON otherwise.
1721 return undefined;
1722 }
1723 case 'D': {
1724 // Date
1725 return new Date(Date.parse(value.slice(2)));
1726 }
1727 case 'n': {
1728 // BigInt
1729 const bigIntStr = value.slice(2);
1730 if (bigIntStr.length > MAX_BIGINT_DIGITS) {
1731 throw new Error(
1732 'BigInt is too large. Received ' +
1733 bigIntStr.length +
1734 ' digits but the limit is ' +
1735 MAX_BIGINT_DIGITS +
1736 '.',
1737 );
1738 }
1739 if (arrayRoot !== null) {
1740 bumpArrayCount(arrayRoot, bigIntStr.length, response);
1741 }
1742 return BigInt(bigIntStr);
1743 }
1744 case 'A':
1745 return parseTypedArray(
1746 response,
1747 value,
1748 ArrayBuffer,
1749 1,
1750 obj,
1751 key,
1752 arrayRoot,
1753 );
1754 case 'O':
1755 return parseTypedArray(
1756 response,
1757 value,
1758 Int8Array,
1759 1,
1760 obj,
1761 key,
1762 arrayRoot,
1763 );
1764 case 'o':
1765 return parseTypedArray(
1766 response,
1767 value,
1768 Uint8Array,
1769 1,
1770 obj,
1771 key,
1772 arrayRoot,
1773 );
1774 case 'U':
1775 return parseTypedArray(
1776 response,
1777 value,
1778 Uint8ClampedArray,
1779 1,
1780 obj,
1781 key,
1782 arrayRoot,
1783 );
1784 case 'S':
1785 return parseTypedArray(
1786 response,
1787 value,
1788 Int16Array,
1789 2,
1790 obj,
1791 key,
1792 arrayRoot,
1793 );
1794 case 's':
1795 return parseTypedArray(
1796 response,
1797 value,
1798 Uint16Array,
1799 2,
1800 obj,
1801 key,
1802 arrayRoot,
1803 );
1804 case 'L':
1805 return parseTypedArray(
1806 response,
1807 value,
1808 Int32Array,
1809 4,
1810 obj,
1811 key,
1812 arrayRoot,
1813 );
1814 case 'l':
1815 return parseTypedArray(
1816 response,
1817 value,
1818 Uint32Array,
1819 4,
1820 obj,
1821 key,
1822 arrayRoot,
1823 );
1824 case 'G':
1825 return parseTypedArray(
1826 response,
1827 value,
1828 Float32Array,
1829 4,
1830 obj,
1831 key,
1832 arrayRoot,
1833 );
1834 case 'g':
1835 return parseTypedArray(
1836 response,
1837 value,
1838 Float64Array,
1839 8,
1840 obj,
1841 key,
1842 arrayRoot,
1843 );
1844 case 'M':
1845 return parseTypedArray(
1846 response,
1847 value,
1848 BigInt64Array,
1849 8,
1850 obj,
1851 key,
1852 arrayRoot,
1853 );
1854 case 'm':
1855 return parseTypedArray(
1856 response,
1857 value,
1858 BigUint64Array,
1859 8,
1860 obj,
1861 key,
1862 arrayRoot,
1863 );
1864 case 'V':
1865 return parseTypedArray(
1866 response,
1867 value,
1868 DataView,
1869 1,
1870 obj,
1871 key,
1872 arrayRoot,
1873 );
1874 case 'B': {
1875 // Blob
1876 const id = parseInt(value.slice(2), 16);
1877 const prefix = response._prefix;
1878 const blobKey = prefix + id;
1879 // We should have this backingEntry in the store already because we emitted
1880 // it before referencing it. It should be a Blob.
1881 const backingEntry: Blob = getBackingEntry(
1882 response._formData,
1883 blobKey,
1884 ) as any;
1885 if (!(backingEntry instanceof Blob)) {
1886 throw new Error('Referenced Blob is not a Blob.');
1887 }
1888 return backingEntry;
1889 }
1890 case 'R': {
1891 return parseReadableStream(response, value, undefined, obj, key);
1892 }
1893 case 'r': {
1894 return parseReadableStream(response, value, 'bytes', obj, key);
1895 }
1896 case 'X': {
1897 return parseAsyncIterable(response, value, false, obj, key);
1898 }
1899 case 'x': {
1900 return parseAsyncIterable(response, value, true, obj, key);
1901 }
1902 }
1903 // We assume that anything else is a reference ID.
1904 const ref = value.slice(1);
1905 return getOutlinedModel(response, ref, obj, key, arrayRoot, createModel);
1906 }
1907 if (arrayRoot !== null) {
1908 bumpArrayCount(arrayRoot, value.length, response);
1909 }
1910 return value;
1911 }
1912
1913 const DEFAULT_MAX_ARRAY_NESTING = 1000000;
1914
1915 // Limit BigInt size to prevent CPU exhaustion from parsing very large values.
1916 // 300 digits covers most practical use cases (even 512-bit integers need only
1917 // ~154 digits) and aligns with the implicit limit from the Number approximation
1918 // checks in fulfillReference and getOutlinedModel.
1919 const MAX_BIGINT_DIGITS = 300;
1920
1921 export const MAX_BOUND_ARGS = 1000;
1922
1923 export function createResponse(
1924 bundlerConfig: ServerManifest,
1925 formFieldPrefix: string,
1926 temporaryReferences: void | TemporaryReferenceSet,
1927 backingFormData?: FormData = new FormData(),
1928 arraySizeLimit?: number = DEFAULT_MAX_ARRAY_NESTING,
1929 ): Response {
1930 const chunks: Map<number, SomeChunk<any>> = new Map();
1931
1932 const response: Response = {
1933 _bundlerConfig: bundlerConfig,
1934 _prefix: formFieldPrefix,
1935 _formData: createBackingFormData(backingFormData),
1936 _chunks: chunks,
1937 _closed: false,
1938 _closedReason: null,
1939 _temporaryReferences: temporaryReferences,
1940 _rootArrayContexts: new WeakMap(),
1941 _arraySizeLimit: arraySizeLimit,
1942 };
1943 return response;
1944 }
1945
1946 export function resolveField(
1947 response: Response,
1948 key: string,
1949 value: string,
1950 ): void {
1951 // Add this field to the backing store.
1952 appendBackingEntry(response._formData, key, value);
1953 const prefix = response._prefix;
1954 if (key.startsWith(prefix)) {
1955 const chunks = response._chunks;
1956 const id = +key.slice(prefix.length);
1957 const chunk = chunks.get(id);
1958 if (chunk) {
1959 // We were waiting on this key so now we can resolve it.
1960 resolveModelChunk(response, chunk, value, id);
1961 }
1962 }
1963 }
1964
1965 export function resolveFile(response: Response, key: string, file: File): void {
1966 // Add this field to the backing store.
1967 appendBackingEntry(response._formData, key, file);
1968 }
1969
1970 export opaque type FileHandle = {
1971 chunks: Array<Uint8Array>,
1972 filename: string,
1973 mime: string,
1974 };
1975
1976 export function resolveFileInfo(
1977 response: Response,
1978 key: string,
1979 filename: string,
1980 mime: string,
1981 ): FileHandle {
1982 return {
1983 chunks: [],
1984 filename,
1985 mime,
1986 };
1987 }
1988
1989 export function resolveFileChunk(
1990 response: Response,
1991 handle: FileHandle,
1992 chunk: Uint8Array,
1993 ): void {
1994 handle.chunks.push(chunk);
1995 }
1996
1997 export function resolveFileComplete(
1998 response: Response,
1999 key: string,
2000 handle: FileHandle,
2001 ): void {
2002 // Add this file to the backing store.
2003 // Node.js doesn't expose a global File constructor so we need to use
2004 // the append() form that takes the file name as the third argument,
2005 // to create a File object.
2006 const blob = new Blob(handle.chunks, {type: handle.mime});
2007 appendBackingFile(response._formData, key, blob, handle.filename);
2008 }
2009
2010 export function close(response: Response): void {
2011 // In case there are any remaining unresolved chunks, they won't
2012 // be resolved now. So we need to issue an error to those.
2013 // Ideally we should be able to early bail out if we kept a
2014 // ref count of pending chunks.
2015 reportGlobalError(response, new Error('Connection closed.'));
2016 }