@samitouri / QOS-React-1 / commits / 22b2b1a05a

[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.

Sebastian Markbåge committed Oct 19, 2024 at 21:10 UTC 22b2b1a05a86f599d2eea9d0419ac57db510e134
12 files changed +239 -12
packages/react-client/src/ReactFlightClient.js
+153 -12
@@ -21,6 +21,7 @@ import type {
21 ClientReference,
22 ClientReferenceMetadata,
23 ServerConsumerModuleMap,
24 + ServerManifest,
25 StringDecoder,
26 ModuleLoading,
27 } from './ReactFlightClientConfig';
@@ -51,6 +52,7 @@ import {
52
53 import {
54 resolveClientReference,
55 + resolveServerReference,
56 preloadModule,
57 requireModule,
58 dispatchHint,
@@ -270,6 +272,7 @@ export type FindSourceMapURLCallback = (
272
273 export type Response = {
274 _bundlerConfig: ServerConsumerModuleMap,
275 + _serverReferenceConfig: null | ServerManifest,
276 _moduleLoading: ModuleLoading,
277 _callServer: CallServerCallback,
278 _encodeFormAction: void | EncodeFormActionCallback,
@@ -896,7 +899,7 @@ function waitForReference<T>(
899 parentObject: Object,
900 key: string,
901 response: Response,
899 - map: (response: Response, model: any) => T,
902 + map: (response: Response, model: any, parentObject: Object, key: string) => T,
903 path: Array<string>,
904 ): T {
905 let handler: InitializationHandler;
@@ -938,7 +941,7 @@ function waitForReference<T>(
941 }
942 value = value[path[i]];
943 }
941 - const mappedValue = map(response, value);
944 + const mappedValue = map(response, value, parentObject, key);
945 parentObject[key] = mappedValue;
946
947 // If this is the root object for a model reference, where `handler.value`
@@ -1041,7 +1044,7 @@ function waitForReference<T>(
1044 return (null: any);
1045 }
1046
1044 -function createServerReferenceProxy<A: Iterable<any>, T>(
1047 +function loadServerReference<A: Iterable<any>, T>(
1048 response: Response,
1049 metaData: {
1050 id: any,
@@ -1050,13 +1053,147 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
1053 env?: string, // DEV-only
1054 location?: ReactCallSite, // DEV-only
1055 },
1056 + parentObject: Object,
1057 + key: string,
1058 ): (...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);
1197 }
1198
1199 function getOutlinedModel<T>(
@@ -1064,7 +1201,7 @@ function getOutlinedModel<T>(
1201 reference: string,
1202 parentObject: Object,
1203 key: string,
1067 - map: (response: Response, model: any) => T,
1204 + map: (response: Response, model: any, parentObject: Object, key: string) => T,
1205 ): T {
1206 const path = reference.split(':');
1207 const id = parseInt(path[0], 16);
@@ -1099,7 +1236,7 @@ function getOutlinedModel<T>(
1236 }
1237 value = value[path[i]];
1238 }
1102 - const chunkValue = map(response, value);
1239 + const chunkValue = map(response, value, parentObject, key);
1240 if (__DEV__ && chunk._debugInfo) {
1241 // If we have a direct reference to an object that was rendered by a synchronous
1242 // server component, it might have some debug info about how it was rendered.
@@ -1244,7 +1381,7 @@ function parseModelString(
1381 ref,
1382 parentObject,
1383 key,
1247 - createServerReferenceProxy,
1384 + loadServerReference,
1385 );
1386 }
1387 case 'T': {
@@ -1421,6 +1558,7 @@ function missingCall() {
1558 function ResponseInstance(
1559 this: $FlowFixMe,
1560 bundlerConfig: ServerConsumerModuleMap,
1561 + serverReferenceConfig: null | ServerManifest,
1562 moduleLoading: ModuleLoading,
1563 callServer: void | CallServerCallback,
1564 encodeFormAction: void | EncodeFormActionCallback,
@@ -1432,6 +1570,7 @@ function ResponseInstance(
1570 ) {
1571 const chunks: Map<number, SomeChunk<any>> = new Map();
1572 this._bundlerConfig = bundlerConfig;
1573 + this._serverReferenceConfig = serverReferenceConfig;
1574 this._moduleLoading = moduleLoading;
1575 this._callServer = callServer !== undefined ? callServer : missingCall;
1576 this._encodeFormAction = encodeFormAction;
@@ -1486,6 +1625,7 @@ function ResponseInstance(
1625
1626 export function createResponse(
1627 bundlerConfig: ServerConsumerModuleMap,
1628 + serverReferenceConfig: null | ServerManifest,
1629 moduleLoading: ModuleLoading,
1630 callServer: void | CallServerCallback,
1631 encodeFormAction: void | EncodeFormActionCallback,
@@ -1498,6 +1638,7 @@ export function createResponse(
1638 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
1639 return new ResponseInstance(
1640 bundlerConfig,
1641 + serverReferenceConfig,
1642 moduleLoading,
1643 callServer,
1644 encodeFormAction,
packages/react-markup/src/ReactMarkupServer.js
+1
@@ -173,6 +173,7 @@ export function experimental_renderToHTML(
173 undefined,
174 );
175 const flightResponse = createFlightResponse(
176 + null,
177 null,
178 null,
179 noServerCallOrFormAction,
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+1
@@ -62,6 +62,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
62 const response = createResponse(
63 source,
64 null,
65 + null,
66 undefined,
67 undefined,
68 undefined,
packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
+1
@@ -51,6 +51,7 @@ function createResponseFromOptions(options: void | Options) {
51 return createResponse(
52 options && options.moduleBaseURL ? options.moduleBaseURL : '',
53 null,
54 + null,
55 options && options.callServer ? options.callServer : undefined,
56 undefined, // encodeFormAction
57 undefined, // nonce
packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js
+1
@@ -62,6 +62,7 @@ function createFromNodeStream<T>(
62 ): Thenable<T> {
63 const response: Response = createResponse(
64 moduleRootPath,
65 + null,
66 moduleBaseURL,
67 noServerCall,
68 options ? options.encodeFormAction : undefined,
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
+1
@@ -48,6 +48,7 @@ export type Options = {
48
49 function createResponseFromOptions(options: void | Options) {
50 return createResponse(
51 + null,
52 null,
53 null,
54 options && options.callServer ? options.callServer : undefined,
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js
+3
@@ -19,11 +19,13 @@ import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
19 import type {
20 ServerConsumerModuleMap,
21 ModuleLoading,
22 + ServerManifest,
23 } from 'react-client/src/ReactFlightClientConfig';
24
25 type ServerConsumerManifest = {
26 moduleMap: ServerConsumerModuleMap,
27 moduleLoading: ModuleLoading,
28 + serverModuleMap: null | ServerManifest,
29 };
30
31 import {
@@ -78,6 +80,7 @@ export type Options = {
80 function createResponseFromOptions(options: Options) {
81 return createResponse(
82 options.serverManifest.moduleMap,
83 + options.serverManifest.serverModuleMap,
84 options.serverManifest.moduleLoading,
85 noServerCall,
86 options.encodeFormAction,
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js
+3
@@ -17,11 +17,13 @@ import type {
17 import type {
18 ServerConsumerModuleMap,
19 ModuleLoading,
20 + ServerManifest,
21 } from 'react-client/src/ReactFlightClientConfig';
22
23 type ServerConsumerManifest = {
24 moduleMap: ServerConsumerModuleMap,
25 moduleLoading: ModuleLoading,
26 + serverModuleMap: null | ServerManifest,
27 };
28
29 import type {Readable} from 'stream';
@@ -71,6 +73,7 @@ function createFromNodeStream<T>(
73 ): Thenable<T> {
74 const response: Response = createResponse(
75 serverConsumerManifest.moduleMap,
76 + serverConsumerManifest.serverModuleMap,
77 serverConsumerManifest.moduleLoading,
78 noServerCall,
79 options ? options.encodeFormAction : undefined,
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+68
@@ -26,6 +26,7 @@ global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
26 let serverExports;
27 let clientExports;
28 let webpackMap;
29 +let webpackServerMap;
30 let webpackModules;
31 let webpackModuleLoading;
32 let React;
@@ -63,6 +64,7 @@ describe('ReactFlightDOMEdge', () => {
64 serverExports = WebpackMock.serverExports;
65 clientExports = WebpackMock.clientExports;
66 webpackMap = WebpackMock.webpackMap;
67 + webpackServerMap = WebpackMock.webpackServerMap;
68 webpackModules = WebpackMock.webpackModules;
69 webpackModuleLoading = WebpackMock.moduleLoading;
70
@@ -212,6 +214,72 @@ describe('ReactFlightDOMEdge', () => {
214 expect(result).toEqual('<span>Client Component</span>');
215 });
216
217 + it('should be able to load a server reference on a consuming server if a mapping exists', async () => {
218 + function greet() {
219 + return 'hi';
220 + }
221 + const ServerModule = serverExports({
222 + greet,
223 + });
224 +
225 + const stream = await serverAct(() =>
226 + ReactServerDOMServer.renderToReadableStream(
227 + {
228 + method: ServerModule.greet,
229 + },
230 + webpackMap,
231 + ),
232 + );
233 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
234 + serverConsumerManifest: {
235 + moduleMap: webpackMap,
236 + serverModuleMap: webpackServerMap,
237 + moduleLoading: webpackModuleLoading,
238 + },
239 + });
240 +
241 + const result = await response;
242 +
243 + expect(result.method).toBe(greet);
244 + });
245 +
246 + it('should be able to load a server reference on a consuming server if a mapping exists (async)', async () => {
247 + let resolve;
248 + const chunkPromise = new Promise(r => (resolve = r));
249 +
250 + function greet() {
251 + return 'hi';
252 + }
253 + const ServerModule = serverExports(
254 + {
255 + greet,
256 + },
257 + chunkPromise,
258 + );
259 +
260 + const stream = await serverAct(() =>
261 + ReactServerDOMServer.renderToReadableStream(
262 + {
263 + method: ServerModule.greet,
264 + },
265 + webpackMap,
266 + ),
267 + );
268 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
269 + serverConsumerManifest: {
270 + moduleMap: webpackMap,
271 + serverModuleMap: webpackServerMap,
272 + moduleLoading: webpackModuleLoading,
273 + },
274 + });
275 +
276 + await resolve();
277 +
278 + const result = await response;
279 +
280 + expect(result.method).toBe(greet);
281 + });
282 +
283 it('should encode long string in a compact format', async () => {
284 const testString = '"\n\t'.repeat(500) + '🙃';
285 const testString2 = 'hello'.repeat(400);
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
+1
@@ -48,6 +48,7 @@ export type Options = {
48
49 function createResponseFromOptions(options: void | Options) {
50 return createResponse(
51 + null,
52 null,
53 null,
54 options && options.callServer ? options.callServer : undefined,
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js
+3
@@ -19,11 +19,13 @@ import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
19 import type {
20 ServerConsumerModuleMap,
21 ModuleLoading,
22 + ServerManifest,
23 } from 'react-client/src/ReactFlightClientConfig';
24
25 type ServerConsumerManifest = {
26 moduleMap: ServerConsumerModuleMap,
27 moduleLoading: ModuleLoading,
28 + serverModuleMap: null | ServerManifest,
29 };
30
31 import {
@@ -78,6 +80,7 @@ export type Options = {
80 function createResponseFromOptions(options: Options) {
81 return createResponse(
82 options.serverConsumerManifest.moduleMap,
83 + options.serverConsumerManifest.serverModuleMap,
84 options.serverConsumerManifest.moduleLoading,
85 noServerCall,
86 options.encodeFormAction,
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js
+3
@@ -17,11 +17,13 @@ import type {
17 import type {
18 ServerConsumerModuleMap,
19 ModuleLoading,
20 + ServerManifest,
21 } from 'react-client/src/ReactFlightClientConfig';
22
23 type ServerConsumerManifest = {
24 moduleMap: ServerConsumerModuleMap,
25 moduleLoading: ModuleLoading,
26 + serverModuleMap: null | ServerManifest,
27 };
28
29 import type {Readable} from 'stream';
@@ -72,6 +74,7 @@ function createFromNodeStream<T>(
74 ): Thenable<T> {
75 const response: Response = createResponse(
76 serverConsumerManifest.moduleMap,
77 + serverConsumerManifest.serverModuleMap,
78 serverConsumerManifest.moduleLoading,
79 noServerCall,
80 options ? options.encodeFormAction : undefined,