Skip to content

Commit

Permalink
Transfer debug info for server-to-server request
Browse files Browse the repository at this point in the history
A Flight Server can be a consumer of a stream from another Server.

In this case the meta data is attached to debugInfo properties on lazy,
Promises, Arrays or Elements that might in turn get forwarded to the next
stream. In this case we want to forward this debug information to the
client.
  • Loading branch information
sebmarkbage committed Feb 8, 2024
1 parent 2a299a6 commit 46fc2aa
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 3 deletions.
55 changes: 55 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1806,4 +1806,59 @@ describe('ReactFlight', () => {

expect(ReactNoop).toMatchRenderedOutput(<div>Ba</div>);
});

it('preserves debug info for server-to-server pass through', async () => {
function ThirdPartyLazyComponent() {
return <span>!</span>;
}

const lazy = React.lazy(async () => ({
default: <ThirdPartyLazyComponent />,
}));

function ThirdPartyComponent() {
return <span>stranger</span>;
}

function ServerComponent({transport}) {
// This is a Server Component that receives other Server Components from a third party.
const children = ReactNoopFlightClient.read(transport);
return <div>Hello, {children}</div>;
}

const promise = Promise.resolve(<ThirdPartyComponent />);

const thirdPartyTransport = ReactNoopFlightServer.render([promise, lazy]);

// Wait for the lazy component to initialize
await 0;

const transport = ReactNoopFlightServer.render(
<ServerComponent transport={thirdPartyTransport} />,
);

await act(async () => {
const promise = ReactNoopFlightClient.read(transport);
expect(promise._debugInfo).toEqual(
__DEV__ ? [{name: 'ServerComponent'}] : undefined,
);
const result = await promise;
const thirdPartyChildren = await result.props.children[1];
// We expect the debug info to be transferred from the inner stream to the outer.
expect(thirdPartyChildren[0]._debugInfo).toEqual(
__DEV__ ? [{name: 'ThirdPartyComponent'}] : undefined,
);
expect(thirdPartyChildren[1]._debugInfo).toEqual(
__DEV__ ? [{name: 'ThirdPartyLazyComponent'}] : undefined,
);
ReactNoop.render(result);
});

expect(ReactNoop).toMatchRenderedOutput(
<div>
Hello, <span>stranger</span>
<span>!</span>
</div>,
);
});
});
85 changes: 82 additions & 3 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';

initAsyncDebugInfo();

// Dev-only
type ReactDebugInfo = Array<{+name?: string}>;

const ObjectPrototype = Object.prototype;

type JSONValue =
Expand Down Expand Up @@ -325,6 +328,14 @@ function serializeThenable(
request.abortableTasks,
);

if (__DEV__) {
// If this came from Flight, forward any debug info into this new row.
const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
if (debugInfo) {
forwardDebugInfo(request, newTask.id, debugInfo);
}
}

switch (thenable.status) {
case 'fulfilled': {
// We have the resolved value, we can go ahead and schedule it for serialization.
Expand Down Expand Up @@ -475,6 +486,10 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
_payload: thenable,
_init: readThenable,
};
if (__DEV__) {
// If this came from React, transfer the debug info.
lazyType._debugInfo = (thenable: any)._debugInfo || [];
}
return lazyType;
}

Expand Down Expand Up @@ -552,6 +567,22 @@ function renderFragment(
task: Task,
children: $ReadOnlyArray<ReactClientValue>,
): ReactJSONValue {
if (__DEV__) {
const debugInfo: ?ReactDebugInfo = (children: any)._debugInfo;
if (debugInfo) {
// If this came from Flight, forward any debug info into this new row.
if (debugID === null) {
// We don't have a chunk to assign debug info. We need to outline this
// component to assign it an ID.
return outlineTask(request, task);
} else {
// Forward any debug info we have the first time we see it.
// We do this after init so that we have received all the debug info
// from the server by the time we emit it.
forwardDebugInfo(request, debugID, debugInfo);
}
}
}
if (!enableServerComponentKeys) {
return children;
}
Expand Down Expand Up @@ -1206,6 +1237,22 @@ function renderModelDestructive(
}

const element: React$Element<any> = (value: any);

if (__DEV__) {
const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo;
if (debugInfo) {
// If this came from Flight, forward any debug info into this new row.
if (debugID === null) {
// We don't have a chunk to assign debug info. We need to outline this
// component to assign it an ID.
return outlineTask(request, task);
} else {
// Forward any debug info we have the first time we see it.
forwardDebugInfo(request, debugID, debugInfo);
}
}
}

// Attempt to render the Server Component.
return renderElement(
request,
Expand All @@ -1218,9 +1265,30 @@ function renderModelDestructive(
);
}
case REACT_LAZY_TYPE: {
const payload = (value: any)._payload;
const init = (value: any)._init;
// Reset the task's thenable state before continuing. If there was one, it was
// from suspending the lazy before.
task.thenableState = null;

const lazy: LazyComponent<any, any> = (value: any);
const payload = lazy._payload;
const init = lazy._init;
const resolvedModel = init(payload);
if (__DEV__) {
const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
if (debugInfo) {
// If this came from Flight, forward any debug info into this new row.
if (debugID === null) {
// We don't have a chunk to assign debug info. We need to outline this
// component to assign it an ID.
return outlineTask(request, task);
} else {
// Forward any debug info we have the first time we see it.
// We do this after init so that we have received all the debug info
// from the server by the time we emit it.
forwardDebugInfo(request, debugID, debugInfo);
}
}
}
return renderModelDestructive(
request,
task,
Expand Down Expand Up @@ -1649,7 +1717,7 @@ function emitModelChunk(request: Request, id: number, json: string): void {
function emitDebugChunk(
request: Request,
id: number,
debugInfo: {name: string},
debugInfo: {+name?: string},
): void {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
Expand All @@ -1665,6 +1733,17 @@ function emitDebugChunk(
request.completedRegularChunks.push(processedChunk);
}

function forwardDebugInfo(
request: Request,
id: number,
debugInfo: ReactDebugInfo,
) {
for (let i = 0; i < debugInfo.length; i++) {
request.pendingChunks++;
emitDebugChunk(request, id, debugInfo[i]);
}
}

const emptyRoot = {};

function retryTask(request: Request, task: Task): void {
Expand Down

0 comments on commit 46fc2aa

Please sign in to comment.