@samitouri / QOS-React-1 / commits / b123b9c4f0

[Flight] Refactor the Render Loop to Behave More Like Fizz (#28065)

This refactors the Flight render loop to behave more like Fizz with similar naming conventions. So it's easier to apply similar techniques across both. This is not necessarily better/faster - at least not yet. This doesn't yet implement serialization by writing segments to chunks but we probably should do that since the built-in parts that `JSON.stringify` gets us isn't really much anymore (except serializing strings). When we switch to that it probably makes sense for the whole thing to be recursive. Right now it's not technically fully recursive because each recursive render returns the next JSON value to encode. So it's kind of like a trampoline. This means we can't have many contextual things on the stack. It needs to use the Server Context `__POP` trick. However, it does work for things that are contextual only for one sequence of server component abstractions in a row. Since those are now recursive. An interesting observation here is that `renderModel` means that anything can suspend while still serializing the outer siblings. Typically only Lazy or Components would suspend but in principle a Proxy can suspend/postpone too and now that is left serialized by reference to a future value. It's only if the thing that we rendered was something that can reduce to Lazy e.g. an Element that we can serialize it as a lazy. Similarly to how Suspense boundaries in Fizz can catch errors, anything that can be reduced to Lazy can also catch an error rather than bubbling it. It only errors when the Lazy resolves. Unlike Suspense boundaries though, those things don't render anything so they're otherwise going to use the destructive form. To ensure that throwing in an Element can reuse the current task, this must be handled by `renderModel`, not for example `renderElement`.

Sebastian Markbåge committed Jan 25, 2024 at 12:09 UTC b123b9c4f054a7def7ed84e350ccd46cc86672a6
3 files changed +322 -263
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+14
@@ -242,6 +242,20 @@ describe('ReactFlightDOMEdge', () => {
242 expect(result).toEqual(resolvedChildren);
243 });
244
245 + it('should execute repeated server components in a compact form', async () => {
246 + async function ServerComponent({recurse}) {
247 + if (recurse > 0) {
248 + return <ServerComponent recurse={recurse - 1} />;
249 + }
250 + return <div>Fin</div>;
251 + }
252 + const stream = ReactServerDOMServer.renderToReadableStream(
253 + <ServerComponent recurse={20} />,
254 + );
255 + const serializedContent = await readResult(stream);
256 + expect(serializedContent.length).toBeLessThan(150);
257 + });
258 +
259 // @gate enableBinaryFlight
260 it('should be able to serialize any kind of typed array', async () => {
261 const buffer = new Uint8Array([
packages/react-server/src/ReactFizzServer.js
+5 -1
@@ -2201,8 +2201,12 @@ function renderNodeDestructive(
2201 task.node = node;
2202 task.childIndex = childIndex;
2203
2204 + if (node === null) {
2205 + return;
2206 + }
2207 +
2208 // Handle object types
2205 - if (typeof node === 'object' && node !== null) {
2209 + if (typeof node === 'object') {
2210 switch ((node: any).$$typeof) {
2211 case REACT_ELEMENT_TYPE: {
2212 const element: any = node;
packages/react-server/src/ReactFlightServer.js
+303 -262
@@ -137,7 +137,7 @@ type ReactJSONValue =
137 | boolean
138 | number
139 | null
140 - | $ReadOnlyArray<ReactJSONValue>
140 + | $ReadOnlyArray<ReactClientValue>
141 | ReactClientObject;
142
143 // Serializable values
@@ -180,6 +180,7 @@ type Task = {
180 status: 0 | 1 | 3 | 4,
181 model: ReactClientValue,
182 ping: () => void,
183 + toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
184 context: ContextSnapshot,
185 thenableState: ThenableState | null,
186 };
@@ -212,7 +213,6 @@ export type Request = {
213 taintCleanupQueue: Array<string | bigint>,
214 onError: (error: mixed) => ?string,
215 onPostpone: (reason: string) => void,
215 - toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
216 };
217
218 const {
@@ -311,10 +311,6 @@ export function createRequest(
311 taintCleanupQueue: cleanupQueue,
312 onError: onError === undefined ? defaultErrorHandler : onError,
313 onPostpone: onPostpone === undefined ? defaultPostponeHandler : onPostpone,
314 - // $FlowFixMe[missing-this-annot]
315 - toJSON: function (key: string, value: ReactClientValue): ReactJSONValue {
316 - return resolveModelToJSON(request, this, key, value);
317 - },
314 };
315 request.pendingChunks++;
316 const rootContext = createRootContext(context);
@@ -504,14 +500,15 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
500 return lazyType;
501 }
502
507 -function attemptResolveElement(
503 +function renderElement(
504 request: Request,
505 + task: Task,
506 type: any,
507 key: null | React$Key,
508 ref: mixed,
509 props: any,
510 prevThenableState: ThenableState | null,
514 -): ReactClientValue {
511 +): ReactJSONValue {
512 if (ref !== null && ref !== undefined) {
513 // When the ref moves to the regular props object this will implicitly
514 // throw for functions. We could probably relax it to a DEV warning for other
@@ -533,7 +530,7 @@ function attemptResolveElement(
530 }
531 // This is a server-side component.
532 prepareToUseHooksForComponent(prevThenableState);
536 - const result = type(props);
533 + let result = type(props);
534 if (
535 typeof result === 'object' &&
536 result !== null &&
@@ -547,9 +544,9 @@ function attemptResolveElement(
544 }
545 // TODO: Once we accept Promises as children on the client, we can just return
546 // the thenable here.
550 - return createLazyWrapperAroundWakeable(result);
547 + result = createLazyWrapperAroundWakeable(result);
548 }
552 - return result;
549 + return renderModelDestructive(request, task, emptyRoot, '', result, null);
550 } else if (typeof type === 'string') {
551 // This is a host element. E.g. HTML.
552 return [REACT_ELEMENT_TYPE, type, key, props];
@@ -559,7 +556,14 @@ function attemptResolveElement(
556 // it as a wrapper.
557 // TODO: If a key is specified, we should propagate its key to any children.
558 // Same as if a Server Component has a key.
562 - return props.children;
559 + return renderModelDestructive(
560 + request,
561 + task,
562 + emptyRoot,
563 + '',
564 + props.children,
565 + null,
566 + );
567 }
568 // This might be a built-in React component. We'll let the client decide.
569 // Any built-in works as long as its props are serializable.
@@ -574,8 +578,9 @@ function attemptResolveElement(
578 const payload = type._payload;
579 const init = type._init;
580 const wrappedType = init(payload);
577 - return attemptResolveElement(
581 + return renderElement(
582 request,
583 + task,
584 wrappedType,
585 key,
586 ref,
@@ -586,11 +591,20 @@ function attemptResolveElement(
591 case REACT_FORWARD_REF_TYPE: {
592 const render = type.render;
593 prepareToUseHooksForComponent(prevThenableState);
589 - return render(props, undefined);
594 + const result = render(props, undefined);
595 + return renderModelDestructive(
596 + request,
597 + task,
598 + emptyRoot,
599 + '',
600 + result,
601 + null,
602 + );
603 }
604 case REACT_MEMO_TYPE: {
592 - return attemptResolveElement(
605 + return renderElement(
606 request,
607 + task,
608 type.type,
609 key,
610 ref,
@@ -600,7 +614,7 @@ function attemptResolveElement(
614 }
615 case REACT_PROVIDER_TYPE: {
616 if (enableServerContext) {
603 - pushProvider(type._context, props.value);
617 + task.context = pushProvider(type._context, props.value);
618 if (__DEV__) {
619 const extraKeys = Object.keys(props).filter(value => {
620 if (value === 'children' || value === 'value') {
@@ -648,12 +662,81 @@ function createTask(
662 abortSet: Set<Task>,
663 ): Task {
664 const id = request.nextChunkId++;
665 + if (typeof model === 'object' && model !== null) {
666 + // Register this model as having the ID we're about to write.
667 + request.writtenObjects.set(model, id);
668 + }
669 const task: Task = {
670 id,
671 status: PENDING,
672 model,
673 context,
674 ping: () => pingTask(request, task),
675 + toJSON: function (
676 + this:
677 + | {+[key: string | number]: ReactClientValue}
678 + | $ReadOnlyArray<ReactClientValue>,
679 + parentPropertyName: string,
680 + value: ReactClientValue,
681 + ): ReactJSONValue {
682 + const parent = this;
683 + // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
684 + if (__DEV__) {
685 + // $FlowFixMe[incompatible-use]
686 + const originalValue = parent[parentPropertyName];
687 + if (
688 + typeof originalValue === 'object' &&
689 + originalValue !== value &&
690 + !(originalValue instanceof Date)
691 + ) {
692 + if (objectName(originalValue) !== 'Object') {
693 + const jsxParentType = jsxChildrenParents.get(parent);
694 + if (typeof jsxParentType === 'string') {
695 + console.error(
696 + '%s objects cannot be rendered as text children. Try formatting it using toString().%s',
697 + objectName(originalValue),
698 + describeObjectForErrorMessage(parent, parentPropertyName),
699 + );
700 + } else {
701 + console.error(
702 + 'Only plain objects can be passed to Client Components from Server Components. ' +
703 + '%s objects are not supported.%s',
704 + objectName(originalValue),
705 + describeObjectForErrorMessage(parent, parentPropertyName),
706 + );
707 + }
708 + } else {
709 + console.error(
710 + 'Only plain objects can be passed to Client Components from Server Components. ' +
711 + 'Objects with toJSON methods are not supported. Convert it manually ' +
712 + 'to a simple value before passing it to props.%s',
713 + describeObjectForErrorMessage(parent, parentPropertyName),
714 + );
715 + }
716 + }
717 +
718 + if (
719 + enableServerContext &&
720 + parent[0] === REACT_ELEMENT_TYPE &&
721 + parent[1] &&
722 + (parent[1]: any).$$typeof === REACT_PROVIDER_TYPE &&
723 + parentPropertyName === '3'
724 + ) {
725 + insideContextProps = value;
726 + } else if (
727 + insideContextProps === parent &&
728 + parentPropertyName === 'value'
729 + ) {
730 + isInsideContextValue = true;
731 + } else if (
732 + insideContextProps === parent &&
733 + parentPropertyName === 'children'
734 + ) {
735 + isInsideContextValue = false;
736 + }
737 + }
738 + return renderModel(request, task, parent, parentPropertyName, value);
739 + },
740 thenableState: null,
741 };
742 abortSet.add(task);
@@ -733,9 +816,9 @@ function encodeReferenceChunk(
816 function serializeClientReference(
817 request: Request,
818 parent:
736 - | {+[key: string | number]: ReactClientValue}
819 + | {+[propertyName: string | number]: ReactClientValue}
820 | $ReadOnlyArray<ReactClientValue>,
738 - key: string,
821 + parentPropertyName: string,
822 clientReference: ClientReference<any>,
823 ): string {
824 const clientReferenceKey: ClientReferenceKey =
@@ -743,7 +826,7 @@ function serializeClientReference(
826 const writtenClientReferences = request.writtenClientReferences;
827 const existingId = writtenClientReferences.get(clientReferenceKey);
828 if (existingId !== undefined) {
746 - if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
829 + if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') {
830 // If we're encoding the "type" of an element, we can refer
831 // to that by a lazy reference instead of directly since React
832 // knows how to deal with lazy values. This lets us suspend
@@ -760,7 +843,7 @@ function serializeClientReference(
843 const importId = request.nextChunkId++;
844 emitImportChunk(request, importId, clientReferenceMetadata);
845 writtenClientReferences.set(clientReferenceKey, importId);
763 - if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
846 + if (parent[0] === REACT_ELEMENT_TYPE && parentPropertyName === '1') {
847 // If we're encoding the "type" of an element, we can refer
848 // to that by a lazy reference instead of directly since React
849 // knows how to deal with lazy values. This lets us suspend
@@ -778,7 +861,7 @@ function serializeClientReference(
861 }
862 }
863
781 -function outlineModel(request: Request, value: any): number {
864 +function outlineModel(request: Request, value: ReactClientValue): number {
865 request.pendingChunks++;
866 const newTask = createTask(
867 request,
@@ -792,10 +875,6 @@ function outlineModel(request: Request, value: any): number {
875
876 function serializeServerReference(
877 request: Request,
795 - parent:
796 - | {+[key: string | number]: ReactClientValue}
797 - | $ReadOnlyArray<ReactClientValue>,
798 - key: string,
878 serverReference: ServerReference<any>,
879 ): string {
880 const writtenServerReferences = request.writtenServerReferences;
@@ -911,166 +990,68 @@ let insideContextProps = null;
990 let isInsideContextValue = false;
991 let modelRoot: null | ReactClientValue = false;
992
914 -function resolveModelToJSON(
993 +function renderModel(
994 request: Request,
995 + task: Task,
996 parent:
997 | {+[key: string | number]: ReactClientValue}
998 | $ReadOnlyArray<ReactClientValue>,
999 key: string,
1000 value: ReactClientValue,
1001 ): ReactJSONValue {
922 - // Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
923 - if (__DEV__) {
924 - // $FlowFixMe[incompatible-use]
925 - const originalValue = parent[key];
926 - if (
927 - typeof originalValue === 'object' &&
928 - originalValue !== value &&
929 - !(originalValue instanceof Date)
930 - ) {
931 - if (objectName(originalValue) !== 'Object') {
932 - const jsxParentType = jsxChildrenParents.get(parent);
933 - if (typeof jsxParentType === 'string') {
934 - console.error(
935 - '%s objects cannot be rendered as text children. Try formatting it using toString().%s',
936 - objectName(originalValue),
937 - describeObjectForErrorMessage(parent, key),
938 - );
939 - } else {
940 - console.error(
941 - 'Only plain objects can be passed to Client Components from Server Components. ' +
942 - '%s objects are not supported.%s',
943 - objectName(originalValue),
944 - describeObjectForErrorMessage(parent, key),
945 - );
946 - }
947 - } else {
948 - console.error(
949 - 'Only plain objects can be passed to Client Components from Server Components. ' +
950 - 'Objects with toJSON methods are not supported. Convert it manually ' +
951 - 'to a simple value before passing it to props.%s',
952 - describeObjectForErrorMessage(parent, key),
1002 + try {
1003 + return renderModelDestructive(request, task, parent, key, value, null);
1004 + } catch (thrownValue) {
1005 + const x =
1006 + thrownValue === SuspenseException
1007 + ? // This is a special type of exception used for Suspense. For historical
1008 + // reasons, the rest of the Suspense implementation expects the thrown
1009 + // value to be a thenable, because before `use` existed that was the
1010 + // (unstable) API for suspending. This implementation detail can change
1011 + // later, once we deprecate the old API in favor of `use`.
1012 + getSuspendedThenable()
1013 + : thrownValue;
1014 + // If the suspended/errored value was an element or lazy it can be reduced
1015 + // to a lazy reference, so that it doesn't error the parent.
1016 + const model = task.model;
1017 + const wasReactNode =
1018 + typeof model === 'object' &&
1019 + model !== null &&
1020 + ((model: any).$$typeof === REACT_ELEMENT_TYPE ||
1021 + (model: any).$$typeof === REACT_LAZY_TYPE);
1022 + if (typeof x === 'object' && x !== null) {
1023 + // $FlowFixMe[method-unbinding]
1024 + if (typeof x.then === 'function') {
1025 + // Something suspended, we'll need to create a new task and resolve it later.
1026 + request.pendingChunks++;
1027 + const newTask = createTask(
1028 + request,
1029 + task.model,
1030 + getActiveContext(),
1031 + request.abortableTasks,
1032 );
954 - }
955 - }
956 - }
957 -
958 - // Special Symbols
959 - switch (value) {
960 - case REACT_ELEMENT_TYPE:
961 - return '$';
962 - }
963 -
964 - if (__DEV__) {
965 - if (
966 - enableServerContext &&
967 - parent[0] === REACT_ELEMENT_TYPE &&
968 - parent[1] &&
969 - (parent[1]: any).$$typeof === REACT_PROVIDER_TYPE &&
970 - key === '3'
971 - ) {
972 - insideContextProps = value;
973 - } else if (insideContextProps === parent && key === 'value') {
974 - isInsideContextValue = true;
975 - } else if (insideContextProps === parent && key === 'children') {
976 - isInsideContextValue = false;
977 - }
978 - }
979 -
980 - // Resolve Server Components.
981 - while (
982 - typeof value === 'object' &&
983 - value !== null &&
984 - ((value: any).$$typeof === REACT_ELEMENT_TYPE ||
985 - (value: any).$$typeof === REACT_LAZY_TYPE)
986 - ) {
987 - if (__DEV__) {
988 - if (enableServerContext && isInsideContextValue) {
989 - console.error('React elements are not allowed in ServerContext');
990 - }
991 - }
992 -
993 - try {
994 - switch ((value: any).$$typeof) {
995 - case REACT_ELEMENT_TYPE: {
996 - const writtenObjects = request.writtenObjects;
997 - const existingId = writtenObjects.get(value);
998 - if (existingId !== undefined) {
999 - if (existingId === -1) {
1000 - // Seen but not yet outlined.
1001 - const newId = outlineModel(request, value);
1002 - return serializeByValueID(newId);
1003 - } else if (modelRoot === value) {
1004 - // This is the ID we're currently emitting so we need to write it
1005 - // once but if we discover it again, we refer to it by id.
1006 - modelRoot = null;
1007 - } else {
1008 - // We've already emitted this as an outlined object, so we can
1009 - // just refer to that by its existing ID.
1010 - return serializeByValueID(existingId);
1011 - }
1012 - } else {
1013 - // This is the first time we've seen this object. We may never see it again
1014 - // so we'll inline it. Mark it as seen. If we see it again, we'll outline.
1015 - writtenObjects.set(value, -1);
1016 - }
1017 -
1018 - // TODO: Concatenate keys of parents onto children.
1019 - const element: React$Element<any> = (value: any);
1020 - // Attempt to render the Server Component.
1021 - value = attemptResolveElement(
1022 - request,
1023 - element.type,
1024 - element.key,
1025 - element.ref,
1026 - element.props,
1027 - null,
1028 - );
1029 - break;
1030 - }
1031 - case REACT_LAZY_TYPE: {
1032 - const payload = (value: any)._payload;
1033 - const init = (value: any)._init;
1034 - value = init(payload);
1035 - break;
1036 - }
1037 - }
1038 - } catch (thrownValue) {
1039 - const x =
1040 - thrownValue === SuspenseException
1041 - ? // This is a special type of exception used for Suspense. For historical
1042 - // reasons, the rest of the Suspense implementation expects the thrown
1043 - // value to be a thenable, because before `use` existed that was the
1044 - // (unstable) API for suspending. This implementation detail can change
1045 - // later, once we deprecate the old API in favor of `use`.
1046 - getSuspendedThenable()
1047 - : thrownValue;
1048 - if (typeof x === 'object' && x !== null) {
1049 - // $FlowFixMe[method-unbinding]
1050 - if (typeof x.then === 'function') {
1051 - // Something suspended, we'll need to create a new task and resolve it later.
1052 - request.pendingChunks++;
1053 - const newTask = createTask(
1054 - request,
1055 - value,
1056 - getActiveContext(),
1057 - request.abortableTasks,
1058 - );
1059 - const ping = newTask.ping;
1060 - x.then(ping, ping);
1061 - newTask.thenableState = getThenableStateAfterSuspending();
1033 + const ping = newTask.ping;
1034 + (x: any).then(ping, ping);
1035 + newTask.thenableState = getThenableStateAfterSuspending();
1036 + if (wasReactNode) {
1037 return serializeLazyID(newTask.id);
1063 - } else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
1064 - // Something postponed. We'll still send everything we have up until this point.
1065 - // We'll replace this element with a lazy reference that postpones on the client.
1066 - const postponeInstance: Postpone = (x: any);
1067 - request.pendingChunks++;
1068 - const postponeId = request.nextChunkId++;
1069 - logPostpone(request, postponeInstance.message);
1070 - emitPostponeChunk(request, postponeId, postponeInstance);
1038 + }
1039 + return serializeByValueID(newTask.id);
1040 + } else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
1041 + // Something postponed. We'll still send everything we have up until this point.
1042 + // We'll replace this element with a lazy reference that postpones on the client.
1043 + const postponeInstance: Postpone = (x: any);
1044 + request.pendingChunks++;
1045 + const postponeId = request.nextChunkId++;
1046 + logPostpone(request, postponeInstance.message);
1047 + emitPostponeChunk(request, postponeId, postponeInstance);
1048 + if (wasReactNode) {
1049 return serializeLazyID(postponeId);
1050 }
1051 + return serializeByValueID(postponeId);
1052 }
1053 + }
1054 + if (wasReactNode) {
1055 // Something errored. We'll still send everything we have up until this point.
1056 // We'll replace this element with a lazy reference that throws on the client
1057 // once it gets rendered.
@@ -1080,6 +1061,29 @@ function resolveModelToJSON(
1061 emitErrorChunk(request, errorId, digest, x);
1062 return serializeLazyID(errorId);
1063 }
1064 + // Something errored but it was not in a React Node. There's no need to serialize
1065 + // it by value because it'll just error the whole parent row anyway so we can
1066 + // just stop any siblings and error the whole parent row.
1067 + throw x;
1068 + }
1069 +}
1070 +
1071 +function renderModelDestructive(
1072 + request: Request,
1073 + task: Task,
1074 + parent:
1075 + | {+[propertyName: string | number]: ReactClientValue}
1076 + | $ReadOnlyArray<ReactClientValue>,
1077 + parentPropertyName: string,
1078 + value: ReactClientValue,
1079 + prevThenableState: ThenableState | null,
1080 +): ReactJSONValue {
1081 + // Set the currently rendering model
1082 + task.model = value;
1083 +
1084 + // Special Symbol, that's very common.
1085 + if (value === REACT_ELEMENT_TYPE) {
1086 + return '$';
1087 }
1088
1089 if (value === null) {
@@ -1087,15 +1091,78 @@ function resolveModelToJSON(
1091 }
1092
1093 if (typeof value === 'object') {
1094 + switch ((value: any).$$typeof) {
1095 + case REACT_ELEMENT_TYPE: {
1096 + if (__DEV__) {
1097 + if (enableServerContext && isInsideContextValue) {
1098 + console.error('React elements are not allowed in ServerContext');
1099 + }
1100 + }
1101 + const writtenObjects = request.writtenObjects;
1102 + const existingId = writtenObjects.get(value);
1103 + if (existingId !== undefined) {
1104 + if (existingId === -1) {
1105 + // Seen but not yet outlined.
1106 + const newId = outlineModel(request, value);
1107 + return serializeByValueID(newId);
1108 + } else if (modelRoot === value) {
1109 + // This is the ID we're currently emitting so we need to write it
1110 + // once but if we discover it again, we refer to it by id.
1111 + modelRoot = null;
1112 + } else {
1113 + // We've already emitted this as an outlined object, so we can
1114 + // just refer to that by its existing ID.
1115 + return serializeByValueID(existingId);
1116 + }
1117 + } else {
1118 + // This is the first time we've seen this object. We may never see it again
1119 + // so we'll inline it. Mark it as seen. If we see it again, we'll outline.
1120 + writtenObjects.set(value, -1);
1121 + }
1122 +
1123 + // TODO: Concatenate keys of parents onto children.
1124 + const element: React$Element<any> = (value: any);
1125 + // Attempt to render the Server Component.
1126 + return renderElement(
1127 + request,
1128 + task,
1129 + element.type,
1130 + element.key,
1131 + element.ref,
1132 + element.props,
1133 + prevThenableState,
1134 + );
1135 + }
1136 + case REACT_LAZY_TYPE: {
1137 + const payload = (value: any)._payload;
1138 + const init = (value: any)._init;
1139 + const resolvedModel = init(payload);
1140 + return renderModelDestructive(
1141 + request,
1142 + task,
1143 + emptyRoot,
1144 + '',
1145 + resolvedModel,
1146 + null,
1147 + );
1148 + }
1149 + }
1150 +
1151 + if (isClientReference(value)) {
1152 + return serializeClientReference(
1153 + request,
1154 + parent,
1155 + parentPropertyName,
1156 + (value: any),
1157 + );
1158 + }
1159 +
1160 if (enableTaint) {
1161 const tainted = TaintRegistryObjects.get(value);
1162 if (tainted !== undefined) {
1163 throwTaintViolation(tainted);
1164 }
1165 }
1096 - if (isClientReference(value)) {
1097 - return serializeClientReference(request, parent, key, (value: any));
1098 - }
1166
1167 const writtenObjects = request.writtenObjects;
1168 const existingId = writtenObjects.get(value);
@@ -1123,7 +1190,7 @@ function resolveModelToJSON(
1190 const providerKey = ((value: any): ReactProviderType<any>)._context
1191 ._globalName;
1192 const writtenProviders = request.writtenProviders;
1126 - let providerId = writtenProviders.get(key);
1193 + let providerId = writtenProviders.get(providerKey);
1194 if (providerId === undefined) {
1195 request.pendingChunks++;
1196 providerId = request.nextChunkId++;
@@ -1132,7 +1199,7 @@ function resolveModelToJSON(
1199 }
1200 return serializeByValueID(providerId);
1201 } else if (value === POP) {
1135 - popProvider();
1202 + task.context = popProvider();
1203 if (__DEV__) {
1204 insideContextProps = null;
1205 isInsideContextValue = false;
@@ -1249,13 +1316,13 @@ function resolveModelToJSON(
1316 'Only plain objects can be passed to Client Components from Server Components. ' +
1317 '%s objects are not supported.%s',
1318 objectName(value),
1252 - describeObjectForErrorMessage(parent, key),
1319 + describeObjectForErrorMessage(parent, parentPropertyName),
1320 );
1321 } else if (!isSimpleObject(value)) {
1322 console.error(
1323 'Only plain objects can be passed to Client Components from Server Components. ' +
1324 'Classes or other objects with methods are not supported.%s',
1258 - describeObjectForErrorMessage(parent, key),
1325 + describeObjectForErrorMessage(parent, parentPropertyName),
1326 );
1327 } else if (Object.getOwnPropertySymbols) {
1328 const symbols = Object.getOwnPropertySymbols(value);
@@ -1264,7 +1331,7 @@ function resolveModelToJSON(
1331 'Only plain objects can be passed to Client Components from Server Components. ' +
1332 'Objects with symbol properties like %s are not supported.%s',
1333 symbols[0].description,
1267 - describeObjectForErrorMessage(parent, key),
1334 + describeObjectForErrorMessage(parent, parentPropertyName),
1335 );
1336 }
1337 }
@@ -1285,7 +1352,7 @@ function resolveModelToJSON(
1352 if (value[value.length - 1] === 'Z') {
1353 // Possibly a Date, whose toJSON automatically calls toISOString
1354 // $FlowFixMe[incompatible-use]
1288 - const originalValue = parent[key];
1355 + const originalValue = parent[parentPropertyName];
1356 if (originalValue instanceof Date) {
1357 return serializeDateFromDateJSON(value);
1358 }
@@ -1312,29 +1379,36 @@ function resolveModelToJSON(
1379 }
1380
1381 if (typeof value === 'function') {
1382 + if (isClientReference(value)) {
1383 + return serializeClientReference(
1384 + request,
1385 + parent,
1386 + parentPropertyName,
1387 + (value: any),
1388 + );
1389 + }
1390 + if (isServerReference(value)) {
1391 + return serializeServerReference(request, (value: any));
1392 + }
1393 +
1394 if (enableTaint) {
1395 const tainted = TaintRegistryObjects.get(value);
1396 if (tainted !== undefined) {
1397 throwTaintViolation(tainted);
1398 }
1399 }
1321 - if (isClientReference(value)) {
1322 - return serializeClientReference(request, parent, key, (value: any));
1323 - }
1324 - if (isServerReference(value)) {
1325 - return serializeServerReference(request, parent, key, (value: any));
1326 - }
1327 - if (/^on[A-Z]/.test(key)) {
1400 +
1401 + if (/^on[A-Z]/.test(parentPropertyName)) {
1402 throw new Error(
1403 'Event handlers cannot be passed to Client Component props.' +
1330 - describeObjectForErrorMessage(parent, key) +
1404 + describeObjectForErrorMessage(parent, parentPropertyName) +
1405 '\nIf you need interactivity, consider converting part of this to a Client Component.',
1406 );
1407 } else {
1408 throw new Error(
1409 'Functions cannot be passed directly to Client Components ' +
1410 'unless you explicitly expose it by marking it with "use server".' +
1337 - describeObjectForErrorMessage(parent, key),
1411 + describeObjectForErrorMessage(parent, parentPropertyName),
1412 );
1413 }
1414 }
@@ -1355,7 +1429,7 @@ function resolveModelToJSON(
1429 // $FlowFixMe[incompatible-type] `description` might be undefined
1430 value.description
1431 }) cannot be found among global symbols.` +
1358 - describeObjectForErrorMessage(parent, key),
1432 + describeObjectForErrorMessage(parent, parentPropertyName),
1433 );
1434 }
1435
@@ -1378,7 +1452,7 @@ function resolveModelToJSON(
1452
1453 throw new Error(
1454 `Type ${typeof value} is not supported in Client Component props.` +
1381 - describeObjectForErrorMessage(parent, key),
1455 + describeObjectForErrorMessage(parent, parentPropertyName),
1456 );
1457 }
1458
@@ -1508,22 +1582,14 @@ function emitProviderChunk(
1582 request.completedRegularChunks.push(processedChunk);
1583 }
1584
1511 -function emitModelChunk(
1512 - request: Request,
1513 - id: number,
1514 - model: ReactClientValue,
1515 -): void {
1516 - // Track the root so we know that we have to emit this object even though it
1517 - // already has an ID. This is needed because we might see this object twice
1518 - // in the same toJSON if it is cyclic.
1519 - modelRoot = model;
1520 - // $FlowFixMe[incompatible-type] stringify can return null
1521 - const json: string = stringify(model, request.toJSON);
1585 +function emitModelChunk(request: Request, id: number, json: string): void {
1586 const row = id.toString(16) + ':' + json + '\n';
1587 const processedChunk = stringToChunk(row);
1588 request.completedRegularChunks.push(processedChunk);
1589 }
1590
1591 +const emptyRoot = {};
1592 +
1593 function retryTask(request: Request, task: Task): void {
1594 if (task.status !== PENDING) {
1595 // We completed this by other means before we had a chance to retry it.
@@ -1532,67 +1598,42 @@ function retryTask(request: Request, task: Task): void {
1598
1599 switchContext(task.context);
1600 try {
1535 - let value = task.model;
1536 - if (
1537 - typeof value === 'object' &&
1538 - value !== null &&
1539 - (value: any).$$typeof === REACT_ELEMENT_TYPE
1540 - ) {
1541 - request.writtenObjects.set(value, task.id);
1542 -
1543 - // TODO: Concatenate keys of parents onto children.
1544 - const element: React$Element<any> = (value: any);
1545 -
1546 - // When retrying a component, reuse the thenableState from the
1547 - // previous attempt.
1548 - const prevThenableState = task.thenableState;
1549 -
1550 - // Attempt to render the Server Component.
1551 - // Doing this here lets us reuse this same task if the next component
1552 - // also suspends.
1553 - task.model = value;
1554 - value = attemptResolveElement(
1555 - request,
1556 - element.type,
1557 - element.key,
1558 - element.ref,
1559 - element.props,
1560 - prevThenableState,
1561 - );
1601 + // Reset the task's thenable state before continuing, so that if a later
1602 + // component suspends we can reuse the same task object. If the same
1603 + // component suspends again, the thenable state will be restored.
1604 + const prevThenableState = task.thenableState;
1605 + task.thenableState = null;
1606 +
1607 + // Track the root so we know that we have to emit this object even though it
1608 + // already has an ID. This is needed because we might see this object twice
1609 + // in the same toJSON if it is cyclic.
1610 + modelRoot = task.model;
1611 +
1612 + // We call the destructive form that mutates this task. That way if something
1613 + // suspends again, we can reuse the same task instead of spawning a new one.
1614 + const resolvedModel = renderModelDestructive(
1615 + request,
1616 + task,
1617 + emptyRoot,
1618 + '',
1619 + task.model,
1620 + prevThenableState,
1621 + );
1622
1563 - // Successfully finished this component. We're going to keep rendering
1564 - // using the same task, but we reset its thenable state before continuing.
1565 - task.thenableState = null;
1566 -
1567 - // Keep rendering and reuse the same task. This inner loop is separate
1568 - // from the render above because we don't need to reset the thenable state
1569 - // until the next time something suspends and retries.
1570 - while (
1571 - typeof value === 'object' &&
1572 - value !== null &&
1573 - (value: any).$$typeof === REACT_ELEMENT_TYPE
1574 - ) {
1575 - request.writtenObjects.set(value, task.id);
1576 - // TODO: Concatenate keys of parents onto children.
1577 - const nextElement: React$Element<any> = (value: any);
1578 - task.model = value;
1579 - value = attemptResolveElement(
1580 - request,
1581 - nextElement.type,
1582 - nextElement.key,
1583 - nextElement.ref,
1584 - nextElement.props,
1585 - null,
1586 - );
1587 - }
1588 - }
1623 + // Track the root again for the resolved object.
1624 + modelRoot = resolvedModel;
1625
1590 - // Track that this object is outlined and has an id.
1591 - if (typeof value === 'object' && value !== null) {
1592 - request.writtenObjects.set(value, task.id);
1593 - }
1626 + // If the value is a string, it means it's a terminal value adn we already escaped it
1627 + // We don't need to escape it again so it's not passed the toJSON replacer.
1628 + // Object might contain unresolved values like additional elements.
1629 + // This is simulating what the JSON loop would do if this was part of it.
1630 + // $FlowFixMe[incompatible-type] stringify can return null
1631 + const json: string =
1632 + typeof resolvedModel === 'string'
1633 + ? stringify(resolvedModel)
1634 + : stringify(resolvedModel, task.toJSON);
1635 + emitModelChunk(request, task.id, json);
1636
1595 - emitModelChunk(request, task.id, value);
1637 request.abortableTasks.delete(task);
1638 task.status = COMPLETED;
1639 } catch (thrownValue) {