Skip to content

Commit ff93c44

Browse files
authored
[Flight] Track Debug Info from Synchronously Unwrapped Promises (#33485)
Stacked on #33482. There's a flaw with getting information from the execution context of the ping. For the soft-deprecated "throw a promise" technique, this is a bit unreliable because you could in theory throw the same one multiple times. Similarly, a more fundamental flaw with that API is that it doesn't allow for tracking the information of Promises that are already synchronously able to resolve. This stops tracking the async debug info in the case of throwing a Promise and only when you render a Promise. That means some loss of data but we should just warn for throwing a Promise anyway. Instead, this also adds support for tracking `use()`d thenables and forwarding `_debugInfo` from then. This is done by extracting the info from the Promise after the fact instead of in the resolve so that it only happens once at the end after the pings are done. This also supports passing the same Promise in multiple places and tracking the debug info at each location, even if it was already instrumented with a synchronous value by the time of the second use.
1 parent 6c86e56 commit ff93c44

File tree

11 files changed

+845
-161
lines changed

11 files changed

+845
-161
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2991,6 +2991,64 @@ describe('ReactFlight', () => {
29912991
);
29922992
});
29932993

2994+
// @gate !__DEV__ || enableComponentPerformanceTrack
2995+
it('preserves debug info for server-to-server through use()', async () => {
2996+
function ThirdPartyComponent() {
2997+
return 'hi';
2998+
}
2999+
3000+
function ServerComponent({transport}) {
3001+
// This is a Server Component that receives other Server Components from a third party.
3002+
const text = ReactServer.use(ReactNoopFlightClient.read(transport));
3003+
return <div>{text.toUpperCase()}</div>;
3004+
}
3005+
3006+
const thirdPartyTransport = ReactNoopFlightServer.render(
3007+
<ThirdPartyComponent />,
3008+
{
3009+
environmentName: 'third-party',
3010+
},
3011+
);
3012+
3013+
const transport = ReactNoopFlightServer.render(
3014+
<ServerComponent transport={thirdPartyTransport} />,
3015+
);
3016+
3017+
await act(async () => {
3018+
const promise = ReactNoopFlightClient.read(transport);
3019+
expect(getDebugInfo(promise)).toEqual(
3020+
__DEV__
3021+
? [
3022+
{time: 16},
3023+
{
3024+
name: 'ServerComponent',
3025+
env: 'Server',
3026+
key: null,
3027+
stack: ' in Object.<anonymous> (at **)',
3028+
props: {
3029+
transport: expect.arrayContaining([]),
3030+
},
3031+
},
3032+
{time: 16},
3033+
{
3034+
name: 'ThirdPartyComponent',
3035+
env: 'third-party',
3036+
key: null,
3037+
stack: ' in Object.<anonymous> (at **)',
3038+
props: {},
3039+
},
3040+
{time: 16},
3041+
{time: 17},
3042+
]
3043+
: undefined,
3044+
);
3045+
const result = await promise;
3046+
ReactNoop.render(result);
3047+
});
3048+
3049+
expect(ReactNoop).toMatchRenderedOutput(<div>HI</div>);
3050+
});
3051+
29943052
it('preserves error stacks passed through server-to-server with source maps', async () => {
29953053
async function ServerComponent({transport}) {
29963054
// This is a Server Component that receives other Server Components from a third party.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ const deepProxyHandlers = {
161161
// reference.
162162
case 'defaultProps':
163163
return undefined;
164+
// React looks for debugInfo on thenables.
165+
case '_debugInfo':
166+
return undefined;
164167
// Avoid this attempting to be serialized.
165168
case 'toJSON':
166169
return undefined;
@@ -210,6 +213,9 @@ function getReference(target: Function, name: string | symbol): $FlowFixMe {
210213
// reference.
211214
case 'defaultProps':
212215
return undefined;
216+
// React looks for debugInfo on thenables.
217+
case '_debugInfo':
218+
return undefined;
213219
// Avoid this attempting to be serialized.
214220
case 'toJSON':
215221
return undefined;

packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ const deepProxyHandlers = {
162162
// reference.
163163
case 'defaultProps':
164164
return undefined;
165+
// React looks for debugInfo on thenables.
166+
case '_debugInfo':
167+
return undefined;
165168
// Avoid this attempting to be serialized.
166169
case 'toJSON':
167170
return undefined;
@@ -211,6 +214,9 @@ function getReference(target: Function, name: string | symbol): $FlowFixMe {
211214
// reference.
212215
case 'defaultProps':
213216
return undefined;
217+
// React looks for debugInfo on thenables.
218+
case '_debugInfo':
219+
return undefined;
214220
// Avoid this attempting to be serialized.
215221
case 'toJSON':
216222
return undefined;

packages/react-server/src/ReactFlightHooks.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ export function getThenableStateAfterSuspending(): ThenableState {
5858
return state;
5959
}
6060

61+
export function getTrackedThenablesAfterRendering(): null | Array<
62+
Thenable<any>,
63+
> {
64+
return thenableState;
65+
}
66+
6167
export const HooksDispatcher: Dispatcher = {
6268
readContext: (unsupportedContext: any),
6369

0 commit comments

Comments
 (0)