[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