@samitouri / QOS-React-2 / commits / 55c9d45f3b

[Flight] Let environmentName vary over time by making it a function of string (#29867)

This lets the environment name vary within a request by the context a component, log or error being executed in. A potentially different API would be something like `setEnvironmentName()` but we'd have to extend the `ReadableStream` or something to do that like we do for `.allReady`. As a function though it has some expansion possibilities, e.g. we could potentially also pass some information to it for context about what is being asked for. If it changes before completing a task, we also emit the change so that we have the debug info for what the environment was before entering a component and what it was after completing it.

Sebastian Markbåge committed Jun 12, 2024 at 10:55 UTC 55c9d45f3b9239d5997788f10b8cdad5bcb81daf
10 files changed +92 -18
packages/react-client/src/__tests__/ReactFlight-test.js
+44
@@ -2565,6 +2565,50 @@ describe('ReactFlight', () => {
2565 );
2566 });
2567
2568 + it('can change the environment name inside a component', async () => {
2569 + let env = 'A';
2570 + function Component(props) {
2571 + env = 'B';
2572 + return <div>hi</div>;
2573 + }
2574 +
2575 + const transport = ReactNoopFlightServer.render(
2576 + {
2577 + greeting: <Component />,
2578 + },
2579 + {
2580 + environmentName() {
2581 + return env;
2582 + },
2583 + },
2584 + );
2585 +
2586 + await act(async () => {
2587 + const rootModel = await ReactNoopFlightClient.read(transport);
2588 + const greeting = rootModel.greeting;
2589 + expect(getDebugInfo(greeting)).toEqual(
2590 + __DEV__
2591 + ? [
2592 + {
2593 + name: 'Component',
2594 + env: 'A',
2595 + owner: null,
2596 + stack: gate(flag => flag.enableOwnerStacks)
2597 + ? ' in Object.<anonymous> (at **)'
2598 + : undefined,
2599 + },
2600 + {
2601 + env: 'B',
2602 + },
2603 + ]
2604 + : undefined,
2605 + );
2606 + ReactNoop.render(greeting);
2607 + });
2608 +
2609 + expect(ReactNoop).toMatchRenderedOutput(<div>hi</div>);
2610 + });
2611 +
2612 // @gate enableServerComponentLogs && __DEV__
2613 it('replays logs, but not onError logs', async () => {
2614 function foo() {
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+1 -1
@@ -67,7 +67,7 @@ const ReactNoopFlightServer = ReactFlightServer({
67 });
68
69 type Options = {
70 - environmentName?: string,
70 + environmentName?: string | (() => string),
71 identifierPrefix?: string,
72 onError?: (error: mixed) => void,
73 onPostpone?: (reason: string) => void,
packages/react-server-dom-esm/src/ReactFlightDOMServerNode.js
+1 -1
@@ -66,7 +66,7 @@ function createCancelHandler(request: Request, reason: string) {
66 }
67
68 type Options = {
69 - environmentName?: string,
69 + environmentName?: string | (() => string),
70 onError?: (error: mixed) => void,
71 onPostpone?: (reason: string) => void,
72 identifierPrefix?: string,
packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js
+1 -1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44 export type {TemporaryReferenceSet};
45
46 type Options = {
47 - environmentName?: string,
47 + environmentName?: string | (() => string),
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js
+1 -1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44 export type {TemporaryReferenceSet};
45
46 type Options = {
47 - environmentName?: string,
47 + environmentName?: string | (() => string),
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js
+1 -1
@@ -67,7 +67,7 @@ function createCancelHandler(request: Request, reason: string) {
67 }
68
69 type Options = {
70 - environmentName?: string,
70 + environmentName?: string | (() => string),
71 onError?: (error: mixed) => void,
72 onPostpone?: (reason: string) => void,
73 identifierPrefix?: string,
packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js
+1 -1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44 export type {TemporaryReferenceSet};
45
46 type Options = {
47 - environmentName?: string,
47 + environmentName?: string | (() => string),
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js
+1 -1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44 export type {TemporaryReferenceSet};
45
46 type Options = {
47 - environmentName?: string,
47 + environmentName?: string | (() => string),
48 identifierPrefix?: string,
49 signal?: AbortSignal,
50 temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js
+1 -1
@@ -67,7 +67,7 @@ function createCancelHandler(request: Request, reason: string) {
67 }
68
69 type Options = {
70 - environmentName?: string,
70 + environmentName?: string | (() => string),
71 onError?: (error: mixed) => void,
72 onPostpone?: (reason: string) => void,
73 identifierPrefix?: string,
packages/react-server/src/ReactFlightServer.js
+40 -10
@@ -393,6 +393,7 @@ type Task = {
393 keyPath: null | string, // parent server component keys
394 implicitSlot: boolean, // true if the root server component of this sequence had a null key
395 thenableState: ThenableState | null,
396 + environmentName: string, // DEV-only. Used to track if the environment for this task changed.
397 };
398
399 interface Reference {}
@@ -425,7 +426,7 @@ export type Request = {
426 onError: (error: mixed) => ?string,
427 onPostpone: (reason: string) => void,
428 // DEV-only
428 - environmentName: string,
429 + environmentName: () => string,
430 didWarnForKey: null | WeakSet<ReactComponentInfo>,
431 };
432
@@ -481,7 +482,7 @@ function RequestInstance(
482 onError: void | ((error: mixed) => ?string),
483 identifierPrefix?: string,
484 onPostpone: void | ((reason: string) => void),
484 - environmentName: void | string,
485 + environmentName: void | string | (() => string),
486 temporaryReferences: void | TemporaryReferenceSet,
487 ) {
488 if (
@@ -531,7 +532,11 @@ function RequestInstance(
532
533 if (__DEV__) {
534 this.environmentName =
534 - environmentName === undefined ? 'Server' : environmentName;
535 + environmentName === undefined
536 + ? () => 'Server'
537 + : typeof environmentName !== 'function'
538 + ? () => environmentName
539 + : environmentName;
540 this.didWarnForKey = null;
541 }
542 const rootTask = createTask(this, model, null, false, abortSet);
@@ -544,7 +549,7 @@ export function createRequest(
549 onError: void | ((error: mixed) => ?string),
550 identifierPrefix?: string,
551 onPostpone: void | ((reason: string) => void),
547 - environmentName: void | string,
552 + environmentName: void | string | (() => string),
553 temporaryReferences: void | TemporaryReferenceSet,
554 ): Request {
555 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
@@ -1049,14 +1054,14 @@ function renderFunctionComponent<Props>(
1054 componentDebugInfo = (prevThenableState: any)._componentDebugInfo;
1055 } else {
1056 // This is a new component in the same task so we can emit more debug info.
1057 + const componentDebugID = debugID;
1058 const componentName =
1059 (Component: any).displayName || Component.name || '';
1060 + const componentEnv = request.environmentName();
1061 request.pendingChunks++;
1055 -
1056 - const componentDebugID = debugID;
1062 componentDebugInfo = ({
1063 name: componentName,
1059 - env: request.environmentName,
1064 + env: componentEnv,
1065 owner: owner,
1066 }: ReactComponentInfo);
1067 if (enableOwnerStacks) {
@@ -1069,6 +1074,9 @@ function renderFunctionComponent<Props>(
1074 outlineModel(request, componentDebugInfo);
1075 emitDebugChunk(request, componentDebugID, componentDebugInfo);
1076
1077 + // We've emitted the latest environment for this task so we track that.
1078 + task.environmentName = componentEnv;
1079 +
1080 if (enableOwnerStacks) {
1081 warnForMissingKey(request, key, validated, componentDebugInfo);
1082 }
@@ -1644,7 +1652,7 @@ function createTask(
1652 request.writtenObjects.set(model, serializeByValueID(id));
1653 }
1654 }
1647 - const task: Task = {
1655 + const task: Task = (({
1656 id,
1657 status: PENDING,
1658 model,
@@ -1697,7 +1705,10 @@ function createTask(
1705 return renderModel(request, task, parent, parentPropertyName, value);
1706 },
1707 thenableState: null,
1700 - };
1708 + }: Omit<Task, 'environmentName'>): any);
1709 + if (__DEV__) {
1710 + task.environmentName = request.environmentName();
1711 + }
1712 abortSet.add(task);
1713 return task;
1714 }
@@ -3252,7 +3263,7 @@ function emitConsoleChunk(
3263 }
3264
3265 // TODO: Don't double badge if this log came from another Flight Client.
3255 - const env = request.environmentName;
3266 + const env = request.environmentName();
3267 const payload = [methodName, stackTrace, owner, env];
3268 // $FlowFixMe[method-unbinding]
3269 payload.push.apply(payload, args);
@@ -3420,6 +3431,15 @@ function retryTask(request: Request, task: Task): void {
3431 // any future references.
3432 request.writtenObjects.set(resolvedModel, serializeByValueID(task.id));
3433
3434 + if (__DEV__) {
3435 + const currentEnv = request.environmentName();
3436 + if (currentEnv !== task.environmentName) {
3437 + // The environment changed since we last emitted any debug information for this
3438 + // task. We emit an entry that just includes the environment name change.
3439 + emitDebugChunk(request, task.id, {env: currentEnv});
3440 + }
3441 + }
3442 +
3443 // Object might contain unresolved values like additional elements.
3444 // This is simulating what the JSON loop would do if this was part of it.
3445 emitChunk(request, task, resolvedModel);
@@ -3428,6 +3448,16 @@ function retryTask(request: Request, task: Task): void {
3448 // We don't need to escape it again so it's not passed the toJSON replacer.
3449 // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
3450 const json: string = stringify(resolvedModel);
3451 +
3452 + if (__DEV__) {
3453 + const currentEnv = request.environmentName();
3454 + if (currentEnv !== task.environmentName) {
3455 + // The environment changed since we last emitted any debug information for this
3456 + // task. We emit an entry that just includes the environment name change.
3457 + emitDebugChunk(request, task.id, {env: currentEnv});
3458 + }
3459 + }
3460 +
3461 emitModelChunk(request, task.id, json);
3462 }
3463