Skip to content

Commit 41b3e9a

Browse files
authored
[Fizz] Push a stalled use() to the ownerStack/debugTask (facebook#35226)
1 parent 195fd22 commit 41b3e9a

File tree

4 files changed

+527
-27
lines changed

4 files changed

+527
-27
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ describe('ReactFlightDOMNode', () => {
108108
);
109109
}
110110

111+
/**
112+
* Removes all stackframes not pointing into this file
113+
*/
114+
function ignoreListStack(str) {
115+
if (!str) {
116+
return str;
117+
}
118+
119+
let ignoreListedStack = '';
120+
const lines = str.split('\n');
121+
122+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
123+
for (const line of lines) {
124+
if (line.indexOf(__filename) === -1) {
125+
} else {
126+
ignoreListedStack += '\n' + line.replace(__dirname, '.');
127+
}
128+
}
129+
130+
return ignoreListedStack;
131+
}
132+
111133
function readResult(stream) {
112134
return new Promise((resolve, reject) => {
113135
let buffer = '';
@@ -784,6 +806,165 @@ describe('ReactFlightDOMNode', () => {
784806
}
785807
});
786808

809+
// @gate enableHalt
810+
it('includes source locations in component and owner stacks for halted Client components', async () => {
811+
function SharedComponent({p1, p2, p3}) {
812+
use(p1);
813+
use(p2);
814+
use(p3);
815+
return <div>Hello, Dave!</div>;
816+
}
817+
const ClientComponentOnTheServer = clientExports(SharedComponent);
818+
const ClientComponentOnTheClient = clientExports(
819+
SharedComponent,
820+
123,
821+
'path/to/chunk.js',
822+
);
823+
824+
let resolvePendingPromise;
825+
function ServerComponent() {
826+
const p1 = Promise.resolve();
827+
const p2 = new Promise(resolve => {
828+
resolvePendingPromise = value => {
829+
p2.status = 'fulfilled';
830+
p2.value = value;
831+
resolve(value);
832+
};
833+
});
834+
const p3 = new Promise(() => {});
835+
return ReactServer.createElement(ClientComponentOnTheClient, {
836+
p1: p1,
837+
p2: p2,
838+
p3: p3,
839+
});
840+
}
841+
842+
function App() {
843+
return ReactServer.createElement(
844+
'html',
845+
null,
846+
ReactServer.createElement(
847+
'body',
848+
null,
849+
ReactServer.createElement(
850+
ReactServer.Suspense,
851+
{fallback: 'Loading...'},
852+
ReactServer.createElement(ServerComponent, null),
853+
),
854+
),
855+
);
856+
}
857+
858+
const errors = [];
859+
const rscStream = await serverAct(() =>
860+
ReactServerDOMServer.renderToPipeableStream(
861+
ReactServer.createElement(App, null),
862+
webpackMap,
863+
),
864+
);
865+
866+
const readable = new Stream.PassThrough(streamOptions);
867+
rscStream.pipe(readable);
868+
869+
function ClientRoot({response}) {
870+
return use(response);
871+
}
872+
873+
const serverConsumerManifest = {
874+
moduleMap: {
875+
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
876+
'*': webpackMap[ClientComponentOnTheServer.$$id],
877+
},
878+
},
879+
moduleLoading: webpackModuleLoading,
880+
};
881+
882+
expect(errors).toEqual([]);
883+
884+
function ClientRoot({response}) {
885+
return use(response);
886+
}
887+
888+
const response = ReactServerDOMClient.createFromNodeStream(
889+
readable,
890+
serverConsumerManifest,
891+
);
892+
893+
let componentStack;
894+
let ownerStack;
895+
896+
const clientAbortController = new AbortController();
897+
898+
const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
899+
React.createElement(ClientRoot, {response}),
900+
{
901+
signal: clientAbortController.signal,
902+
onError(error, errorInfo) {
903+
componentStack = errorInfo.componentStack;
904+
ownerStack = React.captureOwnerStack
905+
? React.captureOwnerStack()
906+
: null;
907+
},
908+
},
909+
);
910+
911+
resolvePendingPromise('custom-instrum-resolve');
912+
await serverAct(
913+
async () =>
914+
new Promise(resolve => {
915+
setImmediate(() => {
916+
clientAbortController.abort();
917+
resolve();
918+
});
919+
}),
920+
);
921+
922+
const fizzPrerenderStream = await fizzPrerenderStreamResult;
923+
const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
924+
925+
expect(prerenderHTML).toContain('Loading...');
926+
927+
if (__DEV__) {
928+
expect(normalizeCodeLocInfo(componentStack)).toBe(
929+
'\n' +
930+
' in SharedComponent (at **)\n' +
931+
' in ServerComponent' +
932+
(gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)' : '') +
933+
'\n' +
934+
' in Suspense\n' +
935+
' in body\n' +
936+
' in html\n' +
937+
' in App (at **)\n' +
938+
' in ClientRoot (at **)',
939+
);
940+
} else {
941+
expect(normalizeCodeLocInfo(componentStack)).toBe(
942+
'\n' +
943+
' in SharedComponent (at **)\n' +
944+
' in Suspense\n' +
945+
' in body\n' +
946+
' in html\n' +
947+
' in ClientRoot (at **)',
948+
);
949+
}
950+
951+
if (__DEV__) {
952+
expect(ignoreListStack(ownerStack)).toBe(
953+
// eslint-disable-next-line react-internal/safe-string-coercion
954+
'' +
955+
// The concrete location may change as this test is updated.
956+
// Just make sure they still point at React.use(p2)
957+
(gate(flags => flags.enableAsyncDebugInfo)
958+
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813:7)'
959+
: '') +
960+
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835:26)' +
961+
'\n at App (file://./ReactFlightDOMNode-test.js:852:25)',
962+
);
963+
} else {
964+
expect(ownerStack).toBeNull();
965+
}
966+
});
967+
787968
// @gate enableHalt
788969
it('includes deeper location for aborted stacks', async () => {
789970
async function getData() {
@@ -1364,12 +1545,12 @@ describe('ReactFlightDOMNode', () => {
13641545
'\n' +
13651546
' in Dynamic' +
13661547
(gate(flags => flags.enableAsyncDebugInfo)
1367-
? ' (file://ReactFlightDOMNode-test.js:1238:27)\n'
1548+
? ' (file://ReactFlightDOMNode-test.js:1419:27)\n'
13681549
: '\n') +
13691550
' in body\n' +
13701551
' in html\n' +
1371-
' in App (file://ReactFlightDOMNode-test.js:1251:25)\n' +
1372-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1552+
' in App (file://ReactFlightDOMNode-test.js:1432:25)\n' +
1553+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13731554
);
13741555
} else {
13751556
expect(
@@ -1378,7 +1559,7 @@ describe('ReactFlightDOMNode', () => {
13781559
'\n' +
13791560
' in body\n' +
13801561
' in html\n' +
1381-
' in ClientRoot (ReactFlightDOMNode-test.js:1326:16)',
1562+
' in ClientRoot (ReactFlightDOMNode-test.js:1507:16)',
13821563
);
13831564
}
13841565

@@ -1388,16 +1569,16 @@ describe('ReactFlightDOMNode', () => {
13881569
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
13891570
).toBe(
13901571
'\n' +
1391-
' in Dynamic (file://ReactFlightDOMNode-test.js:1238:27)\n' +
1392-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1572+
' in Dynamic (file://ReactFlightDOMNode-test.js:1419:27)\n' +
1573+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
13931574
);
13941575
} else {
13951576
expect(
13961577
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
13971578
).toBe(
13981579
'' +
13991580
'\n' +
1400-
' in App (file://ReactFlightDOMNode-test.js:1251:25)',
1581+
' in App (file://ReactFlightDOMNode-test.js:1432:25)',
14011582
);
14021583
}
14031584
} else {

packages/react-server/src/ReactFizzServer.js

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ import assign from 'shared/assign';
190190
import noop from 'shared/noop';
191191
import getComponentNameFromType from 'shared/getComponentNameFromType';
192192
import isArray from 'shared/isArray';
193-
import {SuspenseException, getSuspendedThenable} from './ReactFizzThenable';
193+
import {
194+
SuspenseException,
195+
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
197+
getSuspendedCallSiteStackDEV,
198+
getSuspendedCallSiteDebugTaskDEV,
199+
setCaptureSuspendedCallSiteDEV,
200+
} from './ReactFizzThenable';
194201

195202
// Linked list representing the identity of a component given the component/tag name and key.
196203
// The name might be minified but we assume that it's going to be the same generated name. Typically
@@ -355,6 +362,7 @@ const OPEN = 11;
355362
const ABORTING = 12;
356363
const CLOSING = 13;
357364
const CLOSED = 14;
365+
const STALLED_DEV = 15;
358366

359367
export opaque type Request = {
360368
destination: null | Destination,
@@ -363,7 +371,7 @@ export opaque type Request = {
363371
+renderState: RenderState,
364372
+rootFormatContext: FormatContext,
365373
+progressiveChunkSize: number,
366-
status: 10 | 11 | 12 | 13 | 14,
374+
status: 10 | 11 | 12 | 13 | 14 | 15,
367375
fatalError: mixed,
368376
nextSegmentId: number,
369377
allPendingTasks: number, // when it reaches zero, we can close the connection.
@@ -1023,6 +1031,89 @@ function pushHaltedAwaitOnComponentStack(
10231031
}
10241032
}
10251033

1034+
// performWork + retryTask without mutation
1035+
function rerenderStalledTask(request: Request, task: Task): void {
1036+
const prevStatus = request.status;
1037+
request.status = STALLED_DEV;
1038+
1039+
const prevContext = getActiveContext();
1040+
const prevDispatcher = ReactSharedInternals.H;
1041+
ReactSharedInternals.H = HooksDispatcher;
1042+
const prevAsyncDispatcher = ReactSharedInternals.A;
1043+
ReactSharedInternals.A = DefaultAsyncDispatcher;
1044+
1045+
const prevRequest = currentRequest;
1046+
currentRequest = request;
1047+
1048+
const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;
1049+
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
1050+
1051+
const prevResumableState = currentResumableState;
1052+
setCurrentResumableState(request.resumableState);
1053+
switchContext(task.context);
1054+
const prevTaskInDEV = currentTaskInDEV;
1055+
setCurrentTaskInDEV(task);
1056+
try {
1057+
retryNode(request, task);
1058+
} catch (x) {
1059+
// Suspended again.
1060+
resetHooksState();
1061+
} finally {
1062+
setCurrentTaskInDEV(prevTaskInDEV);
1063+
setCurrentResumableState(prevResumableState);
1064+
1065+
ReactSharedInternals.H = prevDispatcher;
1066+
ReactSharedInternals.A = prevAsyncDispatcher;
1067+
1068+
ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;
1069+
if (prevDispatcher === HooksDispatcher) {
1070+
// This means that we were in a reentrant work loop. This could happen
1071+
// in a renderer that supports synchronous work like renderToString,
1072+
// when it's called from within another renderer.
1073+
// Normally we don't bother switching the contexts to their root/default
1074+
// values when leaving because we'll likely need the same or similar
1075+
// context again. However, when we're inside a synchronous loop like this
1076+
// we'll to restore the context to what it was before returning.
1077+
switchContext(prevContext);
1078+
}
1079+
currentRequest = prevRequest;
1080+
request.status = prevStatus;
1081+
}
1082+
}
1083+
1084+
function pushSuspendedCallSiteOnComponentStack(
1085+
request: Request,
1086+
task: Task,
1087+
): void {
1088+
setCaptureSuspendedCallSiteDEV(true);
1089+
const restoreThenableState = ensureSuspendableThenableStateDEV(
1090+
// refined at the callsite
1091+
((task.thenableState: any): ThenableState),
1092+
);
1093+
try {
1094+
rerenderStalledTask(request, task);
1095+
} finally {
1096+
restoreThenableState();
1097+
setCaptureSuspendedCallSiteDEV(false);
1098+
}
1099+
1100+
const suspendCallSiteStack = getSuspendedCallSiteStackDEV();
1101+
const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();
1102+
1103+
if (suspendCallSiteStack !== null) {
1104+
const ownerStack = task.componentStack;
1105+
task.componentStack = {
1106+
// The owner of the suspended call site would be the owner of this task.
1107+
// We need the task itself otherwise we'd miss a frame.
1108+
owner: ownerStack,
1109+
parent: suspendCallSiteStack.parent,
1110+
stack: suspendCallSiteStack.stack,
1111+
type: suspendCallSiteStack.type,
1112+
};
1113+
}
1114+
task.debugTask = suspendCallSiteDebugTask;
1115+
}
1116+
10261117
function pushServerComponentStack(
10271118
task: Task,
10281119
debugInfo: void | null | ReactDebugInfo,
@@ -2723,7 +2814,12 @@ function renderLazyComponent(
27232814
const init = lazyComponent._init;
27242815
Component = init(payload);
27252816
}
2726-
if (request.status === ABORTING) {
2817+
if (
2818+
request.status === ABORTING &&
2819+
// We're going to discard this render anyway.
2820+
// We just need to reach the point where we suspended in dev.
2821+
(!__DEV__ || request.status !== STALLED_DEV)
2822+
) {
27272823
// eslint-disable-next-line no-throw-literal
27282824
throw null;
27292825
}
@@ -4535,12 +4631,9 @@ function abortTask(task: Task, request: Request, error: mixed): void {
45354631
debugInfo = node._debugInfo;
45364632
}
45374633
pushHaltedAwaitOnComponentStack(task, debugInfo);
4538-
/*
45394634
if (task.thenableState !== null) {
4540-
// TODO: If we were stalled inside use() of a Client Component then we should
4541-
// rerender to get the stack trace from the use() call.
4635+
pushSuspendedCallSiteOnComponentStack(request, task);
45424636
}
4543-
*/
45444637
}
45454638
}
45464639

0 commit comments

Comments
 (0)