@samitouri / QOS-React / commits / 6c409acefd

[Flight Reply] Encode Objects Returned to the Client by Reference (#29010)

Stacked on #28997. We can use the technique of referencing an object by its row + property name path for temporary references - like we do for deduping. That way we don't need to generate an ID for temporary references. Instead, they can just be an opaque marker in the slot and it has the implicit ID of the row + path. Then we can stash all objects, even the ones that are actually available to read on the server, as temporary references. Without adding anything to the payload since the IDs are implicit. If the same object is returned to the client, it can be referenced by reference instead of serializing it back to the client. This also helps preserve object identity. We assume that the objects are immutable when they pass the boundary. I'm not sure if this is worth it but with this mechanism, if you return the `FormData` payload from a `useActionState` it doesn't have to be serialized on the way back to the client. This is a common pattern for having access to the last submission as "default value" to the form fields. However you can still control it by replacing it with another object if you want. In MPA mode, the temporary references are not configured and so it needs to be serialized in that case. That's required anyway for hydration purposes. I'm not sure if people will actually use this in practice though or if FormData will always be destructured into some other object like with a library that turns it into typed data, and back. If so, the object identity is lost.

Sebastian Markbåge committed May 9, 2024 at 20:00 UTC 6c409acefde29d6ef87dfb208716ec3272bd3c54
16 files changed +492 -118
packages/react-client/src/ReactFlightClient.js
+2 -2
@@ -915,7 +915,7 @@ function parseModelString(
915 }
916 case 'T': {
917 // Temporary Reference
918 - const id = parseInt(value.slice(2), 16);
918 + const reference = '$' + value.slice(2);
919 const temporaryReferences = response._tempRefs;
920 if (temporaryReferences == null) {
921 throw new Error(
@@ -923,7 +923,7 @@ function parseModelString(
923 'Pass a temporaryReference option with the set that was used with the reply.',
924 );
925 }
926 - return readTemporaryReference(temporaryReferences, id);
926 + return readTemporaryReference(temporaryReferences, reference);
927 }
928 case 'Q': {
929 // Map
packages/react-client/src/ReactFlightReplyClient.js
+61 -31
@@ -109,8 +109,8 @@ function serializeServerReferenceID(id: number): string {
109 return '$F' + id.toString(16);
110 }
111
112 -function serializeTemporaryReferenceID(id: number): string {
113 - return '$T' + id.toString(16);
112 +function serializeTemporaryReferenceMarker(): string {
113 + return '$T';
114 }
115
116 function serializeFormDataReference(id: number): string {
@@ -405,15 +405,22 @@ export function processReply(
405 if (typeof value === 'object') {
406 switch ((value: any).$$typeof) {
407 case REACT_ELEMENT_TYPE: {
408 - if (temporaryReferences === undefined) {
409 - throw new Error(
410 - 'React Element cannot be passed to Server Functions from the Client without a ' +
411 - 'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
412 - (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
413 - );
408 + if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
409 + // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
410 + const parentReference = writtenObjects.get(parent);
411 + if (parentReference !== undefined) {
412 + // If the parent has a reference, we can refer to this object indirectly
413 + // through the property name inside that parent.
414 + const reference = parentReference + ':' + key;
415 + // Store this object so that the server can refer to it later in responses.
416 + writeTemporaryReference(temporaryReferences, reference, value);
417 + return serializeTemporaryReferenceMarker();
418 + }
419 }
415 - return serializeTemporaryReferenceID(
416 - writeTemporaryReference(temporaryReferences, value),
420 + throw new Error(
421 + 'React Element cannot be passed to Server Functions from the Client without a ' +
422 + 'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
423 + (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
424 );
425 }
426 case REACT_LAZY_TYPE: {
@@ -529,7 +536,12 @@ export function processReply(
536 if (parentReference !== undefined) {
537 // If the parent has a reference, we can refer to this object indirectly
538 // through the property name inside that parent.
532 - writtenObjects.set(value, parentReference + ':' + key);
539 + const reference = parentReference + ':' + key;
540 + writtenObjects.set(value, reference);
541 + if (temporaryReferences !== undefined) {
542 + // Store this object so that the server can refer to it later in responses.
543 + writeTemporaryReference(temporaryReferences, reference, value);
544 + }
545 }
546 }
547
@@ -693,10 +705,9 @@ export function processReply(
705 'Classes or null prototypes are not supported.',
706 );
707 }
696 - // We can serialize class instances as temporary references.
697 - return serializeTemporaryReferenceID(
698 - writeTemporaryReference(temporaryReferences, value),
699 - );
708 + // We will have written this object to the temporary reference set above
709 + // so we can replace it with a marker to refer to this slot later.
710 + return serializeTemporaryReferenceMarker();
711 }
712 if (__DEV__) {
713 if (
@@ -777,27 +788,41 @@ export function processReply(
788 formData.set(formFieldPrefix + refId, metaDataJSON);
789 return serializeServerReferenceID(refId);
790 }
780 - if (temporaryReferences === undefined) {
781 - throw new Error(
782 - 'Client Functions cannot be passed directly to Server Functions. ' +
783 - 'Only Functions passed from the Server can be passed back again.',
784 - );
791 + if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
792 + // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
793 + const parentReference = writtenObjects.get(parent);
794 + if (parentReference !== undefined) {
795 + // If the parent has a reference, we can refer to this object indirectly
796 + // through the property name inside that parent.
797 + const reference = parentReference + ':' + key;
798 + // Store this object so that the server can refer to it later in responses.
799 + writeTemporaryReference(temporaryReferences, reference, value);
800 + return serializeTemporaryReferenceMarker();
801 + }
802 }
786 - return serializeTemporaryReferenceID(
787 - writeTemporaryReference(temporaryReferences, value),
803 + throw new Error(
804 + 'Client Functions cannot be passed directly to Server Functions. ' +
805 + 'Only Functions passed from the Server can be passed back again.',
806 );
807 }
808
809 if (typeof value === 'symbol') {
792 - if (temporaryReferences === undefined) {
793 - throw new Error(
794 - 'Symbols cannot be passed to a Server Function without a ' +
795 - 'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
796 - (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
797 - );
810 + if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
811 + // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
812 + const parentReference = writtenObjects.get(parent);
813 + if (parentReference !== undefined) {
814 + // If the parent has a reference, we can refer to this object indirectly
815 + // through the property name inside that parent.
816 + const reference = parentReference + ':' + key;
817 + // Store this object so that the server can refer to it later in responses.
818 + writeTemporaryReference(temporaryReferences, reference, value);
819 + return serializeTemporaryReferenceMarker();
820 + }
821 }
799 - return serializeTemporaryReferenceID(
800 - writeTemporaryReference(temporaryReferences, value),
822 + throw new Error(
823 + 'Symbols cannot be passed to a Server Function without a ' +
824 + 'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
825 + (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
826 );
827 }
828
@@ -812,7 +837,12 @@ export function processReply(
837
838 function serializeModel(model: ReactServerValue, id: number): string {
839 if (typeof model === 'object' && model !== null) {
815 - writtenObjects.set(model, serializeByValueID(id));
840 + const reference = serializeByValueID(id);
841 + writtenObjects.set(model, reference);
842 + if (temporaryReferences !== undefined) {
843 + // Store this object so that the server can refer to it later in responses.
844 + writeTemporaryReference(temporaryReferences, reference, model);
845 + }
846 }
847 modelRoot = model;
848 // $FlowFixMe[incompatible-return] it's not going to be undefined because we'll encode it.
packages/react-client/src/ReactFlightTemporaryReferences.js
+7 -17
@@ -9,33 +9,23 @@
9
10 interface Reference {}
11
12 -export opaque type TemporaryReferenceSet = Array<Reference | symbol>;
12 +export opaque type TemporaryReferenceSet = Map<string, Reference | symbol>;
13
14 export function createTemporaryReferenceSet(): TemporaryReferenceSet {
15 - return [];
15 + return new Map();
16 }
17
18 export function writeTemporaryReference(
19 set: TemporaryReferenceSet,
20 + reference: string,
21 object: Reference | symbol,
21 -): number {
22 - // We always create a new entry regardless if we've already written the same
23 - // object. This ensures that we always generate a deterministic encoding of
24 - // each slot in the reply for cacheability.
25 - const newId = set.length;
26 - set.push(object);
27 - return newId;
22 +): void {
23 + set.set(reference, object);
24 }
25
26 export function readTemporaryReference<T>(
27 set: TemporaryReferenceSet,
32 - id: number,
28 + reference: string,
29 ): T {
34 - if (id < 0 || id >= set.length) {
35 - throw new Error(
36 - "The RSC response contained a reference that doesn't exist in the temporary reference set. " +
37 - 'Always pass the matching set that was used to create the reply when parsing its response.',
38 - );
39 - }
40 - return (set[id]: any);
30 + return (set.get(reference): any);
31 }
packages/react-server-dom-esm/src/ReactFlightDOMServerNode.js
+34 -2
@@ -47,15 +47,30 @@ export {
47 registerClientReference,
48 } from './ReactFlightESMReferences';
49
50 +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
51 +
52 +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
53 +
54 +export type {TemporaryReferenceSet};
55 +
56 function createDrainHandler(destination: Destination, request: Request) {
57 return () => startFlowing(request, destination);
58 }
59
60 +function createCancelHandler(request: Request, reason: string) {
61 + return () => {
62 + stopFlowing(request);
63 + // eslint-disable-next-line react-internal/prod-error-codes
64 + abort(request, new Error(reason));
65 + };
66 +}
67 +
68 type Options = {
69 environmentName?: string,
70 onError?: (error: mixed) => void,
71 onPostpone?: (reason: string) => void,
72 identifierPrefix?: string,
73 + temporaryReferences?: TemporaryReferenceSet,
74 };
75
76 type PipeableStream = {
@@ -75,6 +90,7 @@ function renderToPipeableStream(
90 options ? options.identifierPrefix : undefined,
91 options ? options.onPostpone : undefined,
92 options ? options.environmentName : undefined,
93 + options ? options.temporaryReferences : undefined,
94 );
95 let hasStartedFlowing = false;
96 startWork(request);
@@ -88,10 +104,20 @@ function renderToPipeableStream(
104 hasStartedFlowing = true;
105 startFlowing(request, destination);
106 destination.on('drain', createDrainHandler(destination, request));
107 + destination.on(
108 + 'error',
109 + createCancelHandler(
110 + request,
111 + 'The destination stream errored while writing data.',
112 + ),
113 + );
114 + destination.on(
115 + 'close',
116 + createCancelHandler(request, 'The destination stream closed early.'),
117 + );
118 return destination;
119 },
120 abort(reason: mixed) {
94 - stopFlowing(request);
121 abort(request, reason);
122 },
123 };
@@ -155,13 +181,19 @@ function decodeReplyFromBusboy<T>(
181 function decodeReply<T>(
182 body: string | FormData,
183 moduleBasePath: ServerManifest,
184 + options?: {temporaryReferences?: TemporaryReferenceSet},
185 ): Thenable<T> {
186 if (typeof body === 'string') {
187 const form = new FormData();
188 form.append('0', body);
189 body = form;
190 }
164 - const response = createResponse(moduleBasePath, '', body);
191 + const response = createResponse(
192 + moduleBasePath,
193 + '',
194 + options ? options.temporaryReferences : undefined,
195 + body,
196 + );
197 const root = getRoot<T>(response);
198 close(response);
199 return root;
packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js
+25 -4
@@ -16,6 +16,7 @@ import {
16 createRequest,
17 startWork,
18 startFlowing,
19 + stopFlowing,
20 abort,
21 } from 'react-server/src/ReactFlightServer';
22
@@ -25,7 +26,10 @@ import {
26 getRoot,
27 } from 'react-server/src/ReactFlightReplyServer';
28
28 -import {decodeAction} from 'react-server/src/ReactFlightActionServer';
29 +import {
30 + decodeAction,
31 + decodeFormState,
32 +} from 'react-server/src/ReactFlightActionServer';
33
34 export {
35 registerServerReference,
@@ -33,10 +37,17 @@ export {
37 createClientModuleProxy,
38 } from './ReactFlightTurbopackReferences';
39
40 +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
41 +
42 +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
43 +
44 +export type {TemporaryReferenceSet};
45 +
46 type Options = {
47 environmentName?: string,
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 + temporaryReferences?: TemporaryReferenceSet,
51 onError?: (error: mixed) => void,
52 onPostpone?: (reason: string) => void,
53 };
@@ -53,6 +64,7 @@ function renderToReadableStream(
64 options ? options.identifierPrefix : undefined,
65 options ? options.onPostpone : undefined,
66 options ? options.environmentName : undefined,
67 + options ? options.temporaryReferences : undefined,
68 );
69 if (options && options.signal) {
70 const signal = options.signal;
@@ -75,7 +87,10 @@ function renderToReadableStream(
87 pull: (controller): ?Promise<void> => {
88 startFlowing(request, controller);
89 },
78 - cancel: (reason): ?Promise<void> => {},
90 + cancel: (reason): ?Promise<void> => {
91 + stopFlowing(request);
92 + abort(request, reason);
93 + },
94 },
95 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
96 {highWaterMark: 0},
@@ -86,16 +101,22 @@ function renderToReadableStream(
101 function decodeReply<T>(
102 body: string | FormData,
103 turbopackMap: ServerManifest,
104 + options?: {temporaryReferences?: TemporaryReferenceSet},
105 ): Thenable<T> {
106 if (typeof body === 'string') {
107 const form = new FormData();
108 form.append('0', body);
109 body = form;
110 }
95 - const response = createResponse(turbopackMap, '', body);
111 + const response = createResponse(
112 + turbopackMap,
113 + '',
114 + options ? options.temporaryReferences : undefined,
115 + body,
116 + );
117 const root = getRoot<T>(response);
118 close(response);
119 return root;
120 }
121
101 -export {renderToReadableStream, decodeReply, decodeAction};
122 +export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js
+25 -4
@@ -16,6 +16,7 @@ import {
16 createRequest,
17 startWork,
18 startFlowing,
19 + stopFlowing,
20 abort,
21 } from 'react-server/src/ReactFlightServer';
22
@@ -25,7 +26,10 @@ import {
26 getRoot,
27 } from 'react-server/src/ReactFlightReplyServer';
28
28 -import {decodeAction} from 'react-server/src/ReactFlightActionServer';
29 +import {
30 + decodeAction,
31 + decodeFormState,
32 +} from 'react-server/src/ReactFlightActionServer';
33
34 export {
35 registerServerReference,
@@ -33,10 +37,17 @@ export {
37 createClientModuleProxy,
38 } from './ReactFlightTurbopackReferences';
39
40 +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
41 +
42 +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
43 +
44 +export type {TemporaryReferenceSet};
45 +
46 type Options = {
47 environmentName?: string,
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 + temporaryReferences?: TemporaryReferenceSet,
51 onError?: (error: mixed) => void,
52 onPostpone?: (reason: string) => void,
53 };
@@ -53,6 +64,7 @@ function renderToReadableStream(
64 options ? options.identifierPrefix : undefined,
65 options ? options.onPostpone : undefined,
66 options ? options.environmentName : undefined,
67 + options ? options.temporaryReferences : undefined,
68 );
69 if (options && options.signal) {
70 const signal = options.signal;
@@ -75,7 +87,10 @@ function renderToReadableStream(
87 pull: (controller): ?Promise<void> => {
88 startFlowing(request, controller);
89 },
78 - cancel: (reason): ?Promise<void> => {},
90 + cancel: (reason): ?Promise<void> => {
91 + stopFlowing(request);
92 + abort(request, reason);
93 + },
94 },
95 // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
96 {highWaterMark: 0},
@@ -86,16 +101,22 @@ function renderToReadableStream(
101 function decodeReply<T>(
102 body: string | FormData,
103 turbopackMap: ServerManifest,
104 + options?: {temporaryReferences?: TemporaryReferenceSet},
105 ): Thenable<T> {
106 if (typeof body === 'string') {
107 const form = new FormData();
108 form.append('0', body);
109 body = form;
110 }
95 - const response = createResponse(turbopackMap, '', body);
111 + const response = createResponse(
112 + turbopackMap,
113 + '',
114 + options ? options.temporaryReferences : undefined,
115 + body,
116 + );
117 const root = getRoot<T>(response);
118 close(response);
119 return root;
120 }
121
101 -export {renderToReadableStream, decodeReply, decodeAction};
122 +export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js
+40 -2
@@ -22,6 +22,7 @@ import {
22 createRequest,
23 startWork,
24 startFlowing,
25 + stopFlowing,
26 abort,
27 } from 'react-server/src/ReactFlightServer';
28
@@ -36,7 +37,10 @@ import {
37 getRoot,
38 } from 'react-server/src/ReactFlightReplyServer';
39
39 -import {decodeAction} from 'react-server/src/ReactFlightActionServer';
40 +import {
41 + decodeAction,
42 + decodeFormState,
43 +} from 'react-server/src/ReactFlightActionServer';
44
45 export {
46 registerServerReference,
@@ -44,15 +48,30 @@ export {
48 createClientModuleProxy,
49 } from './ReactFlightTurbopackReferences';
50
51 +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
52 +
53 +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
54 +
55 +export type {TemporaryReferenceSet};
56 +
57 function createDrainHandler(destination: Destination, request: Request) {
58 return () => startFlowing(request, destination);
59 }
60
61 +function createCancelHandler(request: Request, reason: string) {
62 + return () => {
63 + stopFlowing(request);
64 + // eslint-disable-next-line react-internal/prod-error-codes
65 + abort(request, new Error(reason));
66 + };
67 +}
68 +
69 type Options = {
70 environmentName?: string,
71 onError?: (error: mixed) => void,
72 onPostpone?: (reason: string) => void,
73 identifierPrefix?: string,
74 + temporaryReferences?: TemporaryReferenceSet,
75 };
76
77 type PipeableStream = {
@@ -72,6 +91,7 @@ function renderToPipeableStream(
91 options ? options.identifierPrefix : undefined,
92 options ? options.onPostpone : undefined,
93 options ? options.environmentName : undefined,
94 + options ? options.temporaryReferences : undefined,
95 );
96 let hasStartedFlowing = false;
97 startWork(request);
@@ -85,6 +105,17 @@ function renderToPipeableStream(
105 hasStartedFlowing = true;
106 startFlowing(request, destination);
107 destination.on('drain', createDrainHandler(destination, request));
108 + destination.on(
109 + 'error',
110 + createCancelHandler(
111 + request,
112 + 'The destination stream errored while writing data.',
113 + ),
114 + );
115 + destination.on(
116 + 'close',
117 + createCancelHandler(request, 'The destination stream closed early.'),
118 + );
119 return destination;
120 },
121 abort(reason: mixed) {
@@ -151,13 +182,19 @@ function decodeReplyFromBusboy<T>(
182 function decodeReply<T>(
183 body: string | FormData,
184 turbopackMap: ServerManifest,
185 + options?: {temporaryReferences?: TemporaryReferenceSet},
186 ): Thenable<T> {
187 if (typeof body === 'string') {
188 const form = new FormData();
189 form.append('0', body);
190 body = form;
191 }
160 - const response = createResponse(turbopackMap, '', body);
192 + const response = createResponse(
193 + turbopackMap,
194 + '',
195 + options ? options.temporaryReferences : undefined,
196 + body,
197 + );
198 const root = getRoot<T>(response);
199 close(response);
200 return root;
@@ -168,4 +205,5 @@ export {
205 decodeReplyFromBusboy,
206 decodeReply,
207 decodeAction,
208 + decodeFormState,
209 };
packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js
+15 -1
@@ -37,10 +37,17 @@ export {
37 createClientModuleProxy,
38 } from './ReactFlightWebpackReferences';
39
40 +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
41 +
42 +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
43 +
44 +export type {TemporaryReferenceSet};
45 +
46 type Options = {
47 environmentName?: string,
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 + temporaryReferences?: TemporaryReferenceSet,
51 onError?: (error: mixed) => void,
52 onPostpone?: (reason: string) => void,
53 };
@@ -57,6 +64,7 @@ function renderToReadableStream(
64 options ? options.identifierPrefix : undefined,
65 options ? options.onPostpone : undefined,
66 options ? options.environmentName : undefined,
67 + options ? options.temporaryReferences : undefined,
68 );
69 if (options && options.signal) {
70 const signal = options.signal;
@@ -93,13 +101,19 @@ function renderToReadableStream(
101 function decodeReply<T>(
102 body: string | FormData,
103 webpackMap: ServerManifest,
104 + options?: {temporaryReferences?: TemporaryReferenceSet},
105 ): Thenable<T> {
106 if (typeof body === 'string') {
107 const form = new FormData();
108 form.append('0', body);
109 body = form;
110 }
102 - const response = createResponse(webpackMap, '', body);
111 + const response = createResponse(
112 + webpackMap,
113 + '',
114 + options ? options.temporaryReferences : undefined,
115 + body,
116 + );
117 const root = getRoot<T>(response);
118 close(response);
119 return root;
packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js
+15 -1
@@ -37,10 +37,17 @@ export {
37 createClientModuleProxy,
38 } from './ReactFlightWebpackReferences';
39
40 +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
41 +
42 +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
43 +
44 +export type {TemporaryReferenceSet};
45 +
46 type Options = {
47 environmentName?: string,
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 + temporaryReferences?: TemporaryReferenceSet,
51 onError?: (error: mixed) => void,
52 onPostpone?: (reason: string) => void,
53 };
@@ -57,6 +64,7 @@ function renderToReadableStream(
64 options ? options.identifierPrefix : undefined,
65 options ? options.onPostpone : undefined,
66 options ? options.environmentName : undefined,
67 + options ? options.temporaryReferences : undefined,
68 );
69 if (options && options.signal) {
70 const signal = options.signal;
@@ -93,13 +101,19 @@ function renderToReadableStream(
101 function decodeReply<T>(
102 body: string | FormData,
103 webpackMap: ServerManifest,
104 + options?: {temporaryReferences?: TemporaryReferenceSet},
105 ): Thenable<T> {
106 if (typeof body === 'string') {
107 const form = new FormData();
108 form.append('0', body);
109 body = form;
110 }
102 - const response = createResponse(webpackMap, '', body);
111 + const response = createResponse(
112 + webpackMap,
113 + '',
114 + options ? options.temporaryReferences : undefined,
115 + body,
116 + );
117 const root = getRoot<T>(response);
118 close(response);
119 return root;
packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js
+15 -1
@@ -48,6 +48,12 @@ export {
48 createClientModuleProxy,
49 } from './ReactFlightWebpackReferences';
50
51 +import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
52 +
53 +export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
54 +
55 +export type {TemporaryReferenceSet};
56 +
57 function createDrainHandler(destination: Destination, request: Request) {
58 return () => startFlowing(request, destination);
59 }
@@ -65,6 +71,7 @@ type Options = {
71 onError?: (error: mixed) => void,
72 onPostpone?: (reason: string) => void,
73 identifierPrefix?: string,
74 + temporaryReferences?: TemporaryReferenceSet,
75 };
76
77 type PipeableStream = {
@@ -84,6 +91,7 @@ function renderToPipeableStream(
91 options ? options.identifierPrefix : undefined,
92 options ? options.onPostpone : undefined,
93 options ? options.environmentName : undefined,
94 + options ? options.temporaryReferences : undefined,
95 );
96 let hasStartedFlowing = false;
97 startWork(request);
@@ -174,13 +182,19 @@ function decodeReplyFromBusboy<T>(
182 function decodeReply<T>(
183 body: string | FormData,
184 webpackMap: ServerManifest,
185 + options?: {temporaryReferences?: TemporaryReferenceSet},
186 ): Thenable<T> {
187 if (typeof body === 'string') {
188 const form = new FormData();
189 form.append('0', body);
190 body = form;
191 }
183 - const response = createResponse(webpackMap, '', body);
192 + const response = createResponse(
193 + webpackMap,
194 + '',
195 + options ? options.temporaryReferences : undefined,
196 + body,
197 + );
198 const root = getRoot<T>(response);
199 close(response);
200 return root;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
+53 -1
@@ -361,11 +361,21 @@ describe('ReactFlightDOMReply', () => {
361 temporaryReferences,
362 },
363 );
364 +
365 + const temporaryReferencesServer =
366 + ReactServerDOMServer.createTemporaryReferenceSet();
367 const serverPayload = await ReactServerDOMServer.decodeReply(
368 body,
369 webpackServerMap,
370 + {temporaryReferences: temporaryReferencesServer},
371 + );
372 + const stream = ReactServerDOMServer.renderToReadableStream(
373 + serverPayload,
374 + null,
375 + {
376 + temporaryReferences: temporaryReferencesServer,
377 + },
378 );
368 - const stream = ReactServerDOMServer.renderToReadableStream(serverPayload);
379 const response = await ReactServerDOMClient.createFromReadableStream(
380 stream,
381 {
@@ -377,6 +387,48 @@ describe('ReactFlightDOMReply', () => {
387 expect(response.children).toBe(children);
388 });
389
390 + it('can return the same object using temporary references', async () => {
391 + const obj = {
392 + this: {is: 'a large object'},
393 + with: {many: 'properties in it'},
394 + };
395 +
396 + const root = {obj};
397 +
398 + const temporaryReferences =
399 + ReactServerDOMClient.createTemporaryReferenceSet();
400 + const body = await ReactServerDOMClient.encodeReply(root, {
401 + temporaryReferences,
402 + });
403 +
404 + const temporaryReferencesServer =
405 + ReactServerDOMServer.createTemporaryReferenceSet();
406 + const serverPayload = await ReactServerDOMServer.decodeReply(
407 + body,
408 + webpackServerMap,
409 + {temporaryReferences: temporaryReferencesServer},
410 + );
411 + const stream = ReactServerDOMServer.renderToReadableStream(
412 + {
413 + root: serverPayload,
414 + obj: serverPayload.obj,
415 + },
416 + null,
417 + {temporaryReferences: temporaryReferencesServer},
418 + );
419 + const response = await ReactServerDOMClient.createFromReadableStream(
420 + stream,
421 + {
422 + temporaryReferences,
423 + },
424 + );
425 +
426 + // This should've been the same reference that we already saw because
427 + // we returned it by reference.
428 + expect(response.root).toBe(root);
429 + expect(response.obj).toBe(obj);
430 + });
431 +
432 // @gate enableFlightReadableStream
433 it('should supports streaming ReadableStream with objects', async () => {
434 let controller1;
packages/react-server/src/ReactFlightActionServer.js
+6 -1
@@ -59,7 +59,12 @@ function decodeBoundActionMetaData(
59 formFieldPrefix: string,
60 ): {id: ServerReferenceId, bound: null | Promise<Array<any>>} {
61 // The data for this reference is encoded in multiple fields under this prefix.
62 - const actionResponse = createResponse(serverManifest, formFieldPrefix, body);
62 + const actionResponse = createResponse(
63 + serverManifest,
64 + formFieldPrefix,
65 + undefined,
66 + body,
67 + );
68 close(actionResponse);
69 const refPromise = getRoot<{
70 id: ServerReferenceId,
packages/react-server/src/ReactFlightReplyServer.js
+111 -19
@@ -18,19 +18,26 @@ import type {
18 ClientReference as ServerReference,
19 } from 'react-client/src/ReactFlightClientConfig';
20
21 +import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences';
22 +
23 import {
24 resolveServerReference,
25 preloadModule,
26 requireModule,
27 } from 'react-client/src/ReactFlightClientConfig';
28
27 -import {createTemporaryReference} from './ReactFlightServerTemporaryReferences';
29 +import {
30 + createTemporaryReference,
31 + registerTemporaryReference,
32 +} from './ReactFlightServerTemporaryReferences';
33 import {
34 enableBinaryFlight,
35 enableFlightReadableStream,
36 } from 'shared/ReactFeatureFlags';
37 import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
38
39 +import hasOwnProperty from 'shared/hasOwnProperty';
40 +
41 interface FlightStreamController {
42 enqueueModel(json: string): void;
43 close(json: string): void;
@@ -76,7 +83,7 @@ type CyclicChunk<T> = {
83 type ResolvedModelChunk<T> = {
84 status: 'resolved_model',
85 value: string,
79 - reason: null,
86 + reason: number,
87 _response: Response,
88 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
89 };
@@ -166,7 +173,7 @@ export type Response = {
173 _prefix: string,
174 _formData: FormData,
175 _chunks: Map<number, SomeChunk<any>>,
169 - _fromJSON: (key: string, value: JSONValue) => any,
176 + _temporaryReferences: void | TemporaryReferenceSet,
177 };
178
179 export function getRoot<T>(response: Response): Thenable<T> {
@@ -233,12 +240,17 @@ function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void {
240 function createResolvedModelChunk<T>(
241 response: Response,
242 value: string,
243 + id: number,
244 ): ResolvedModelChunk<T> {
245 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
238 - return new Chunk(RESOLVED_MODEL, value, null, response);
246 + return new Chunk(RESOLVED_MODEL, value, id, response);
247 }
248
241 -function resolveModelChunk<T>(chunk: SomeChunk<T>, value: string): void {
249 +function resolveModelChunk<T>(
250 + chunk: SomeChunk<T>,
251 + value: string,
252 + id: number,
253 +): void {
254 if (chunk.status !== PENDING) {
255 if (enableFlightReadableStream) {
256 // If we get more data to an already resolved ID, we assume that it's
@@ -258,6 +270,7 @@ function resolveModelChunk<T>(chunk: SomeChunk<T>, value: string): void {
270 const resolvedChunk: ResolvedModelChunk<T> = (chunk: any);
271 resolvedChunk.status = RESOLVED_MODEL;
272 resolvedChunk.value = value;
273 + resolvedChunk.reason = id;
274 if (resolveListeners !== null) {
275 // This is unfortunate that we're reading this eagerly if
276 // we already have listeners attached since they might no
@@ -290,7 +303,7 @@ function createResolvedIteratorResultChunk<T>(
303 const iteratorResultJSON =
304 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
305 // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
293 - return new Chunk(RESOLVED_MODEL, iteratorResultJSON, null, response);
306 + return new Chunk(RESOLVED_MODEL, iteratorResultJSON, -1, response);
307 }
308
309 function resolveIteratorResultChunk<T>(
@@ -301,7 +314,7 @@ function resolveIteratorResultChunk<T>(
314 // To reuse code as much code as possible we add the wrapper element as part of the JSON.
315 const iteratorResultJSON =
316 (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}';
304 - resolveModelChunk(chunk, iteratorResultJSON);
317 + resolveModelChunk(chunk, iteratorResultJSON, -1);
318 }
319
320 function bindArgs(fn: any, args: any) {
@@ -353,6 +366,64 @@ function loadServerReference<T>(
366 return (null: any);
367 }
368
369 +function reviveModel(
370 + response: Response,
371 + parentObj: any,
372 + parentKey: string,
373 + value: JSONValue,
374 + reference: void | string,
375 +): any {
376 + if (typeof value === 'string') {
377 + // We can't use .bind here because we need the "this" value.
378 + return parseModelString(response, parentObj, parentKey, value, reference);
379 + }
380 + if (typeof value === 'object' && value !== null) {
381 + if (
382 + reference !== undefined &&
383 + response._temporaryReferences !== undefined
384 + ) {
385 + // Store this object's reference in case it's returned later.
386 + registerTemporaryReference(
387 + response._temporaryReferences,
388 + value,
389 + reference,
390 + );
391 + }
392 + if (Array.isArray(value)) {
393 + for (let i = 0; i < value.length; i++) {
394 + const childRef =
395 + reference !== undefined ? reference + ':' + i : undefined;
396 + // $FlowFixMe[cannot-write]
397 + value[i] = reviveModel(response, value, '' + i, value[i], childRef);
398 + }
399 + } else {
400 + for (const key in value) {
401 + if (hasOwnProperty.call(value, key)) {
402 + const childRef =
403 + reference !== undefined && key.indexOf(':') === -1
404 + ? reference + ':' + key
405 + : undefined;
406 + const newValue = reviveModel(
407 + response,
408 + value,
409 + key,
410 + value[key],
411 + childRef,
412 + );
413 + if (newValue !== undefined) {
414 + // $FlowFixMe[cannot-write]
415 + value[key] = newValue;
416 + } else {
417 + // $FlowFixMe[cannot-write]
418 + delete value[key];
419 + }
420 + }
421 + }
422 + }
423 + }
424 + return value;
425 +}
426 +
427 let initializingChunk: ResolvedModelChunk<any> = (null: any);
428 let initializingChunkBlockedModel: null | {deps: number, value: any} = null;
429 function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
@@ -361,6 +432,9 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
432 initializingChunk = chunk;
433 initializingChunkBlockedModel = null;
434
435 + const rootReference =
436 + chunk.reason === -1 ? undefined : chunk.reason.toString(16);
437 +
438 const resolvedModel = chunk.value;
439
440 // We go to the CYCLIC state until we've fully resolved this.
@@ -372,7 +446,15 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
446 cyclicChunk.reason = null;
447
448 try {
375 - const value: T = JSON.parse(resolvedModel, chunk._response._fromJSON);
449 + const rawModel = JSON.parse(resolvedModel);
450 +
451 + const value: T = reviveModel(
452 + chunk._response,
453 + {'': rawModel},
454 + '',
455 + rawModel,
456 + rootReference,
457 + );
458 if (
459 initializingChunkBlockedModel !== null &&
460 initializingChunkBlockedModel.deps > 0
@@ -426,7 +508,7 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
508 const backingEntry = response._formData.get(key);
509 if (backingEntry != null) {
510 // We assume that this is a string entry for now.
429 - chunk = createResolvedModelChunk(response, (backingEntry: any));
511 + chunk = createResolvedModelChunk(response, (backingEntry: any), id);
512 } else {
513 // We're still waiting on this entry to stream in.
514 chunk = createPendingChunk(response);
@@ -643,6 +725,7 @@ function parseReadableStream<T>(
725 const chunk: ResolvedModelChunk<T> = createResolvedModelChunk(
726 response,
727 json,
728 + -1,
729 );
730 initializeModelChunk(chunk);
731 const initializedChunk: SomeChunk<T> = chunk;
@@ -670,7 +753,7 @@ function parseReadableStream<T>(
753 // to synchronous emitting.
754 previousBlockedChunk = null;
755 }
673 - resolveModelChunk(chunk, json);
756 + resolveModelChunk(chunk, json, -1);
757 });
758 }
759 },
@@ -814,6 +897,7 @@ function parseModelString(
897 obj: Object,
898 key: string,
899 value: string,
900 + reference: void | string,
901 ): any {
902 if (value[0] === '$') {
903 switch (value[1]) {
@@ -844,7 +928,20 @@ function parseModelString(
928 }
929 case 'T': {
930 // Temporary Reference
847 - return createTemporaryReference(value.slice(2));
931 + if (
932 + reference === undefined ||
933 + response._temporaryReferences === undefined
934 + ) {
935 + throw new Error(
936 + 'Could not reference an opaque temporary reference. ' +
937 + 'This is likely due to misconfiguring the temporaryReferences options ' +
938 + 'on the server.',
939 + );
940 + }
941 + return createTemporaryReference(
942 + response._temporaryReferences,
943 + reference,
944 + );
945 }
946 case 'Q': {
947 // Map
@@ -982,6 +1079,7 @@ function parseModelString(
1079 export function createResponse(
1080 bundlerConfig: ServerManifest,
1081 formFieldPrefix: string,
1082 + temporaryReferences: void | TemporaryReferenceSet,
1083 backingFormData?: FormData = new FormData(),
1084 ): Response {
1085 const chunks: Map<number, SomeChunk<any>> = new Map();
@@ -990,13 +1088,7 @@ export function createResponse(
1088 _prefix: formFieldPrefix,
1089 _formData: backingFormData,
1090 _chunks: chunks,
993 - _fromJSON: function (this: any, key: string, value: JSONValue) {
994 - if (typeof value === 'string') {
995 - // We can't use .bind here because we need the "this" value.
996 - return parseModelString(response, this, key, value);
997 - }
998 - return value;
999 - },
1091 + _temporaryReferences: temporaryReferences,
1092 };
1093 return response;
1094 }
@@ -1015,7 +1107,7 @@ export function resolveField(
1107 const chunk = chunks.get(id);
1108 if (chunk) {
1109 // We were waiting on this key so now we can resolve it.
1018 - resolveModelChunk(chunk, value);
1110 + resolveModelChunk(chunk, value, id);
1111 }
1112 }
1113 }
packages/react-server/src/ReactFlightServer.js
+52 -16
@@ -11,6 +11,8 @@ import type {Chunk, BinaryChunk, Destination} from './ReactServerStreamConfig';
11
12 import type {Postpone} from 'react/src/ReactPostpone';
13
14 +import type {TemporaryReferenceSet} from './ReactFlightServerTemporaryReferences';
15 +
16 import {
17 enableBinaryFlight,
18 enablePostpone,
@@ -62,7 +64,6 @@ import type {
64 } from 'shared/ReactTypes';
65 import type {ReactElement} from 'shared/ReactElementType';
66 import type {LazyComponent} from 'react/src/ReactLazy';
65 -import type {TemporaryReference} from './ReactFlightServerTemporaryReferences';
67
68 import {
69 resolveClientReferenceMetadata,
@@ -80,8 +81,8 @@ import {
81 } from './ReactFlightServerConfig';
82
83 import {
83 - isTemporaryReference,
84 - resolveTemporaryReferenceID,
84 + resolveTemporaryReference,
85 + isOpaqueTemporaryReference,
86 } from './ReactFlightServerTemporaryReferences';
87
88 import {
@@ -389,6 +390,7 @@ export type Request = {
390 writtenClientReferences: Map<ClientReferenceKey, number>,
391 writtenServerReferences: Map<ServerReference<any>, number>,
392 writtenObjects: WeakMap<Reference, string>,
393 + temporaryReferences: void | TemporaryReferenceSet,
394 identifierPrefix: string,
395 identifierCount: number,
396 taintCleanupQueue: Array<string | bigint>,
@@ -447,6 +449,7 @@ export function createRequest(
449 identifierPrefix?: string,
450 onPostpone: void | ((reason: string) => void),
451 environmentName: void | string,
452 + temporaryReferences: void | TemporaryReferenceSet,
453 ): Request {
454 if (
455 ReactSharedInternals.A !== null &&
@@ -486,6 +489,7 @@ export function createRequest(
489 writtenClientReferences: new Map(),
490 writtenServerReferences: new Map(),
491 writtenObjects: new WeakMap(),
492 + temporaryReferences: temporaryReferences,
493 identifierPrefix: identifierPrefix || '',
494 identifierCount: 1,
495 taintCleanupQueue: cleanupQueue,
@@ -1305,7 +1309,7 @@ function renderElement(
1309 }
1310 }
1311 if (typeof type === 'function') {
1308 - if (isClientReference(type) || isTemporaryReference(type)) {
1312 + if (isClientReference(type) || isOpaqueTemporaryReference(type)) {
1313 // This is a reference to a Client Component.
1314 return renderClientElement(task, type, key, props, owner, stack);
1315 }
@@ -1505,10 +1509,6 @@ function serializeServerReferenceID(id: number): string {
1509 return '$F' + id.toString(16);
1510 }
1511
1508 -function serializeTemporaryReferenceID(id: string): string {
1509 - return '$T' + id;
1510 -}
1511 -
1512 function serializeSymbolReference(name: string): string {
1513 return '$S' + name;
1514 }
@@ -1647,10 +1647,9 @@ function serializeServerReference(
1647
1648 function serializeTemporaryReference(
1649 request: Request,
1650 - temporaryReference: TemporaryReference<any>,
1650 + reference: string,
1651 ): string {
1652 - const id = resolveTemporaryReferenceID(temporaryReference);
1653 - return serializeTemporaryReferenceID(id);
1652 + return '$T' + reference;
1653 }
1654
1655 function serializeLargeTextString(request: Request, text: string): string {
@@ -2016,6 +2015,16 @@ function renderModelDestructive(
2015 );
2016 }
2017
2018 + if (request.temporaryReferences !== undefined) {
2019 + const tempRef = resolveTemporaryReference(
2020 + request.temporaryReferences,
2021 + value,
2022 + );
2023 + if (tempRef !== undefined) {
2024 + return serializeTemporaryReference(request, tempRef);
2025 + }
2026 + }
2027 +
2028 if (enableTaint) {
2029 const tainted = TaintRegistryObjects.get(value);
2030 if (tainted !== undefined) {
@@ -2284,8 +2293,14 @@ function renderModelDestructive(
2293 if (isServerReference(value)) {
2294 return serializeServerReference(request, (value: any));
2295 }
2287 - if (isTemporaryReference(value)) {
2288 - return serializeTemporaryReference(request, (value: any));
2296 + if (request.temporaryReferences !== undefined) {
2297 + const tempRef = resolveTemporaryReference(
2298 + request.temporaryReferences,
2299 + value,
2300 + );
2301 + if (tempRef !== undefined) {
2302 + return serializeTemporaryReference(request, tempRef);
2303 + }
2304 }
2305
2306 if (enableTaint) {
@@ -2295,7 +2310,13 @@ function renderModelDestructive(
2310 }
2311 }
2312
2298 - if (/^on[A-Z]/.test(parentPropertyName)) {
2313 + if (isOpaqueTemporaryReference(value)) {
2314 + throw new Error(
2315 + 'Could not reference an opaque temporary reference. ' +
2316 + 'This is likely due to misconfiguring the temporaryReferences options ' +
2317 + 'on the server.',
2318 + );
2319 + } else if (/^on[A-Z]/.test(parentPropertyName)) {
2320 throw new Error(
2321 'Event handlers cannot be passed to Client Component props.' +
2322 describeObjectForErrorMessage(parent, parentPropertyName) +
@@ -2642,6 +2663,15 @@ function renderConsoleValue(
2663 (value: any),
2664 );
2665 }
2666 + if (request.temporaryReferences !== undefined) {
2667 + const tempRef = resolveTemporaryReference(
2668 + request.temporaryReferences,
2669 + value,
2670 + );
2671 + if (tempRef !== undefined) {
2672 + return serializeTemporaryReference(request, tempRef);
2673 + }
2674 + }
2675
2676 if (counter.objectCount > 20) {
2677 // We've reached our max number of objects to serialize across the wire so we serialize this
@@ -2818,8 +2848,14 @@ function renderConsoleValue(
2848 (value: any),
2849 );
2850 }
2821 - if (isTemporaryReference(value)) {
2822 - return serializeTemporaryReference(request, (value: any));
2851 + if (request.temporaryReferences !== undefined) {
2852 + const tempRef = resolveTemporaryReference(
2853 + request.temporaryReferences,
2854 + value,
2855 + );
2856 + if (tempRef !== undefined) {
2857 + return serializeTemporaryReference(request, tempRef);
2858 + }
2859 }
2860
2861 // Serialize the body of the function as an eval so it can be printed.
packages/react-server/src/ReactFlightServerTemporaryReferences.js
+29 -15
@@ -9,20 +9,27 @@
9
10 const TEMPORARY_REFERENCE_TAG = Symbol.for('react.temporary.reference');
11
12 +export opaque type TemporaryReferenceSet = WeakMap<
13 + TemporaryReference<any>,
14 + string,
15 +>;
16 +
17 // eslint-disable-next-line no-unused-vars
13 -export opaque type TemporaryReference<T> = {
14 - $$typeof: symbol,
15 - $$id: string,
16 -};
18 +export interface TemporaryReference<T> {}
19
18 -export function isTemporaryReference(reference: Object): boolean {
20 +export function createTemporaryReferenceSet(): TemporaryReferenceSet {
21 + return new WeakMap();
22 +}
23 +
24 +export function isOpaqueTemporaryReference(reference: Object): boolean {
25 return reference.$$typeof === TEMPORARY_REFERENCE_TAG;
26 }
27
22 -export function resolveTemporaryReferenceID<T>(
28 +export function resolveTemporaryReference<T>(
29 + temporaryReferences: TemporaryReferenceSet,
30 temporaryReference: TemporaryReference<T>,
24 -): string {
25 - return temporaryReference.$$id;
31 +): void | string {
32 + return temporaryReferences.get(temporaryReference);
33 }
34
35 const proxyHandlers = {
@@ -37,10 +44,6 @@ const proxyHandlers = {
44 // These names are a little too common. We should probably have a way to
45 // have the Flight runtime extract the inner target instead.
46 return target.$$typeof;
40 - case '$$id':
41 - return target.$$id;
42 - case '$$async':
43 - return target.$$async;
47 case 'name':
48 return undefined;
49 case 'displayName':
@@ -79,7 +82,10 @@ const proxyHandlers = {
82 },
83 };
84
82 -export function createTemporaryReference<T>(id: string): TemporaryReference<T> {
85 +export function createTemporaryReference<T>(
86 + temporaryReferences: TemporaryReferenceSet,
87 + id: string,
88 +): TemporaryReference<T> {
89 const reference: TemporaryReference<any> = Object.defineProperties(
90 (function () {
91 throw new Error(
@@ -91,9 +97,17 @@ export function createTemporaryReference<T>(id: string): TemporaryReference<T> {
97 }: any),
98 {
99 $$typeof: {value: TEMPORARY_REFERENCE_TAG},
94 - $$id: {value: id},
100 },
101 );
102 + const wrapper = new Proxy(reference, proxyHandlers);
103 + registerTemporaryReference(temporaryReferences, wrapper, id);
104 + return wrapper;
105 +}
106
98 - return new Proxy(reference, proxyHandlers);
107 +export function registerTemporaryReference(
108 + temporaryReferences: TemporaryReferenceSet,
109 + object: TemporaryReference<any>,
110 + id: string,
111 +): void {
112 + temporaryReferences.set(object, id);
113 }
scripts/error-codes/codes.json
+2 -1
@@ -510,5 +510,6 @@
510 "522": "Invalid form element. requestFormReset must be passed a form that was rendered by React.",
511 "523": "The render was aborted due to being postponed.",
512 "524": "Values cannot be passed to next() of AsyncIterables passed to Client Components.",
513 - "525": "A React Element from an older version of React was rendered. This is not supported. It can happen if:\n- Multiple copies of the \"react\" package is used.\n- A library pre-bundled an old copy of \"react\" or \"react/jsx-runtime\".\n- A compiler tries to \"inline\" JSX instead of using the runtime."
513 + "525": "A React Element from an older version of React was rendered. This is not supported. It can happen if:\n- Multiple copies of the \"react\" package is used.\n- A library pre-bundled an old copy of \"react\" or \"react/jsx-runtime\".\n- A compiler tries to \"inline\" JSX instead of using the runtime.",
514 + "526": "Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server."
515 }