72
73
import getComponentNameFromType from 'shared/getComponentNameFromType';
74
75
+import isArray from 'shared/isArray';
76
+
77
export type {CallServerCallback, EncodeFormActionCallback};
78
79
interface FlightStreamController {
103
104
const PENDING = 'pending';
105
const BLOCKED = 'blocked';
104
-const CYCLIC = 'cyclic';
106
const RESOLVED_MODEL = 'resolved_model';
107
const RESOLVED_MODULE = 'resolved_module';
108
const INITIALIZED = 'fulfilled';
124
_debugInfo?: null | ReactDebugInfo,
125
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
126
};
126
-type CyclicChunk<T> = {
127
- status: 'cyclic',
128
- value: null | Array<(T) => mixed>,
129
- reason: null | Array<(mixed) => mixed>,
130
- _response: Response,
131
- _debugInfo?: null | ReactDebugInfo,
132
- then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
133
-};
127
type ResolvedModelChunk<T> = {
128
status: 'resolved_model',
129
value: UninitializedModel,
169
type SomeChunk<T> =
170
| PendingChunk<T>
171
| BlockedChunk<T>
179
- | CyclicChunk<T>
172
| ResolvedModelChunk<T>
173
| ResolvedModuleChunk<T>
174
| InitializedChunk<T>
210
break;
211
case PENDING:
212
case BLOCKED:
221
- case CYCLIC:
213
if (resolve) {
214
if (chunk.value === null) {
215
chunk.value = ([]: Array<(T) => mixed>);
269
return chunk.value;
270
case PENDING:
271
case BLOCKED:
281
- case CYCLIC:
272
// eslint-disable-next-line no-throw-literal
273
throw ((chunk: any): Thenable<T>);
274
default:
317
break;
318
case PENDING:
319
case BLOCKED:
330
- case CYCLIC:
320
if (chunk.value) {
321
for (let i = 0; i < resolveListeners.length; i++) {
322
chunk.value.push(resolveListeners[i]);
490
}
491
}
492
504
-let initializingChunk: ResolvedModelChunk<any> = (null: any);
505
-let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
493
+type InitializationHandler = {
494
+ parent: null | InitializationHandler,
495
+ chunk: null | BlockedChunk<any>,
496
+ value: any,
497
+ deps: number,
498
+ errored: boolean,
499
+};
500
+let initializingHandler: null | InitializationHandler = null;
501
+
502
function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
507
- const prevChunk = initializingChunk;
508
- const prevBlocked = initializingChunkBlockedModel;
509
- initializingChunk = chunk;
510
- initializingChunkBlockedModel = null;
503
+ const prevHandler = initializingHandler;
504
+ initializingHandler = null;
505
506
const resolvedModel = chunk.value;
507
514
- // We go to the CYCLIC state until we've fully resolved this.
508
+ // We go to the BLOCKED state until we've fully resolved this.
509
// We do this before parsing in case we try to initialize the same chunk
510
// while parsing the model. Such as in a cyclic reference.
517
- const cyclicChunk: CyclicChunk<T> = (chunk: any);
518
- cyclicChunk.status = CYCLIC;
511
+ const cyclicChunk: BlockedChunk<T> = (chunk: any);
512
+ cyclicChunk.status = BLOCKED;
513
cyclicChunk.value = null;
514
cyclicChunk.reason = null;
515
516
try {
517
const value: T = parseModel(chunk._response, resolvedModel);
524
- if (
525
- initializingChunkBlockedModel !== null &&
526
- initializingChunkBlockedModel.deps > 0
527
- ) {
528
- initializingChunkBlockedModel.value = value;
529
- // We discovered new dependencies on modules that are not yet resolved.
530
- // We have to go the BLOCKED state until they're resolved.
531
- const blockedChunk: BlockedChunk<T> = (chunk: any);
532
- blockedChunk.status = BLOCKED;
533
- } else {
534
- const resolveListeners = cyclicChunk.value;
535
- const initializedChunk: InitializedChunk<T> = (chunk: any);
536
- initializedChunk.status = INITIALIZED;
537
- initializedChunk.value = value;
538
- if (resolveListeners !== null) {
539
- wakeChunk(resolveListeners, value);
518
+ // Invoke any listeners added while resolving this model. I.e. cyclic
519
+ // references. This may or may not fully resolve the model depending on
520
+ // if they were blocked.
521
+ const resolveListeners = cyclicChunk.value;
522
+ if (resolveListeners !== null) {
523
+ cyclicChunk.value = null;
524
+ cyclicChunk.reason = null;
525
+ wakeChunk(resolveListeners, value);
526
+ }
527
+ if (initializingHandler !== null) {
528
+ if (initializingHandler.errored) {
529
+ throw initializingHandler.value;
530
+ }
531
+ if (initializingHandler.deps > 0) {
532
+ // We discovered new dependencies on modules that are not yet resolved.
533
+ // We have to keep the BLOCKED state until they're resolved.
534
+ initializingHandler.value = value;
535
+ initializingHandler.chunk = cyclicChunk;
536
+ return;
537
}
538
}
539
+ const initializedChunk: InitializedChunk<T> = (chunk: any);
540
+ initializedChunk.status = INITIALIZED;
541
+ initializedChunk.value = value;
542
} catch (error) {
543
const erroredChunk: ErroredChunk<T> = (chunk: any);
544
erroredChunk.status = ERRORED;
545
erroredChunk.reason = error;
546
} finally {
547
- initializingChunk = prevChunk;
548
- initializingChunkBlockedModel = prevBlocked;
547
+ initializingHandler = prevHandler;
548
}
549
}
550
625
owner: null | ReactComponentInfo, // DEV-only
626
stack: null | string, // DEV-only
627
validated: number, // DEV-only
629
-): React$Element<any> {
628
+):
629
+ | React$Element<any>
630
+ | LazyComponent<React$Element<any>, SomeChunk<React$Element<any>>> {
631
let element: any;
632
if (__DEV__ && enableRefAsProp) {
633
// `ref` is non-enumerable in dev
724
value: task,
725
});
726
}
727
+ }
728
+
729
+ if (initializingHandler !== null) {
730
+ const handler = initializingHandler;
731
+ // We pop the stack to the previous outer handler before leaving the Element.
732
+ // This is effectively the complete phase.
733
+ initializingHandler = handler.parent;
734
+ if (handler.errored) {
735
+ // Something errored inside this Element's props. We can turn this Element
736
+ // into a Lazy so that we can still render up until that Lazy is rendered.
737
+ const erroredChunk: ErroredChunk<React$Element<any>> = createErrorChunk(
738
+ response,
739
+ handler.value,
740
+ );
741
+ if (__DEV__) {
742
+ // Conceptually the error happened inside this Element but right before
743
+ // it was rendered. We don't have a client side component to render but
744
+ // we can add some DebugInfo to explain that this was conceptually a
745
+ // Server side error that errored inside this element. That way any stack
746
+ // traces will point to the nearest JSX that errored - e.g. during
747
+ // serialization.
748
+ const erroredComponent: ReactComponentInfo = {
749
+ name: getComponentNameFromType(element.type) || '',
750
+ owner: element._owner,
751
+ };
752
+ if (enableOwnerStacks) {
753
+ // $FlowFixMe[cannot-write]
754
+ erroredComponent.stack = element._debugStack;
755
+ // $FlowFixMe[cannot-write]
756
+ erroredComponent.task = element._debugTask;
757
+ }
758
+ erroredChunk._debugInfo = [erroredComponent];
759
+ }
760
+ return createLazyChunkWrapper(erroredChunk);
761
+ }
762
+ if (handler.deps > 0) {
763
+ // We have blocked references inside this Element but we can turn this into
764
+ // a Lazy node referencing this Element to let everything around it proceed.
765
+ const blockedChunk: BlockedChunk<React$Element<any>> =
766
+ createBlockedChunk(response);
767
+ handler.value = element;
768
+ handler.chunk = blockedChunk;
769
+ if (__DEV__) {
770
+ const freeze = Object.freeze.bind(Object, element.props);
771
+ blockedChunk.then(freeze, freeze);
772
+ }
773
+ return createLazyChunkWrapper(blockedChunk);
774
+ }
775
+ } else if (__DEV__) {
776
// TODO: We should be freezing the element but currently, we might write into
777
// _debugInfo later. We could move it into _store which remains mutable.
728
- if (initializingChunkBlockedModel !== null) {
729
- const freeze = Object.freeze.bind(Object, element.props);
730
- initializingChunk.then(freeze, freeze);
731
- } else {
732
- Object.freeze(element.props);
733
- }
778
+ Object.freeze(element.props);
779
}
780
+
781
return element;
782
}
783
808
return chunk;
809
}
810
765
-function createModelResolver<T>(
766
- chunk: SomeChunk<T>,
811
+function waitForReference<T>(
812
+ referencedChunk: PendingChunk<T> | BlockedChunk<T>,
813
parentObject: Object,
814
key: string,
769
- cyclic: boolean,
815
response: Response,
816
map: (response: Response, model: any) => T,
817
path: Array<string>,
773
-): (value: any) => void {
774
- let blocked;
775
- if (initializingChunkBlockedModel) {
776
- blocked = initializingChunkBlockedModel;
777
- if (!cyclic) {
778
- blocked.deps++;
779
- }
818
+): T {
819
+ let handler: InitializationHandler;
820
+ if (initializingHandler) {
821
+ handler = initializingHandler;
822
+ handler.deps++;
823
} else {
781
- blocked = initializingChunkBlockedModel = {
782
- deps: cyclic ? 0 : 1,
783
- value: (null: any),
824
+ handler = initializingHandler = {
825
+ parent: null,
826
+ chunk: null,
827
+ value: null,
828
+ deps: 1,
829
+ errored: false,
830
};
831
}
786
- return value => {
832
+
833
+ function fulfill(value: any): void {
834
for (let i = 1; i < path.length; i++) {
835
+ while (value.$$typeof === REACT_LAZY_TYPE) {
836
+ // We never expect to see a Lazy node on this path because we encode those as
837
+ // separate models. This must mean that we have inserted an extra lazy node
838
+ // e.g. to replace a blocked element. We must instead look for it inside.
839
+ const chunk: SomeChunk<any> = value._payload;
840
+ if (chunk === handler.chunk) {
841
+ // This is a reference to the thing we're currently blocking. We can peak
842
+ // inside of it to get the value.
843
+ value = handler.value;
844
+ continue;
845
+ } else if (chunk.status === INITIALIZED) {
846
+ value = chunk.value;
847
+ continue;
848
+ } else {
849
+ // If we're not yet initialized we need to skip what we've already drilled
850
+ // through and then wait for the next value to become available.
851
+ path.splice(0, i - 1);
852
+ chunk.then(fulfill, reject);
853
+ return;
854
+ }
855
+ }
856
value = value[path[i]];
857
}
858
parentObject[key] = map(response, value);
859
792
- // If this is the root object for a model reference, where `blocked.value`
860
+ // If this is the root object for a model reference, where `handler.value`
861
// is a stale `null`, the resolved value can be used directly.
794
- if (key === '' && blocked.value === null) {
795
- blocked.value = parentObject[key];
862
+ if (key === '' && handler.value === null) {
863
+ handler.value = parentObject[key];
864
}
865
798
- blocked.deps--;
799
- if (blocked.deps === 0) {
800
- if (chunk.status !== BLOCKED) {
866
+ handler.deps--;
867
+
868
+ if (handler.deps === 0) {
869
+ const chunk = handler.chunk;
870
+ if (chunk === null || chunk.status !== BLOCKED) {
871
return;
872
}
873
const resolveListeners = chunk.value;
874
const initializedChunk: InitializedChunk<T> = (chunk: any);
875
initializedChunk.status = INITIALIZED;
806
- initializedChunk.value = blocked.value;
876
+ initializedChunk.value = handler.value;
877
if (resolveListeners !== null) {
808
- wakeChunk(resolveListeners, blocked.value);
878
+ wakeChunk(resolveListeners, handler.value);
879
}
880
}
811
- };
812
-}
881
+ }
882
+
883
+ function reject(error: mixed): void {
884
+ if (handler.errored) {
885
+ // We've already errored. We could instead build up an AggregateError
886
+ // but if there are multiple errors we just take the first one like
887
+ // Promise.all.
888
+ return;
889
+ }
890
+ const blockedValue = handler.value;
891
+ handler.errored = true;
892
+ handler.value = error;
893
+ const chunk = handler.chunk;
894
+ if (chunk === null || chunk.status !== BLOCKED) {
895
+ return;
896
+ }
897
814
-function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void {
815
- return (error: mixed) => triggerErrorOnChunk(chunk, error);
898
+ if (__DEV__) {
899
+ if (
900
+ typeof blockedValue === 'object' &&
901
+ blockedValue !== null &&
902
+ blockedValue.$$typeof === REACT_ELEMENT_TYPE
903
+ ) {
904
+ const element = blockedValue;
905
+ // Conceptually the error happened inside this Element but right before
906
+ // it was rendered. We don't have a client side component to render but
907
+ // we can add some DebugInfo to explain that this was conceptually a
908
+ // Server side error that errored inside this element. That way any stack
909
+ // traces will point to the nearest JSX that errored - e.g. during
910
+ // serialization.
911
+ const erroredComponent: ReactComponentInfo = {
912
+ name: getComponentNameFromType(element.type) || '',
913
+ owner: element._owner,
914
+ };
915
+ if (enableOwnerStacks) {
916
+ // $FlowFixMe[cannot-write]
917
+ erroredComponent.stack = element._debugStack;
918
+ // $FlowFixMe[cannot-write]
919
+ erroredComponent.task = element._debugTask;
920
+ }
921
+ const chunkDebugInfo: ReactDebugInfo =
922
+ chunk._debugInfo || (chunk._debugInfo = []);
923
+ chunkDebugInfo.push(erroredComponent);
924
+ }
925
+ }
926
+
927
+ triggerErrorOnChunk(chunk, error);
928
+ }
929
+
930
+ referencedChunk.then(fulfill, reject);
931
+
932
+ // Return a place holder value for now.
933
+ return (null: any);
934
}
935
936
function createServerReferenceProxy<A: Iterable<any>, T>(
998
if (
999
typeof chunkValue === 'object' &&
1000
chunkValue !== null &&
883
- (Array.isArray(chunkValue) ||
1001
+ (isArray(chunkValue) ||
1002
typeof chunkValue[ASYNC_ITERATOR] === 'function' ||
1003
chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
1004
!chunkValue._debugInfo
1016
return chunkValue;
1017
case PENDING:
1018
case BLOCKED:
901
- case CYCLIC:
902
- const parentChunk = initializingChunk;
903
- chunk.then(
904
- createModelResolver(
905
- parentChunk,
906
- parentObject,
907
- key,
908
- chunk.status === CYCLIC,
909
- response,
910
- map,
911
- path,
912
- ),
913
- createModelReject(parentChunk),
914
- );
915
- return (null: any);
1019
+ return waitForReference(chunk, parentObject, key, response, map, path);
1020
default:
917
- throw chunk.reason;
1021
+ // This is an error. Instead of erroring directly, we're going to encode this on
1022
+ // an initialization handler so that we can catch it at the nearest Element.
1023
+ if (initializingHandler) {
1024
+ initializingHandler.errored = true;
1025
+ initializingHandler.value = chunk.reason;
1026
+ } else {
1027
+ initializingHandler = {
1028
+ parent: null,
1029
+ chunk: null,
1030
+ value: chunk.reason,
1031
+ deps: 0,
1032
+ errored: true,
1033
+ };
1034
+ }
1035
+ // Placeholder
1036
+ return (null: any);
1037
}
1038
}
1039
1081
if (value[0] === '$') {
1082
if (value === '$') {
1083
// A very common symbol.
1084
+ if (initializingHandler !== null && key === '0') {
1085
+ // We we already have an initializing handler and we're abound to enter
1086
+ // a new element, we need to shadow it because we're now in a new scope.
1087
+ // This is effectively the "begin" or "push" phase of Element parsing.
1088
+ // We'll pop later when we parse the array itself.
1089
+ initializingHandler = {
1090
+ parent: initializingHandler,
1091
+ chunk: null,
1092
+ value: null,
1093
+ deps: 0,
1094
+ errored: false,
1095
+ };
1096
+ }
1097
return REACT_ELEMENT_TYPE;
1098
}
1099
switch (value[1]) {