@samitouri / QOS-React / commits / c2d103594d

Configure the requested environment and annotate tasks at boundary between environments (#30455)

This enables configuring the name of the requested environment. When we currently use createTask, we start with a `"use server"` annotation. This option basically configures that string. I now also deal with the case when switching environments along the owner path. If you go from `"Third Party"` to `"Server"` to `"Client"`, it'll have a task named `"use third party"` at the root, then `"use server"` and then finally `"use client"`. We don't really have the concept of a Server Component making a request during render to then create another Server Component. Really the inner one should conceptually have the first one as its owner in that case. So currently the inner one will always have a null owner. We could somehow connect them in this server-to-server case. We don't currently have a way to configure the `"use client"` option but I figured maybe that could be inferred by the server environment that the Flight Client is executed within. Note: We did talk before about annotating each stack frame with the environment. You can effectively do that manually when parsing `rsc://React/{environment}/` from `captureOwnerStack`. However, we can't do that natively. At least not without deeper integration. Because it's the source map that's responsible for the actual function name of each stack frame - not what we give it at runtime. So for the native stacks, the task showing the change in environment is more practical.

Sebastian Markbåge committed Jul 25, 2024 at 11:46 UTC c2d103594dfe8c58d31964a1fbaa31276db0cbf4
10 files changed +129 -39
packages/react-client/src/ReactFlightClient.js
+96 -39
@@ -255,6 +255,7 @@ export type Response = {
255 _debugRootTask?: null | ConsoleTask, // DEV-only
256 _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
257 _replayConsole: boolean, // DEV-only
258 + _rootEnvironmentName: string, // DEV-only, the requested environment name.
259 };
260
261 function readChunk<T>(chunk: SomeChunk<T>): T {
@@ -692,7 +693,7 @@ function createElement(
693 writable: true,
694 value: null,
695 });
695 - let env = '';
696 + let env = response._rootEnvironmentName;
697 if (enableOwnerStacks) {
698 if (owner !== null && owner.env != null) {
699 // Interestingly we don't actually have the environment name of where
@@ -737,7 +738,7 @@ function createElement(
738 // This owner should ideally have already been initialized to avoid getting
739 // user stack frames on the stack.
740 const ownerTask =
740 - owner === null ? null : initializeFakeTask(response, owner);
741 + owner === null ? null : initializeFakeTask(response, owner, env);
742 if (ownerTask === null) {
743 const rootTask = response._debugRootTask;
744 if (rootTask != null) {
@@ -1355,6 +1356,7 @@ function ResponseInstance(
1356 temporaryReferences: void | TemporaryReferenceSet,
1357 findSourceMapURL: void | FindSourceMapURLCallback,
1358 replayConsole: boolean,
1359 + environmentName: void | string,
1360 ) {
1361 const chunks: Map<number, SomeChunk<any>> = new Map();
1362 this._bundlerConfig = bundlerConfig;
@@ -1371,17 +1373,21 @@ function ResponseInstance(
1373 this._rowLength = 0;
1374 this._buffer = [];
1375 this._tempRefs = temporaryReferences;
1374 - if (supportsCreateTask) {
1375 - // Any stacks that appear on the server need to be rooted somehow on the client
1376 - // so we create a root Task for this response which will be the root owner for any
1377 - // elements created by the server. We use the "use server" string to indicate that
1378 - // this is where we enter the server from the client.
1379 - // TODO: Make this string configurable.
1380 - this._debugRootTask = (console: any).createTask('"use server"');
1381 - }
1376 if (__DEV__) {
1377 + const rootEnv = environmentName === undefined ? 'Server' : environmentName;
1378 + if (supportsCreateTask) {
1379 + // Any stacks that appear on the server need to be rooted somehow on the client
1380 + // so we create a root Task for this response which will be the root owner for any
1381 + // elements created by the server. We use the "use server" string to indicate that
1382 + // this is where we enter the server from the client.
1383 + // TODO: Make this string configurable.
1384 + this._debugRootTask = (console: any).createTask(
1385 + '"use ' + rootEnv.toLowerCase() + '"',
1386 + );
1387 + }
1388 this._debugFindSourceMapURL = findSourceMapURL;
1389 this._replayConsole = replayConsole;
1390 + this._rootEnvironmentName = rootEnv;
1391 }
1392 // Don't inline this call because it causes closure to outline the call above.
1393 this._fromJSON = createFromJSONCallback(this);
@@ -1396,6 +1402,7 @@ export function createResponse(
1402 temporaryReferences: void | TemporaryReferenceSet,
1403 findSourceMapURL: void | FindSourceMapURLCallback,
1404 replayConsole: boolean,
1405 + environmentName: void | string,
1406 ): Response {
1407 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
1408 return new ResponseInstance(
@@ -1407,6 +1414,7 @@ export function createResponse(
1414 temporaryReferences,
1415 findSourceMapURL,
1416 replayConsole,
1417 + environmentName,
1418 );
1419 }
1420
@@ -1864,7 +1872,7 @@ function resolveErrorDev(
1872 'An error occurred in the Server Components render but no message was provided',
1873 ),
1874 );
1867 - const rootTask = response._debugRootTask;
1875 + const rootTask = getRootTask(response, env);
1876 if (rootTask != null) {
1877 error = rootTask.run(callStack);
1878 } else {
@@ -2098,52 +2106,99 @@ function buildFakeCallStack<T>(
2106 return callStack;
2107 }
2108
2109 +function getRootTask(
2110 + response: Response,
2111 + childEnvironmentName: string,
2112 +): null | ConsoleTask {
2113 + const rootTask = response._debugRootTask;
2114 + if (!rootTask) {
2115 + return null;
2116 + }
2117 + if (response._rootEnvironmentName !== childEnvironmentName) {
2118 + // If the root most owner component is itself in a different environment than the requested
2119 + // environment then we create an extra task to indicate that we're transitioning into it.
2120 + // Like if one environment just requests another environment.
2121 + const createTaskFn = (console: any).createTask.bind(
2122 + console,
2123 + '"use ' + childEnvironmentName.toLowerCase() + '"',
2124 + );
2125 + return rootTask.run(createTaskFn);
2126 + }
2127 + return rootTask;
2128 +}
2129 +
2130 function initializeFakeTask(
2131 response: Response,
2132 debugInfo: ReactComponentInfo | ReactAsyncInfo,
2133 + childEnvironmentName: string,
2134 ): null | ConsoleTask {
2135 if (!supportsCreateTask) {
2136 return null;
2137 }
2138 const componentInfo: ReactComponentInfo = (debugInfo: any); // Refined
2109 - const cachedEntry = componentInfo.debugTask;
2110 - if (cachedEntry !== undefined) {
2111 - return cachedEntry;
2112 - }
2113 -
2139 if (debugInfo.stack == null) {
2140 // If this is an error, we should've really already initialized the task.
2141 // If it's null, we can't initialize a task.
2142 return null;
2143 }
2119 -
2144 const stack = debugInfo.stack;
2121 - const env = componentInfo.env == null ? '' : componentInfo.env;
2122 - const ownerTask =
2123 - componentInfo.owner == null
2124 - ? null
2125 - : initializeFakeTask(response, componentInfo.owner);
2126 -
2127 - const createTaskFn = (console: any).createTask.bind(
2128 - console,
2129 - getServerComponentTaskName(componentInfo),
2130 - );
2131 - const callStack = buildFakeCallStack(response, stack, env, createTaskFn);
2145 + const env: string =
2146 + componentInfo.env == null
2147 + ? response._rootEnvironmentName
2148 + : componentInfo.env;
2149 + if (env !== childEnvironmentName) {
2150 + // This is the boundary between two environments so we'll annotate the task name.
2151 + // That is unusual so we don't cache it.
2152 + const ownerTask =
2153 + componentInfo.owner == null
2154 + ? null
2155 + : initializeFakeTask(response, componentInfo.owner, env);
2156 + return buildFakeTask(
2157 + response,
2158 + ownerTask,
2159 + stack,
2160 + '"use ' + childEnvironmentName.toLowerCase() + '"',
2161 + env,
2162 + );
2163 + } else {
2164 + const cachedEntry = componentInfo.debugTask;
2165 + if (cachedEntry !== undefined) {
2166 + return cachedEntry;
2167 + }
2168 + const ownerTask =
2169 + componentInfo.owner == null
2170 + ? null
2171 + : initializeFakeTask(response, componentInfo.owner, env);
2172 + // $FlowFixMe[cannot-write]: We consider this part of initialization.
2173 + return (componentInfo.debugTask = buildFakeTask(
2174 + response,
2175 + ownerTask,
2176 + stack,
2177 + getServerComponentTaskName(componentInfo),
2178 + env,
2179 + ));
2180 + }
2181 +}
2182
2133 - let componentTask;
2183 +function buildFakeTask(
2184 + response: Response,
2185 + ownerTask: null | ConsoleTask,
2186 + stack: ReactStackTrace,
2187 + taskName: string,
2188 + env: string,
2189 +): ConsoleTask {
2190 + const createTaskFn = (console: any).createTask.bind(console, taskName);
2191 + const callStack = buildFakeCallStack(response, stack, env, createTaskFn);
2192 if (ownerTask === null) {
2135 - const rootTask = response._debugRootTask;
2193 + const rootTask = getRootTask(response, env);
2194 if (rootTask != null) {
2137 - componentTask = rootTask.run(callStack);
2195 + return rootTask.run(callStack);
2196 } else {
2139 - componentTask = callStack();
2197 + return callStack();
2198 }
2199 } else {
2142 - componentTask = ownerTask.run(callStack);
2200 + return ownerTask.run(callStack);
2201 }
2144 - // $FlowFixMe[cannot-write]: We consider this part of initialization.
2145 - componentInfo.debugTask = componentTask;
2146 - return componentTask;
2202 }
2203
2204 const createFakeJSXCallStack = {
@@ -2216,7 +2271,9 @@ function resolveDebugInfo(
2271 // We eagerly initialize the fake task because this resolving happens outside any
2272 // render phase so we're not inside a user space stack at this point. If we waited
2273 // to initialize it when we need it, we might be inside user code.
2219 - initializeFakeTask(response, debugInfo);
2274 + const env =
2275 + debugInfo.env === undefined ? response._rootEnvironmentName : debugInfo.env;
2276 + initializeFakeTask(response, debugInfo, env);
2277 initializeFakeStack(response, debugInfo);
2278
2279 const chunk = getChunk(response, id);
@@ -2266,7 +2323,7 @@ function resolveConsoleEntry(
2323 printToConsole.bind(null, methodName, args, env),
2324 );
2325 if (owner != null) {
2269 - const task = initializeFakeTask(response, owner);
2326 + const task = initializeFakeTask(response, owner, env);
2327 initializeFakeStack(response, owner);
2328 if (task !== null) {
2329 task.run(callStack);
@@ -2275,7 +2332,7 @@ function resolveConsoleEntry(
2332 // TODO: Set the current owner so that captureOwnerStack() adds the component
2333 // stack during the replay - if needed.
2334 }
2278 - const rootTask = response._debugRootTask;
2335 + const rootTask = getRootTask(response, env);
2336 if (rootTask != null) {
2337 rootTask.run(callStack);
2338 return;
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+1
@@ -65,6 +65,7 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
65 undefined,
66 options !== undefined ? options.findSourceMapURL : undefined,
67 true,
68 + undefined,
69 );
70 for (let i = 0; i < source.length; i++) {
71 processBinaryChunk(response, source[i], 0);
packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js
+4
@@ -43,6 +43,7 @@ export type Options = {
43 temporaryReferences?: TemporaryReferenceSet,
44 findSourceMapURL?: FindSourceMapURLCallback,
45 replayConsoleLogs?: boolean,
46 + environmentName?: string,
47 };
48
49 function createResponseFromOptions(options: void | Options) {
@@ -59,6 +60,9 @@ function createResponseFromOptions(options: void | Options) {
60 ? options.findSourceMapURL
61 : undefined,
62 __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true
63 + __DEV__ && options && options.environmentName
64 + ? options.environmentName
65 + : undefined,
66 );
67 }
68
packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js
+4
@@ -51,6 +51,7 @@ export type Options = {
51 encodeFormAction?: EncodeFormActionCallback,
52 findSourceMapURL?: FindSourceMapURLCallback,
53 replayConsoleLogs?: boolean,
54 + environmentName?: string,
55 };
56
57 function createFromNodeStream<T>(
@@ -70,6 +71,9 @@ function createFromNodeStream<T>(
71 ? options.findSourceMapURL
72 : undefined,
73 __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
74 + __DEV__ && options && options.environmentName
75 + ? options.environmentName
76 + : undefined,
77 );
78 stream.on('data', chunk => {
79 processBinaryChunk(response, chunk);
packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js
+4
@@ -42,6 +42,7 @@ export type Options = {
42 temporaryReferences?: TemporaryReferenceSet,
43 findSourceMapURL?: FindSourceMapURLCallback,
44 replayConsoleLogs?: boolean,
45 + environmentName?: string,
46 };
47
48 function createResponseFromOptions(options: void | Options) {
@@ -58,6 +59,9 @@ function createResponseFromOptions(options: void | Options) {
59 ? options.findSourceMapURL
60 : undefined,
61 __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true
62 + __DEV__ && options && options.environmentName
63 + ? options.environmentName
64 + : undefined,
65 );
66 }
67
packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js
+4
@@ -72,6 +72,7 @@ export type Options = {
72 temporaryReferences?: TemporaryReferenceSet,
73 findSourceMapURL?: FindSourceMapURLCallback,
74 replayConsoleLogs?: boolean,
75 + environmentName?: string,
76 };
77
78 function createResponseFromOptions(options: Options) {
@@ -88,6 +89,9 @@ function createResponseFromOptions(options: Options) {
89 ? options.findSourceMapURL
90 : undefined,
91 __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
92 + __DEV__ && options && options.environmentName
93 + ? options.environmentName
94 + : undefined,
95 );
96 }
97
packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js
+4
@@ -61,6 +61,7 @@ export type Options = {
61 encodeFormAction?: EncodeFormActionCallback,
62 findSourceMapURL?: FindSourceMapURLCallback,
63 replayConsoleLogs?: boolean,
64 + environmentName?: string,
65 };
66
67 function createFromNodeStream<T>(
@@ -79,6 +80,9 @@ function createFromNodeStream<T>(
80 ? options.findSourceMapURL
81 : undefined,
82 __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
83 + __DEV__ && options && options.environmentName
84 + ? options.environmentName
85 + : undefined,
86 );
87 stream.on('data', chunk => {
88 processBinaryChunk(response, chunk);
packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js
+4
@@ -42,6 +42,7 @@ export type Options = {
42 temporaryReferences?: TemporaryReferenceSet,
43 findSourceMapURL?: FindSourceMapURLCallback,
44 replayConsoleLogs?: boolean,
45 + environmentName?: string,
46 };
47
48 function createResponseFromOptions(options: void | Options) {
@@ -58,6 +59,9 @@ function createResponseFromOptions(options: void | Options) {
59 ? options.findSourceMapURL
60 : undefined,
61 __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true
62 + __DEV__ && options && options.environmentName
63 + ? options.environmentName
64 + : undefined,
65 );
66 }
67
packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js
+4
@@ -72,6 +72,7 @@ export type Options = {
72 temporaryReferences?: TemporaryReferenceSet,
73 findSourceMapURL?: FindSourceMapURLCallback,
74 replayConsoleLogs?: boolean,
75 + environmentName?: string,
76 };
77
78 function createResponseFromOptions(options: Options) {
@@ -88,6 +89,9 @@ function createResponseFromOptions(options: Options) {
89 ? options.findSourceMapURL
90 : undefined,
91 __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
92 + __DEV__ && options && options.environmentName
93 + ? options.environmentName
94 + : undefined,
95 );
96 }
97
packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js
+4
@@ -62,6 +62,7 @@ export type Options = {
62 encodeFormAction?: EncodeFormActionCallback,
63 findSourceMapURL?: FindSourceMapURLCallback,
64 replayConsoleLogs?: boolean,
65 + environmentName?: string,
66 };
67
68 function createFromNodeStream<T>(
@@ -80,6 +81,9 @@ function createFromNodeStream<T>(
81 ? options.findSourceMapURL
82 : undefined,
83 __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
84 + __DEV__ && options && options.environmentName
85 + ? options.environmentName
86 + : undefined,
87 );
88 stream.on('data', chunk => {
89 if (typeof chunk === 'string') {