Skip to content

Commit 7a52f2c

Browse files
committed
Temporary references
1 parent 1580a43 commit 7a52f2c

16 files changed

+371
-17
lines changed

packages/react-client/src/ReactFlightClient.js

+19
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import type {
3535

3636
import type {Postpone} from 'react/src/ReactPostpone';
3737

38+
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
39+
3840
import {
3941
enableBinaryFlight,
4042
enablePostpone,
@@ -55,6 +57,8 @@ import {
5557

5658
import {registerServerReference} from './ReactFlightReplyClient';
5759

60+
import {readTemporaryReference} from './ReactFlightTemporaryReferences';
61+
5862
import {
5963
REACT_LAZY_TYPE,
6064
REACT_ELEMENT_TYPE,
@@ -224,6 +228,7 @@ export type Response = {
224228
_rowTag: number, // 0 indicates that we're currently parsing the row ID
225229
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
226230
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
231+
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
227232
};
228233

229234
function readChunk<T>(chunk: SomeChunk<T>): T {
@@ -689,6 +694,18 @@ function parseModelString(
689694
const metadata = getOutlinedModel(response, id);
690695
return createServerReferenceProxy(response, metadata);
691696
}
697+
case 'T': {
698+
// Temporary Reference
699+
const id = parseInt(value.slice(2), 16);
700+
const temporaryReferences = response._tempRefs;
701+
if (temporaryReferences == null) {
702+
throw new Error(
703+
'Missing a temporary reference set but the RSC response returned a temporary reference. ' +
704+
'Pass a temporaryReference option with the set that was used with the reply.',
705+
);
706+
}
707+
return readTemporaryReference(temporaryReferences, id);
708+
}
692709
case 'Q': {
693710
// Map
694711
const id = parseInt(value.slice(2), 16);
@@ -837,6 +854,7 @@ export function createResponse(
837854
callServer: void | CallServerCallback,
838855
encodeFormAction: void | EncodeFormActionCallback,
839856
nonce: void | string,
857+
temporaryReferences: void | TemporaryReferenceSet,
840858
): Response {
841859
const chunks: Map<number, SomeChunk<any>> = new Map();
842860
const response: Response = {
@@ -853,6 +871,7 @@ export function createResponse(
853871
_rowTag: 0,
854872
_rowLength: 0,
855873
_buffer: [],
874+
_tempRefs: temporaryReferences,
856875
};
857876
// Don't inline this call because it causes closure to outline the call above.
858877
response._fromJSON = createFromJSONCallback(response);

packages/react-client/src/ReactFlightReplyClient.js

+35-9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
ReactCustomFormAction,
1616
} from 'shared/ReactTypes';
1717
import type {LazyComponent} from 'react/src/ReactLazy';
18+
import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences';
1819

1920
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
2021

@@ -32,6 +33,8 @@ import {
3233
objectName,
3334
} from 'shared/ReactSerializationErrors';
3435

36+
import {writeTemporaryReference} from './ReactFlightTemporaryReferences';
37+
3538
import isArray from 'shared/isArray';
3639
import getPrototypeOf from 'shared/getPrototypeOf';
3740

@@ -98,6 +101,10 @@ function serializeServerReferenceID(id: number): string {
98101
return '$F' + id.toString(16);
99102
}
100103

104+
function serializeTemporaryReferenceID(id: number): string {
105+
return '$T' + id.toString(16);
106+
}
107+
101108
function serializeSymbolReference(name: string): string {
102109
return '$S' + name;
103110
}
@@ -160,6 +167,7 @@ function escapeStringValue(value: string): string {
160167
export function processReply(
161168
root: ReactServerValue,
162169
formFieldPrefix: string,
170+
temporaryReferences: void | TemporaryReferenceSet,
163171
resolve: (string | FormData) => void,
164172
reject: (error: mixed) => void,
165173
): void {
@@ -210,9 +218,15 @@ export function processReply(
210218
if (typeof value === 'object') {
211219
switch ((value: any).$$typeof) {
212220
case REACT_ELEMENT_TYPE: {
213-
throw new Error(
214-
'React Element cannot be passed to Server Functions from the Client.' +
215-
(__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
221+
if (temporaryReferences === undefined) {
222+
throw new Error(
223+
'React Element cannot be passed to Server Functions from the Client without a ' +
224+
'temporary reference set. Pass a TemporaryReferenceSet to the options.' +
225+
(__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
226+
);
227+
}
228+
return serializeTemporaryReferenceID(
229+
writeTemporaryReference(temporaryReferences, value),
216230
);
217231
}
218232
case REACT_LAZY_TYPE: {
@@ -366,9 +380,15 @@ export function processReply(
366380
proto !== ObjectPrototype &&
367381
(proto === null || getPrototypeOf(proto) !== null)
368382
) {
369-
throw new Error(
370-
'Only plain objects, and a few built-ins, can be passed to Server Actions. ' +
371-
'Classes or null prototypes are not supported.',
383+
if (temporaryReferences === undefined) {
384+
throw new Error(
385+
'Only plain objects, and a few built-ins, can be passed to Server Actions. ' +
386+
'Classes or null prototypes are not supported.',
387+
);
388+
}
389+
// We can serialize class instances as temporary references.
390+
return serializeTemporaryReferenceID(
391+
writeTemporaryReference(temporaryReferences, value),
372392
);
373393
}
374394
if (__DEV__) {
@@ -450,9 +470,14 @@ export function processReply(
450470
formData.set(formFieldPrefix + refId, metaDataJSON);
451471
return serializeServerReferenceID(refId);
452472
}
453-
throw new Error(
454-
'Client Functions cannot be passed directly to Server Functions. ' +
455-
'Only Functions passed from the Server can be passed back again.',
473+
if (temporaryReferences === undefined) {
474+
throw new Error(
475+
'Client Functions cannot be passed directly to Server Functions. ' +
476+
'Only Functions passed from the Server can be passed back again.',
477+
);
478+
}
479+
return serializeTemporaryReferenceID(
480+
writeTemporaryReference(temporaryReferences, value),
456481
);
457482
}
458483

@@ -511,6 +536,7 @@ function encodeFormData(reference: any): Thenable<FormData> {
511536
processReply(
512537
reference,
513538
'',
539+
undefined, // TODO: This means React Elements can't be used as state in progressive enhancement.
514540
(body: string | FormData) => {
515541
if (typeof body === 'string') {
516542
const data = new FormData();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
interface Reference {}
11+
12+
export opaque type TemporaryReferenceSet = Array<Reference>;
13+
14+
export function createTemporaryReferenceSet(): TemporaryReferenceSet {
15+
return [];
16+
}
17+
18+
export function writeTemporaryReference(
19+
set: TemporaryReferenceSet,
20+
object: Reference,
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;
28+
}
29+
30+
export function readTemporaryReference(
31+
set: TemporaryReferenceSet,
32+
id: number,
33+
): Reference {
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];
41+
}

packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,18 @@ import {
2626
createServerReference,
2727
} from 'react-client/src/ReactFlightReplyClient';
2828

29+
import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
30+
31+
export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
32+
33+
export type {TemporaryReferenceSet};
34+
2935
type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
3036

3137
export type Options = {
3238
moduleBaseURL?: string,
3339
callServer?: CallServerCallback,
40+
temporaryReferences?: TemporaryReferenceSet,
3441
};
3542

3643
function createResponseFromOptions(options: void | Options) {
@@ -40,6 +47,9 @@ function createResponseFromOptions(options: void | Options) {
4047
options && options.callServer ? options.callServer : undefined,
4148
undefined, // encodeFormAction
4249
undefined, // nonce
50+
options && options.temporaryReferences
51+
? options.temporaryReferences
52+
: undefined,
4353
);
4454
}
4555

@@ -97,11 +107,20 @@ function createFromFetch<T>(
97107

98108
function encodeReply(
99109
value: ReactServerValue,
110+
options?: {temporaryReferences?: TemporaryReferenceSet},
100111
): Promise<
101112
string | URLSearchParams | FormData,
102113
> /* We don't use URLSearchParams yet but maybe */ {
103114
return new Promise((resolve, reject) => {
104-
processReply(value, '', resolve, reject);
115+
processReply(
116+
value,
117+
'',
118+
options && options.temporaryReferences
119+
? options.temporaryReferences
120+
: undefined,
121+
resolve,
122+
reject,
123+
);
105124
});
106125
}
107126

packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function createFromNodeStream<T>(
6060
noServerCall,
6161
options ? options.encodeFormAction : undefined,
6262
options && typeof options.nonce === 'string' ? options.nonce : undefined,
63+
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
6364
);
6465
stream.on('data', chunk => {
6566
processBinaryChunk(response, chunk);

packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,17 @@ import {
2626
createServerReference,
2727
} from 'react-client/src/ReactFlightReplyClient';
2828

29+
import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
30+
31+
export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
32+
33+
export type {TemporaryReferenceSet};
34+
2935
type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
3036

3137
export type Options = {
3238
callServer?: CallServerCallback,
39+
temporaryReferences?: TemporaryReferenceSet,
3340
};
3441

3542
function createResponseFromOptions(options: void | Options) {
@@ -39,6 +46,9 @@ function createResponseFromOptions(options: void | Options) {
3946
options && options.callServer ? options.callServer : undefined,
4047
undefined, // encodeFormAction
4148
undefined, // nonce
49+
options && options.temporaryReferences
50+
? options.temporaryReferences
51+
: undefined,
4252
);
4353
}
4454

@@ -96,11 +106,20 @@ function createFromFetch<T>(
96106

97107
function encodeReply(
98108
value: ReactServerValue,
109+
options?: {temporaryReferences?: TemporaryReferenceSet},
99110
): Promise<
100111
string | URLSearchParams | FormData,
101112
> /* We don't use URLSearchParams yet but maybe */ {
102113
return new Promise((resolve, reject) => {
103-
processReply(value, '', resolve, reject);
114+
processReply(
115+
value,
116+
'',
117+
options && options.temporaryReferences
118+
? options.temporaryReferences
119+
: undefined,
120+
resolve,
121+
reject,
122+
);
104123
});
105124
}
106125

packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ import {
3636
createServerReference as createServerReferenceImpl,
3737
} from 'react-client/src/ReactFlightReplyClient';
3838

39+
import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
40+
41+
export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
42+
43+
export type {TemporaryReferenceSet};
44+
3945
function noServerCall() {
4046
throw new Error(
4147
'Server Functions cannot be called during initial render. ' +
@@ -60,6 +66,7 @@ export type Options = {
6066
ssrManifest: SSRManifest,
6167
nonce?: string,
6268
encodeFormAction?: EncodeFormActionCallback,
69+
temporaryReferences?: TemporaryReferenceSet,
6370
};
6471

6572
function createResponseFromOptions(options: Options) {
@@ -69,6 +76,9 @@ function createResponseFromOptions(options: Options) {
6976
noServerCall,
7077
options.encodeFormAction,
7178
typeof options.nonce === 'string' ? options.nonce : undefined,
79+
options && options.temporaryReferences
80+
? options.temporaryReferences
81+
: undefined,
7282
);
7383
}
7484

@@ -126,11 +136,20 @@ function createFromFetch<T>(
126136

127137
function encodeReply(
128138
value: ReactServerValue,
139+
options?: {temporaryReferences?: TemporaryReferenceSet},
129140
): Promise<
130141
string | URLSearchParams | FormData,
131142
> /* We don't use URLSearchParams yet but maybe */ {
132143
return new Promise((resolve, reject) => {
133-
processReply(value, '', resolve, reject);
144+
processReply(
145+
value,
146+
'',
147+
options && options.temporaryReferences
148+
? options.temporaryReferences
149+
: undefined,
150+
resolve,
151+
reject,
152+
);
134153
});
135154
}
136155

packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ function createFromNodeStream<T>(
6969
noServerCall,
7070
options ? options.encodeFormAction : undefined,
7171
options && typeof options.nonce === 'string' ? options.nonce : undefined,
72+
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
7273
);
7374
stream.on('data', chunk => {
7475
processBinaryChunk(response, chunk);

0 commit comments

Comments
 (0)