@samitouri / QOS-React-1 / commits / 561ee24d4a

[Fizz] Push halted await to the owner stack for late-arriving I/O info (#35019)

Hendrik Liebau committed Nov 1, 2025 at 16:03 UTC 561ee24d4a7c805e9837ee4cfbb5671a35e41b5a
14 files changed +363 -40
packages/react-client/src/ReactFlightClient.js
+79 -26
@@ -367,6 +367,7 @@ type Response = {
367 _debugRootStack?: null | Error, // DEV-only
368 _debugRootTask?: null | ConsoleTask, // DEV-only
369 _debugStartTime: number, // DEV-only
370 + _debugEndTime?: number, // DEV-only
371 _debugIOStarted: boolean, // DEV-only
372 _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
373 _debugChannel?: void | DebugChannel, // DEV-only
@@ -500,6 +501,34 @@ function createErrorChunk<T>(
501 return new ReactPromise(ERRORED, null, error);
502 }
503
504 +function filterDebugInfo(
505 + response: Response,
506 + value: {_debugInfo: ReactDebugInfo, ...},
507 +) {
508 + if (response._debugEndTime === null) {
509 + // No end time was defined, so we keep all debug info entries.
510 + return;
511 + }
512 +
513 + // Remove any debug info entries that arrived after the defined end time.
514 + const relativeEndTime =
515 + response._debugEndTime -
516 + // $FlowFixMe[prop-missing]
517 + performance.timeOrigin;
518 + const debugInfo = [];
519 + for (let i = 0; i < value._debugInfo.length; i++) {
520 + const info = value._debugInfo[i];
521 + if (typeof info.time === 'number' && info.time > relativeEndTime) {
522 + break;
523 + }
524 + if (info.awaited != null && info.awaited.end > relativeEndTime) {
525 + break;
526 + }
527 + debugInfo.push(info);
528 + }
529 + value._debugInfo = debugInfo;
530 +}
531 +
532 function moveDebugInfoFromChunkToInnerValue<T>(
533 chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
534 value: T,
@@ -534,7 +563,17 @@ function moveDebugInfoFromChunkToInnerValue<T>(
563 }
564 }
565
566 +function processChunkDebugInfo<T>(
567 + response: Response,
568 + chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
569 + value: T,
570 +): void {
571 + filterDebugInfo(response, chunk);
572 + moveDebugInfoFromChunkToInnerValue(chunk, value);
573 +}
574 +
575 function wakeChunk<T>(
576 + response: Response,
577 listeners: Array<InitializationReference | (T => mixed)>,
578 value: T,
579 chunk: InitializedChunk<T>,
@@ -544,16 +583,17 @@ function wakeChunk<T>(
583 if (typeof listener === 'function') {
584 listener(value);
585 } else {
547 - fulfillReference(listener, value, chunk);
586 + fulfillReference(response, listener, value, chunk);
587 }
588 }
589
590 if (__DEV__) {
552 - moveDebugInfoFromChunkToInnerValue(chunk, value);
591 + processChunkDebugInfo(response, chunk, value);
592 }
593 }
594
595 function rejectChunk(
596 + response: Response,
597 listeners: Array<InitializationReference | (mixed => mixed)>,
598 error: mixed,
599 ): void {
@@ -562,7 +602,7 @@ function rejectChunk(
602 if (typeof listener === 'function') {
603 listener(error);
604 } else {
565 - rejectReference(listener, error);
605 + rejectReference(response, listener.handler, error);
606 }
607 }
608 }
@@ -595,13 +635,14 @@ function resolveBlockedCycle<T>(
635 }
636
637 function wakeChunkIfInitialized<T>(
638 + response: Response,
639 chunk: SomeChunk<T>,
640 resolveListeners: Array<InitializationReference | (T => mixed)>,
641 rejectListeners: null | Array<InitializationReference | (mixed => mixed)>,
642 ): void {
643 switch (chunk.status) {
644 case INITIALIZED:
604 - wakeChunk(resolveListeners, chunk.value, chunk);
645 + wakeChunk(response, resolveListeners, chunk.value, chunk);
646 break;
647 case BLOCKED:
648 // It is possible that we're blocked on our own chunk if it's a cycle.
@@ -615,7 +656,7 @@ function wakeChunkIfInitialized<T>(
656 if (cyclicHandler !== null) {
657 // This reference points back to this chunk. We can resolve the cycle by
658 // using the value from that handler.
618 - fulfillReference(reference, cyclicHandler.value, chunk);
659 + fulfillReference(response, reference, cyclicHandler.value, chunk);
660 resolveListeners.splice(i, 1);
661 i--;
662 if (rejectListeners !== null) {
@@ -629,6 +670,7 @@ function wakeChunkIfInitialized<T>(
670 case INITIALIZED:
671 const initializedChunk: InitializedChunk<T> = (chunk: any);
672 wakeChunk(
673 + response,
674 resolveListeners,
675 initializedChunk.value,
676 initializedChunk,
@@ -636,7 +678,7 @@ function wakeChunkIfInitialized<T>(
678 return;
679 case ERRORED:
680 if (rejectListeners !== null) {
639 - rejectChunk(rejectListeners, chunk.reason);
681 + rejectChunk(response, rejectListeners, chunk.reason);
682 }
683 return;
684 }
@@ -666,7 +708,7 @@ function wakeChunkIfInitialized<T>(
708 break;
709 case ERRORED:
710 if (rejectListeners) {
669 - rejectChunk(rejectListeners, chunk.reason);
711 + rejectChunk(response, rejectListeners, chunk.reason);
712 }
713 break;
714 }
@@ -724,7 +766,7 @@ function triggerErrorOnChunk<T>(
766 erroredChunk.status = ERRORED;
767 erroredChunk.reason = error;
768 if (listeners !== null) {
727 - rejectChunk(listeners, error);
769 + rejectChunk(response, listeners, error);
770 }
771 }
772
@@ -832,7 +874,7 @@ function resolveModelChunk<T>(
874 // longer be rendered or might not be the highest pri.
875 initializeModelChunk(resolvedChunk);
876 // The status might have changed after initialization.
835 - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
877 + wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
878 }
879 }
880
@@ -861,12 +903,11 @@ function resolveModuleChunk<T>(
903 }
904 if (resolveListeners !== null) {
905 initializeModuleChunk(resolvedChunk);
864 - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
906 + wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners);
907 }
908 }
909
910 type InitializationReference = {
869 - response: Response, // TODO: Remove Response from here and pass it through instead.
911 handler: InitializationHandler,
912 parentObject: Object,
913 key: string,
@@ -1005,7 +1046,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
1046 if (typeof listener === 'function') {
1047 listener(value);
1048 } else {
1008 - fulfillReference(listener, value, cyclicChunk);
1049 + fulfillReference(response, listener, value, cyclicChunk);
1050 }
1051 }
1052 }
@@ -1026,7 +1067,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
1067 initializedChunk.value = value;
1068
1069 if (__DEV__) {
1029 - moveDebugInfoFromChunkToInnerValue(initializedChunk, value);
1070 + processChunkDebugInfo(response, initializedChunk, value);
1071 }
1072 } catch (error) {
1073 const erroredChunk: ErroredChunk<T> = (chunk: any);
@@ -1413,11 +1454,12 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
1454 }
1455
1456 function fulfillReference(
1457 + response: Response,
1458 reference: InitializationReference,
1459 value: any,
1460 fulfilledChunk: SomeChunk<any>,
1461 ): void {
1420 - const {response, handler, parentObject, key, map, path} = reference;
1462 + const {handler, parentObject, key, map, path} = reference;
1463
1464 for (let i = 1; i < path.length; i++) {
1465 while (
@@ -1487,7 +1529,11 @@ function fulfillReference(
1529 return;
1530 }
1531 default: {
1490 - rejectReference(reference, referencedChunk.reason);
1532 + rejectReference(
1533 + response,
1534 + reference.handler,
1535 + referencedChunk.reason,
1536 + );
1537 return;
1538 }
1539 }
@@ -1585,21 +1631,20 @@ function fulfillReference(
1631 initializedChunk.value = handler.value;
1632 initializedChunk.reason = handler.reason; // Used by streaming chunks
1633 if (resolveListeners !== null) {
1588 - wakeChunk(resolveListeners, handler.value, initializedChunk);
1634 + wakeChunk(response, resolveListeners, handler.value, initializedChunk);
1635 } else {
1636 if (__DEV__) {
1591 - moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
1637 + processChunkDebugInfo(response, initializedChunk, handler.value);
1638 }
1639 }
1640 }
1641 }
1642
1643 function rejectReference(
1598 - reference: InitializationReference,
1644 + response: Response,
1645 + handler: InitializationHandler,
1646 error: mixed,
1647 ): void {
1601 - const {handler, response} = reference;
1602 -
1648 if (handler.errored) {
1649 // We've already errored. We could instead build up an AggregateError
1650 // but if there are multiple errors we just take the first one like
@@ -1690,7 +1735,6 @@ function waitForReference<T>(
1735 }
1736
1737 const reference: InitializationReference = {
1693 - response,
1738 handler,
1739 parentObject,
1740 key,
@@ -1838,10 +1882,10 @@ function loadServerReference<A: Iterable<any>, T>(
1882 initializedChunk.status = INITIALIZED;
1883 initializedChunk.value = handler.value;
1884 if (resolveListeners !== null) {
1841 - wakeChunk(resolveListeners, handler.value, initializedChunk);
1885 + wakeChunk(response, resolveListeners, handler.value, initializedChunk);
1886 } else {
1887 if (__DEV__) {
1844 - moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
1888 + processChunkDebugInfo(response, initializedChunk, handler.value);
1889 }
1890 }
1891 }
@@ -2578,6 +2622,7 @@ function ResponseInstance(
2622 replayConsole: boolean, // DEV-only
2623 environmentName: void | string, // DEV-only
2624 debugStartTime: void | number, // DEV-only
2625 + debugEndTime: void | number, // DEV-only
2626 debugChannel: void | DebugChannel, // DEV-only
2627 ) {
2628 const chunks: Map<number, SomeChunk<any>> = new Map();
@@ -2645,6 +2690,7 @@ function ResponseInstance(
2690 // and is not considered I/O required to load the stream.
2691 setTimeout(markIOStarted.bind(this), 0);
2692 }
2693 + this._debugEndTime = debugEndTime == null ? null : debugEndTime;
2694 this._debugFindSourceMapURL = findSourceMapURL;
2695 this._debugChannel = debugChannel;
2696 this._blockedConsole = null;
@@ -2688,6 +2734,7 @@ export function createResponse(
2734 replayConsole: boolean, // DEV-only
2735 environmentName: void | string, // DEV-only
2736 debugStartTime: void | number, // DEV-only
2737 + debugEndTime: void | number, // DEV-only
2738 debugChannel: void | DebugChannel, // DEV-only
2739 ): WeakResponse {
2740 return getWeakResponse(
@@ -2704,6 +2751,7 @@ export function createResponse(
2751 replayConsole,
2752 environmentName,
2753 debugStartTime,
2754 + debugEndTime,
2755 debugChannel,
2756 ),
2757 );
@@ -3075,10 +3123,10 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
3123 resolvedChunk.value = stream;
3124 resolvedChunk.reason = controller;
3125 if (resolveListeners !== null) {
3078 - wakeChunk(resolveListeners, chunk.value, (chunk: any));
3126 + wakeChunk(response, resolveListeners, chunk.value, (chunk: any));
3127 } else {
3128 if (__DEV__) {
3081 - moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream);
3129 + processChunkDebugInfo(response, resolvedChunk, stream);
3130 }
3131 }
3132 }
@@ -3218,7 +3266,12 @@ function startAsyncIterable<T>(
3266 initializedChunk.status = INITIALIZED;
3267 initializedChunk.value = {done: false, value: value};
3268 if (resolveListeners !== null) {
3221 - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
3269 + wakeChunkIfInitialized(
3270 + response,
3271 + chunk,
3272 + resolveListeners,
3273 + rejectListeners,
3274 + );
3275 }
3276 }
3277 nextWriteIndex++;
packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
+2
@@ -53,6 +53,7 @@ export type Options = {
53 replayConsoleLogs?: boolean,
54 environmentName?: string,
55 startTime?: number,
56 + endTime?: number,
57 };
58
59 function createDebugCallbackFromWritableStream(
@@ -107,6 +108,7 @@ function createResponseFromOptions(options: void | Options) {
108 __DEV__ && options && options.startTime != null
109 ? options.startTime
110 : undefined,
111 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
112 debugChannel,
113 );
114 }
packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js
+2
@@ -58,6 +58,7 @@ export type Options = {
58 replayConsoleLogs?: boolean,
59 environmentName?: string,
60 startTime?: number,
61 + endTime?: number,
62 // For the Node.js client we only support a single-direction debug channel.
63 debugChannel?: Readable,
64 };
@@ -116,6 +117,7 @@ function createFromNodeStream<T>(
117 __DEV__ && options && options.startTime != null
118 ? options.startTime
119 : undefined,
120 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
121 debugChannel,
122 );
123
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js
+2
@@ -132,6 +132,7 @@ function createResponseFromOptions(options: void | Options) {
132 __DEV__ && options && options.startTime != null
133 ? options.startTime
134 : undefined,
135 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
136 debugChannel,
137 );
138 }
@@ -209,6 +210,7 @@ export type Options = {
210 replayConsoleLogs?: boolean,
211 environmentName?: string,
212 startTime?: number,
213 + endTime?: number,
214 };
215
216 export function createFromReadableStream<T>(
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js
+2
@@ -80,6 +80,7 @@ export type Options = {
80 replayConsoleLogs?: boolean,
81 environmentName?: string,
82 startTime?: number,
83 + endTime?: number,
84 // For the Edge client we only support a single-direction debug channel.
85 debugChannel?: {readable?: ReadableStream, ...},
86 };
@@ -111,6 +112,7 @@ function createResponseFromOptions(options?: Options) {
112 __DEV__ && options && options.startTime != null
113 ? options.startTime
114 : undefined,
115 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
116 debugChannel,
117 );
118 }
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js
+2
@@ -53,6 +53,7 @@ export type Options = {
53 replayConsoleLogs?: boolean,
54 environmentName?: string,
55 startTime?: number,
56 + endTime?: number,
57 // For the Node.js client we only support a single-direction debug channel.
58 debugChannel?: Readable,
59 };
@@ -107,6 +108,7 @@ export function createFromNodeStream<T>(
108 __DEV__ && options && options.startTime != null
109 ? options.startTime
110 : undefined,
111 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
112 debugChannel,
113 );
114
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
+2
@@ -52,6 +52,7 @@ export type Options = {
52 replayConsoleLogs?: boolean,
53 environmentName?: string,
54 startTime?: number,
55 + endTime?: number,
56 };
57
58 function createDebugCallbackFromWritableStream(
@@ -106,6 +107,7 @@ function createResponseFromOptions(options: void | Options) {
107 __DEV__ && options && options.startTime != null
108 ? options.startTime
109 : undefined,
110 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
111 debugChannel,
112 );
113 }
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js
+2
@@ -80,6 +80,7 @@ export type Options = {
80 replayConsoleLogs?: boolean,
81 environmentName?: string,
82 startTime?: number,
83 + endTime?: number,
84 // For the Edge client we only support a single-direction debug channel.
85 debugChannel?: {readable?: ReadableStream, ...},
86 };
@@ -113,6 +114,7 @@ function createResponseFromOptions(options: Options) {
114 __DEV__ && options && options.startTime != null
115 ? options.startTime
116 : undefined,
117 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
118 debugChannel,
119 );
120 }
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js
+2
@@ -61,6 +61,7 @@ export type Options = {
61 replayConsoleLogs?: boolean,
62 environmentName?: string,
63 startTime?: number,
64 + endTime?: number,
65 // For the Node.js client we only support a single-direction debug channel.
66 debugChannel?: Readable,
67 };
@@ -118,6 +119,7 @@ function createFromNodeStream<T>(
119 __DEV__ && options && options.startTime != null
120 ? options.startTime
121 : undefined,
122 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
123 debugChannel,
124 );
125
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+234 -4
@@ -86,12 +86,25 @@ describe('ReactFlightDOMNode', () => {
86 );
87 }
88
89 - function normalizeCodeLocInfo(str) {
89 + const relativeFilename = path.relative(__dirname, __filename);
90 +
91 + function normalizeCodeLocInfo(str, {preserveLocation = false} = {}) {
92 return (
93 str &&
92 - str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
93 - return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
94 - })
94 + str.replace(
95 + /^ +(?:at|in) ([\S]+) ([^\n]*)/gm,
96 + function (m, name, location) {
97 + return (
98 + ' in ' +
99 + name +
100 + (/\d/.test(m)
101 + ? preserveLocation
102 + ? ' ' + location.replace(__filename, relativeFilename)
103 + : ' (at **)'
104 + : '')
105 + );
106 + },
107 + )
108 );
109 }
110
@@ -1169,4 +1182,221 @@ describe('ReactFlightDOMNode', () => {
1182 // Must not throw an error.
1183 await readable.pipeTo(writable);
1184 });
1185 +
1186 + describe('with real timers', () => {
1187 + // These tests schedule their rendering in a way that requires real timers
1188 + // to be used to accurately represent how this interacts with React's
1189 + // internal scheduling.
1190 +
1191 + beforeEach(() => {
1192 + jest.useRealTimers();
1193 + });
1194 +
1195 + afterEach(() => {
1196 + jest.useFakeTimers();
1197 + });
1198 +
1199 + it('should use late-arriving I/O debug info to enhance component and owner stacks when aborting a prerender', async () => {
1200 + // This test is constructing a scenario where a framework might separate
1201 + // I/O into different phases, e.g. runtime I/O and dynamic I/O. The
1202 + // framework might choose to define an end time for the Flight client,
1203 + // indicating that all I/O info (or any debug info for that matter) that
1204 + // arrives after that time should be ignored. When rendering in Fizz is
1205 + // then aborted, the late-arriving debug info that's used to enhance the
1206 + // owner stack only includes I/O info up to that end time.
1207 + let resolveRuntimeData;
1208 + let resolveDynamicData;
1209 +
1210 + async function getRuntimeData() {
1211 + return new Promise(resolve => {
1212 + resolveRuntimeData = resolve;
1213 + });
1214 + }
1215 +
1216 + async function getDynamicData() {
1217 + return new Promise(resolve => {
1218 + resolveDynamicData = resolve;
1219 + });
1220 + }
1221 +
1222 + async function Dynamic() {
1223 + const runtimeData = await getRuntimeData();
1224 + const dynamicData = await getDynamicData();
1225 +
1226 + return (
1227 + <p>
1228 + {runtimeData} {dynamicData}
1229 + </p>
1230 + );
1231 + }
1232 +
1233 + function App() {
1234 + return ReactServer.createElement(
1235 + 'html',
1236 + null,
1237 + ReactServer.createElement(
1238 + 'body',
1239 + null,
1240 + ReactServer.createElement(Dynamic),
1241 + ),
1242 + );
1243 + }
1244 +
1245 + const stream = await ReactServerDOMServer.renderToPipeableStream(
1246 + ReactServer.createElement(App),
1247 + webpackMap,
1248 + {filterStackFrame},
1249 + );
1250 +
1251 + const initialChunks = [];
1252 + const dynamicChunks = [];
1253 + let isDynamic = false;
1254 +
1255 + const passThrough = new Stream.PassThrough(streamOptions);
1256 + stream.pipe(passThrough);
1257 +
1258 + passThrough.on('data', chunk => {
1259 + if (isDynamic) {
1260 + dynamicChunks.push(chunk);
1261 + } else {
1262 + initialChunks.push(chunk);
1263 + }
1264 + });
1265 +
1266 + let endTime;
1267 +
1268 + await new Promise(resolve => {
1269 + setTimeout(() => {
1270 + resolveRuntimeData('Hi');
1271 + });
1272 + setTimeout(() => {
1273 + isDynamic = true;
1274 + endTime = performance.now() + performance.timeOrigin;
1275 + resolveDynamicData('Josh');
1276 + resolve();
1277 + });
1278 + });
1279 +
1280 + await new Promise(resolve => {
1281 + passThrough.on('end', resolve);
1282 + });
1283 +
1284 + // Create a new Readable and push all initial chunks immediately.
1285 + const readable = new Stream.Readable({...streamOptions, read() {}});
1286 + for (let i = 0; i < initialChunks.length; i++) {
1287 + readable.push(initialChunks[i]);
1288 + }
1289 +
1290 + const abortController = new AbortController();
1291 +
1292 + // When prerendering is aborted, push all dynamic chunks. They won't be
1293 + // considered for rendering, but they include debug info we want to use.
1294 + abortController.signal.addEventListener(
1295 + 'abort',
1296 + () => {
1297 + for (let i = 0; i < dynamicChunks.length; i++) {
1298 + readable.push(dynamicChunks[i]);
1299 + }
1300 + },
1301 + {once: true},
1302 + );
1303 +
1304 + const response = ReactServerDOMClient.createFromNodeStream(
1305 + readable,
1306 + {
1307 + serverConsumerManifest: {
1308 + moduleMap: null,
1309 + moduleLoading: null,
1310 + },
1311 + },
1312 + {
1313 + // Debug info arriving after this end time will be ignored, e.g. the
1314 + // I/O info for the dynamic data.
1315 + endTime,
1316 + },
1317 + );
1318 +
1319 + function ClientRoot() {
1320 + return use(response);
1321 + }
1322 +
1323 + let componentStack;
1324 + let ownerStack;
1325 +
1326 + const {prelude} = await new Promise(resolve => {
1327 + let result;
1328 +
1329 + setTimeout(() => {
1330 + result = ReactDOMFizzStatic.prerenderToNodeStream(
1331 + React.createElement(ClientRoot),
1332 + {
1333 + signal: abortController.signal,
1334 + onError(error, errorInfo) {
1335 + componentStack = errorInfo.componentStack;
1336 + ownerStack = React.captureOwnerStack
1337 + ? React.captureOwnerStack()
1338 + : null;
1339 + },
1340 + },
1341 + );
1342 + });
1343 +
1344 + setTimeout(() => {
1345 + abortController.abort();
1346 + resolve(result);
1347 + });
1348 + });
1349 +
1350 + const prerenderHTML = await readResult(prelude);
1351 +
1352 + expect(prerenderHTML).toBe('');
1353 +
1354 + if (__DEV__) {
1355 + expect(
1356 + normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1357 + ).toBe(
1358 + '\n' +
1359 + ' in Dynamic' +
1360 + (gate(flags => flags.enableAsyncDebugInfo)
1361 + ? ' (file://ReactFlightDOMNode-test.js:1223:33)\n'
1362 + : '\n') +
1363 + ' in body\n' +
1364 + ' in html\n' +
1365 + ' in App (file://ReactFlightDOMNode-test.js:1240:25)\n' +
1366 + ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)',
1367 + );
1368 + } else {
1369 + expect(
1370 + normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1371 + ).toBe(
1372 + '\n' +
1373 + ' in body\n' +
1374 + ' in html\n' +
1375 + ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)',
1376 + );
1377 + }
1378 +
1379 + if (__DEV__) {
1380 + if (gate(flags => flags.enableAsyncDebugInfo)) {
1381 + expect(
1382 + normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1383 + ).toBe(
1384 + '\n' +
1385 + ' in Dynamic (file://ReactFlightDOMNode-test.js:1223:33)\n' +
1386 + ' in App (file://ReactFlightDOMNode-test.js:1240:25)',
1387 + );
1388 + } else {
1389 + expect(
1390 + normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1391 + ).toBe(
1392 + '' +
1393 + '\n' +
1394 + ' in App (file://ReactFlightDOMNode-test.js:1240:25)',
1395 + );
1396 + }
1397 + } else {
1398 + expect(ownerStack).toBeNull();
1399 + }
1400 + });
1401 + });
1402 });
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
+2
@@ -52,6 +52,7 @@ export type Options = {
52 replayConsoleLogs?: boolean,
53 environmentName?: string,
54 startTime?: number,
55 + endTime?: number,
56 };
57
58 function createDebugCallbackFromWritableStream(
@@ -106,6 +107,7 @@ function createResponseFromOptions(options: void | Options) {
107 __DEV__ && options && options.startTime != null
108 ? options.startTime
109 : undefined,
110 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
111 debugChannel,
112 );
113 }
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js
+2
@@ -80,6 +80,7 @@ export type Options = {
80 replayConsoleLogs?: boolean,
81 environmentName?: string,
82 startTime?: number,
83 + endTime?: number,
84 // For the Edge client we only support a single-direction debug channel.
85 debugChannel?: {readable?: ReadableStream, ...},
86 };
@@ -113,6 +114,7 @@ function createResponseFromOptions(options: Options) {
114 __DEV__ && options && options.startTime != null
115 ? options.startTime
116 : undefined,
117 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
118 debugChannel,
119 );
120 }
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js
+2
@@ -61,6 +61,7 @@ export type Options = {
61 replayConsoleLogs?: boolean,
62 environmentName?: string,
63 startTime?: number,
64 + endTime?: number,
65 // For the Node.js client we only support a single-direction debug channel.
66 debugChannel?: Readable,
67 };
@@ -118,6 +119,7 @@ function createFromNodeStream<T>(
119 __DEV__ && options && options.startTime != null
120 ? options.startTime
121 : undefined,
122 + __DEV__ && options && options.endTime != null ? options.endTime : undefined,
123 debugChannel,
124 );
125
packages/react-server/src/ReactFizzServer.js
+28 -10
@@ -1004,14 +1004,6 @@ function pushHaltedAwaitOnComponentStack(
1004 if (debugInfo != null) {
1005 for (let i = debugInfo.length - 1; i >= 0; i--) {
1006 const info = debugInfo[i];
1007 - if (typeof info.name === 'string') {
1008 - // This is a Server Component. Any awaits in previous Server Components already resolved.
1009 - break;
1010 - }
1011 - if (typeof info.time === 'number') {
1012 - // This had an end time. Any awaits before this must have already resolved.
1013 - break;
1014 - }
1007 if (info.awaited != null) {
1008 const asyncInfo: ReactAsyncInfo = (info: any);
1009 const bestStack =
@@ -4653,10 +4645,36 @@ function abortTask(task: Task, request: Request, error: mixed): void {
4645 // If the task is not rendering, then this is an async abort. Conceptually it's as if
4646 // the abort happened inside the async gap. The abort reason's stack frame won't have that
4647 // on the stack so instead we use the owner stack and debug task of any halted async debug info.
4656 - const node: any = task.node;
4648 + let node: any = task.node;
4649 if (node !== null && typeof node === 'object') {
4650 // Push a fake component stack frame that represents the await.
4659 - pushHaltedAwaitOnComponentStack(task, node._debugInfo);
4651 + let debugInfo = node._debugInfo;
4652 + // First resolve lazy nodes to find debug info that has been transferred
4653 + // to the inner value.
4654 + while (
4655 + typeof node === 'object' &&
4656 + node !== null &&
4657 + node.$$typeof === REACT_LAZY_TYPE
4658 + ) {
4659 + const payload = node._payload;
4660 + if (payload.status === 'fulfilled') {
4661 + node = payload.value;
4662 + continue;
4663 + }
4664 + break;
4665 + }
4666 + if (
4667 + typeof node === 'object' &&
4668 + node !== null &&
4669 + (isArray(node) ||
4670 + typeof node[ASYNC_ITERATOR] === 'function' ||
4671 + node.$$typeof === REACT_ELEMENT_TYPE ||
4672 + node.$$typeof === REACT_LAZY_TYPE) &&
4673 + isArray(node._debugInfo)
4674 + ) {
4675 + debugInfo = node._debugInfo;
4676 + }
4677 + pushHaltedAwaitOnComponentStack(task, debugInfo);
4678 /*
4679 if (task.thenableState !== null) {
4680 // TODO: If we were stalled inside use() of a Client Component then we should