Skip to content

Commit 22b2b1a

Browse files
authored
[Flight] Add serverModuleMap option for mapping ServerReferences (#31300)
Stacked on #31299. We already have an option for resolving Client References to other Client References when consuming an RSC payload on the server. This lets you resolve Server References on the consuming side when the environment where you're consuming the RSC payload also has access to those Server References. Basically they becomes like Client References for this consumer but for another consumer they wouldn't be.
1 parent 39a7730 commit 22b2b1a

File tree

12 files changed

+239
-12
lines changed

12 files changed

+239
-12
lines changed

packages/react-client/src/ReactFlightClient.js

+153-12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
ClientReference,
2222
ClientReferenceMetadata,
2323
ServerConsumerModuleMap,
24+
ServerManifest,
2425
StringDecoder,
2526
ModuleLoading,
2627
} from './ReactFlightClientConfig';
@@ -51,6 +52,7 @@ import {
5152

5253
import {
5354
resolveClientReference,
55+
resolveServerReference,
5456
preloadModule,
5557
requireModule,
5658
dispatchHint,
@@ -270,6 +272,7 @@ export type FindSourceMapURLCallback = (
270272

271273
export type Response = {
272274
_bundlerConfig: ServerConsumerModuleMap,
275+
_serverReferenceConfig: null | ServerManifest,
273276
_moduleLoading: ModuleLoading,
274277
_callServer: CallServerCallback,
275278
_encodeFormAction: void | EncodeFormActionCallback,
@@ -896,7 +899,7 @@ function waitForReference<T>(
896899
parentObject: Object,
897900
key: string,
898901
response: Response,
899-
map: (response: Response, model: any) => T,
902+
map: (response: Response, model: any, parentObject: Object, key: string) => T,
900903
path: Array<string>,
901904
): T {
902905
let handler: InitializationHandler;
@@ -938,7 +941,7 @@ function waitForReference<T>(
938941
}
939942
value = value[path[i]];
940943
}
941-
const mappedValue = map(response, value);
944+
const mappedValue = map(response, value, parentObject, key);
942945
parentObject[key] = mappedValue;
943946

944947
// If this is the root object for a model reference, where `handler.value`
@@ -1041,7 +1044,7 @@ function waitForReference<T>(
10411044
return (null: any);
10421045
}
10431046

1044-
function createServerReferenceProxy<A: Iterable<any>, T>(
1047+
function loadServerReference<A: Iterable<any>, T>(
10451048
response: Response,
10461049
metaData: {
10471050
id: any,
@@ -1050,21 +1053,155 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
10501053
env?: string, // DEV-only
10511054
location?: ReactCallSite, // DEV-only
10521055
},
1056+
parentObject: Object,
1057+
key: string,
10531058
): (...A) => Promise<T> {
1054-
return createBoundServerReference(
1055-
metaData,
1056-
response._callServer,
1057-
response._encodeFormAction,
1058-
__DEV__ ? response._debugFindSourceMapURL : undefined,
1059-
);
1059+
if (!response._serverReferenceConfig) {
1060+
// In the normal case, we can't load this Server Reference in the current environment and
1061+
// we just return a proxy to it.
1062+
return createBoundServerReference(
1063+
metaData,
1064+
response._callServer,
1065+
response._encodeFormAction,
1066+
__DEV__ ? response._debugFindSourceMapURL : undefined,
1067+
);
1068+
}
1069+
// If we have a module mapping we can load the real version of this Server Reference.
1070+
const serverReference: ClientReference<T> =
1071+
resolveServerReference<$FlowFixMe>(
1072+
response._serverReferenceConfig,
1073+
metaData.id,
1074+
);
1075+
1076+
const promise = preloadModule(serverReference);
1077+
if (!promise) {
1078+
return (requireModule(serverReference): any);
1079+
}
1080+
1081+
let handler: InitializationHandler;
1082+
if (initializingHandler) {
1083+
handler = initializingHandler;
1084+
handler.deps++;
1085+
} else {
1086+
handler = initializingHandler = {
1087+
parent: null,
1088+
chunk: null,
1089+
value: null,
1090+
deps: 1,
1091+
errored: false,
1092+
};
1093+
}
1094+
1095+
function fulfill(): void {
1096+
const resolvedValue = (requireModule(serverReference): any);
1097+
parentObject[key] = resolvedValue;
1098+
1099+
// If this is the root object for a model reference, where `handler.value`
1100+
// is a stale `null`, the resolved value can be used directly.
1101+
if (key === '' && handler.value === null) {
1102+
handler.value = resolvedValue;
1103+
}
1104+
1105+
// If the parent object is an unparsed React element tuple, we also need to
1106+
// update the props and owner of the parsed element object (i.e.
1107+
// handler.value).
1108+
if (
1109+
parentObject[0] === REACT_ELEMENT_TYPE &&
1110+
typeof handler.value === 'object' &&
1111+
handler.value !== null &&
1112+
handler.value.$$typeof === REACT_ELEMENT_TYPE
1113+
) {
1114+
const element: any = handler.value;
1115+
switch (key) {
1116+
case '3':
1117+
element.props = resolvedValue;
1118+
break;
1119+
case '4':
1120+
if (__DEV__) {
1121+
element._owner = resolvedValue;
1122+
}
1123+
break;
1124+
}
1125+
}
1126+
1127+
handler.deps--;
1128+
1129+
if (handler.deps === 0) {
1130+
const chunk = handler.chunk;
1131+
if (chunk === null || chunk.status !== BLOCKED) {
1132+
return;
1133+
}
1134+
const resolveListeners = chunk.value;
1135+
const initializedChunk: InitializedChunk<T> = (chunk: any);
1136+
initializedChunk.status = INITIALIZED;
1137+
initializedChunk.value = handler.value;
1138+
if (resolveListeners !== null) {
1139+
wakeChunk(resolveListeners, handler.value);
1140+
}
1141+
}
1142+
}
1143+
1144+
function reject(error: mixed): void {
1145+
if (handler.errored) {
1146+
// We've already errored. We could instead build up an AggregateError
1147+
// but if there are multiple errors we just take the first one like
1148+
// Promise.all.
1149+
return;
1150+
}
1151+
const blockedValue = handler.value;
1152+
handler.errored = true;
1153+
handler.value = error;
1154+
const chunk = handler.chunk;
1155+
if (chunk === null || chunk.status !== BLOCKED) {
1156+
return;
1157+
}
1158+
1159+
if (__DEV__) {
1160+
if (
1161+
typeof blockedValue === 'object' &&
1162+
blockedValue !== null &&
1163+
blockedValue.$$typeof === REACT_ELEMENT_TYPE
1164+
) {
1165+
const element = blockedValue;
1166+
// Conceptually the error happened inside this Element but right before
1167+
// it was rendered. We don't have a client side component to render but
1168+
// we can add some DebugInfo to explain that this was conceptually a
1169+
// Server side error that errored inside this element. That way any stack
1170+
// traces will point to the nearest JSX that errored - e.g. during
1171+
// serialization.
1172+
const erroredComponent: ReactComponentInfo = {
1173+
name: getComponentNameFromType(element.type) || '',
1174+
owner: element._owner,
1175+
};
1176+
if (enableOwnerStacks) {
1177+
// $FlowFixMe[cannot-write]
1178+
erroredComponent.debugStack = element._debugStack;
1179+
if (supportsCreateTask) {
1180+
// $FlowFixMe[cannot-write]
1181+
erroredComponent.debugTask = element._debugTask;
1182+
}
1183+
}
1184+
const chunkDebugInfo: ReactDebugInfo =
1185+
chunk._debugInfo || (chunk._debugInfo = []);
1186+
chunkDebugInfo.push(erroredComponent);
1187+
}
1188+
}
1189+
1190+
triggerErrorOnChunk(chunk, error);
1191+
}
1192+
1193+
promise.then(fulfill, reject);
1194+
1195+
// Return a place holder value for now.
1196+
return (null: any);
10601197
}
10611198

10621199
function getOutlinedModel<T>(
10631200
response: Response,
10641201
reference: string,
10651202
parentObject: Object,
10661203
key: string,
1067-
map: (response: Response, model: any) => T,
1204+
map: (response: Response, model: any, parentObject: Object, key: string) => T,
10681205
): T {
10691206
const path = reference.split(':');
10701207
const id = parseInt(path[0], 16);
@@ -1099,7 +1236,7 @@ function getOutlinedModel<T>(
10991236
}
11001237
value = value[path[i]];
11011238
}
1102-
const chunkValue = map(response, value);
1239+
const chunkValue = map(response, value, parentObject, key);
11031240
if (__DEV__ && chunk._debugInfo) {
11041241
// If we have a direct reference to an object that was rendered by a synchronous
11051242
// server component, it might have some debug info about how it was rendered.
@@ -1244,7 +1381,7 @@ function parseModelString(
12441381
ref,
12451382
parentObject,
12461383
key,
1247-
createServerReferenceProxy,
1384+
loadServerReference,
12481385
);
12491386
}
12501387
case 'T': {
@@ -1421,6 +1558,7 @@ function missingCall() {
14211558
function ResponseInstance(
14221559
this: $FlowFixMe,
14231560
bundlerConfig: ServerConsumerModuleMap,
1561+
serverReferenceConfig: null | ServerManifest,
14241562
moduleLoading: ModuleLoading,
14251563
callServer: void | CallServerCallback,
14261564
encodeFormAction: void | EncodeFormActionCallback,
@@ -1432,6 +1570,7 @@ function ResponseInstance(
14321570
) {
14331571
const chunks: Map<number, SomeChunk<any>> = new Map();
14341572
this._bundlerConfig = bundlerConfig;
1573+
this._serverReferenceConfig = serverReferenceConfig;
14351574
this._moduleLoading = moduleLoading;
14361575
this._callServer = callServer !== undefined ? callServer : missingCall;
14371576
this._encodeFormAction = encodeFormAction;
@@ -1486,6 +1625,7 @@ function ResponseInstance(
14861625

14871626
export function createResponse(
14881627
bundlerConfig: ServerConsumerModuleMap,
1628+
serverReferenceConfig: null | ServerManifest,
14891629
moduleLoading: ModuleLoading,
14901630
callServer: void | CallServerCallback,
14911631
encodeFormAction: void | EncodeFormActionCallback,
@@ -1498,6 +1638,7 @@ export function createResponse(
14981638
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
14991639
return new ResponseInstance(
15001640
bundlerConfig,
1641+
serverReferenceConfig,
15011642
moduleLoading,
15021643
callServer,
15031644
encodeFormAction,

packages/react-markup/src/ReactMarkupServer.js

+1
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export function experimental_renderToHTML(
173173
undefined,
174174
);
175175
const flightResponse = createFlightResponse(
176+
null,
176177
null,
177178
null,
178179
noServerCallOrFormAction,

packages/react-noop-renderer/src/ReactNoopFlightClient.js

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
6262
const response = createResponse(
6363
source,
6464
null,
65+
null,
6566
undefined,
6667
undefined,
6768
undefined,

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

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function createResponseFromOptions(options: void | Options) {
5151
return createResponse(
5252
options && options.moduleBaseURL ? options.moduleBaseURL : '',
5353
null,
54+
null,
5455
options && options.callServer ? options.callServer : undefined,
5556
undefined, // encodeFormAction
5657
undefined, // nonce

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

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function createFromNodeStream<T>(
6262
): Thenable<T> {
6363
const response: Response = createResponse(
6464
moduleRootPath,
65+
null,
6566
moduleBaseURL,
6667
noServerCall,
6768
options ? options.encodeFormAction : undefined,

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

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type Options = {
4848

4949
function createResponseFromOptions(options: void | Options) {
5050
return createResponse(
51+
null,
5152
null,
5253
null,
5354
options && options.callServer ? options.callServer : undefined,

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

+3
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
1919
import type {
2020
ServerConsumerModuleMap,
2121
ModuleLoading,
22+
ServerManifest,
2223
} from 'react-client/src/ReactFlightClientConfig';
2324

2425
type ServerConsumerManifest = {
2526
moduleMap: ServerConsumerModuleMap,
2627
moduleLoading: ModuleLoading,
28+
serverModuleMap: null | ServerManifest,
2729
};
2830

2931
import {
@@ -78,6 +80,7 @@ export type Options = {
7880
function createResponseFromOptions(options: Options) {
7981
return createResponse(
8082
options.serverManifest.moduleMap,
83+
options.serverManifest.serverModuleMap,
8184
options.serverManifest.moduleLoading,
8285
noServerCall,
8386
options.encodeFormAction,

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

+3
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ import type {
1717
import type {
1818
ServerConsumerModuleMap,
1919
ModuleLoading,
20+
ServerManifest,
2021
} from 'react-client/src/ReactFlightClientConfig';
2122

2223
type ServerConsumerManifest = {
2324
moduleMap: ServerConsumerModuleMap,
2425
moduleLoading: ModuleLoading,
26+
serverModuleMap: null | ServerManifest,
2527
};
2628

2729
import type {Readable} from 'stream';
@@ -71,6 +73,7 @@ function createFromNodeStream<T>(
7173
): Thenable<T> {
7274
const response: Response = createResponse(
7375
serverConsumerManifest.moduleMap,
76+
serverConsumerManifest.serverModuleMap,
7477
serverConsumerManifest.moduleLoading,
7578
noServerCall,
7679
options ? options.encodeFormAction : undefined,

0 commit comments

Comments
 (0)