[Flight] Transfer Debug Info from a synchronous Reference to another Chunk (#34229)
Sebastian Markbåge committed
Aug 21, 2025 at 17:50 UTC
253abc78a1db0caab1693ecf9407d9bc10dd6bd1
2 files changed
+85
-37
packages/react-client/src/ReactFlightClient.js
+59
-37
@@ -496,13 +496,14 @@ function createErrorChunk<T>(
496
function wakeChunk<T>(
497
listeners: Array<InitializationReference | (T => mixed)>,
498
value: T,
499
+ chunk: SomeChunk<T>,
500
): void {
501
for (let i = 0; i < listeners.length; i++) {
502
const listener = listeners[i];
503
if (typeof listener === 'function') {
504
listener(value);
505
} else {
505
- fulfillReference(listener, value);
506
+ fulfillReference(listener, value, chunk);
507
}
508
}
509
}
@@ -555,7 +556,7 @@ function wakeChunkIfInitialized<T>(
556
): void {
557
switch (chunk.status) {
558
case INITIALIZED:
558
- wakeChunk(resolveListeners, chunk.value);
559
+ wakeChunk(resolveListeners, chunk.value, chunk);
560
break;
561
case BLOCKED:
562
// It is possible that we're blocked on our own chunk if it's a cycle.
@@ -569,7 +570,7 @@ function wakeChunkIfInitialized<T>(
570
if (cyclicHandler !== null) {
571
// This reference points back to this chunk. We can resolve the cycle by
572
// using the value from that handler.
572
- fulfillReference(reference, cyclicHandler.value);
573
+ fulfillReference(reference, cyclicHandler.value, chunk);
574
resolveListeners.splice(i, 1);
575
i--;
576
if (rejectListeners !== null) {
@@ -637,7 +638,7 @@ function triggerErrorOnChunk<T>(
638
cyclicChunk.status = BLOCKED;
639
cyclicChunk.value = null;
640
cyclicChunk.reason = null;
640
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
641
+ if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) {
642
initializingChunk = cyclicChunk;
643
}
644
try {
@@ -919,7 +920,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
920
cyclicChunk.value = null;
921
cyclicChunk.reason = null;
922
922
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
923
+ if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) {
924
initializingChunk = cyclicChunk;
925
}
926
@@ -938,7 +939,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
939
if (resolveListeners !== null) {
940
cyclicChunk.value = null;
941
cyclicChunk.reason = null;
941
- wakeChunk(resolveListeners, value);
942
+ wakeChunk(resolveListeners, value, cyclicChunk);
943
}
944
if (initializingHandler !== null) {
945
if (initializingHandler.errored) {
@@ -961,7 +962,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
962
erroredChunk.reason = error;
963
} finally {
964
initializingHandler = prevHandler;
964
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
965
+ if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) {
966
initializingChunk = prevChunk;
967
}
968
}
@@ -1298,6 +1299,7 @@ function getChunk(response: Response, id: number): SomeChunk<any> {
1299
function fulfillReference(
1300
reference: InitializationReference,
1301
value: any,
1302
+ fulfilledChunk: SomeChunk<any>,
1303
): void {
1304
const {response, handler, parentObject, key, map, path} = reference;
1305
@@ -1376,6 +1378,8 @@ function fulfillReference(
1378
const mappedValue = map(response, value, parentObject, key);
1379
parentObject[key] = mappedValue;
1380
1381
+ transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
1382
+
1383
// If this is the root object for a model reference, where `handler.value`
1384
// is a stale `null`, the resolved value can be used directly.
1385
if (key === '' && handler.value === null) {
@@ -1422,7 +1426,7 @@ function fulfillReference(
1426
initializedChunk.value = handler.value;
1427
initializedChunk.reason = handler.reason; // Used by streaming chunks
1428
if (resolveListeners !== null) {
1425
- wakeChunk(resolveListeners, handler.value);
1429
+ wakeChunk(resolveListeners, handler.value, initializedChunk);
1430
}
1431
}
1432
}
@@ -1669,7 +1673,7 @@ function loadServerReference<A: Iterable<any>, T>(
1673
initializedChunk.status = INITIALIZED;
1674
initializedChunk.value = handler.value;
1675
if (resolveListeners !== null) {
1672
- wakeChunk(resolveListeners, handler.value);
1676
+ wakeChunk(resolveListeners, handler.value, initializedChunk);
1677
}
1678
}
1679
}
@@ -1728,6 +1732,49 @@ function loadServerReference<A: Iterable<any>, T>(
1732
return (null: any);
1733
}
1734
1735
+function transferReferencedDebugInfo(
1736
+ parentChunk: null | SomeChunk<any>,
1737
+ referencedChunk: SomeChunk<any>,
1738
+ referencedValue: mixed,
1739
+): void {
1740
+ if (__DEV__ && referencedChunk._debugInfo) {
1741
+ const referencedDebugInfo = referencedChunk._debugInfo;
1742
+ // If we have a direct reference to an object that was rendered by a synchronous
1743
+ // server component, it might have some debug info about how it was rendered.
1744
+ // We forward this to the underlying object. This might be a React Element or
1745
+ // an Array fragment.
1746
+ // If this was a string / number return value we lose the debug info. We choose
1747
+ // that tradeoff to allow sync server components to return plain values and not
1748
+ // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
1749
+ if (
1750
+ typeof referencedValue === 'object' &&
1751
+ referencedValue !== null &&
1752
+ (isArray(referencedValue) ||
1753
+ typeof referencedValue[ASYNC_ITERATOR] === 'function' ||
1754
+ referencedValue.$$typeof === REACT_ELEMENT_TYPE) &&
1755
+ !referencedValue._debugInfo
1756
+ ) {
1757
+ // We should maybe use a unique symbol for arrays but this is a React owned array.
1758
+ // $FlowFixMe[prop-missing]: This should be added to elements.
1759
+ Object.defineProperty((referencedValue: any), '_debugInfo', {
1760
+ configurable: false,
1761
+ enumerable: false,
1762
+ writable: true,
1763
+ value: referencedDebugInfo,
1764
+ });
1765
+ }
1766
+ // We also add it to the initializing chunk since the resolution of that promise is
1767
+ // also blocked by these. By adding it to both we can track it even if the array/element
1768
+ // is extracted, or if the root is rendered as is.
1769
+ if (parentChunk !== null) {
1770
+ const parentDebugInfo =
1771
+ parentChunk._debugInfo || (parentChunk._debugInfo = []);
1772
+ // $FlowFixMe[method-unbinding]
1773
+ parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo);
1774
+ }
1775
+ }
1776
+}
1777
+
1778
function getOutlinedModel<T>(
1779
response: Response,
1780
reference: string,
@@ -1825,32 +1872,7 @@ function getOutlinedModel<T>(
1872
value = value[path[i]];
1873
}
1874
const chunkValue = map(response, value, parentObject, key);
1828
- if (__DEV__ && chunk._debugInfo) {
1829
- // If we have a direct reference to an object that was rendered by a synchronous
1830
- // server component, it might have some debug info about how it was rendered.
1831
- // We forward this to the underlying object. This might be a React Element or
1832
- // an Array fragment.
1833
- // If this was a string / number return value we lose the debug info. We choose
1834
- // that tradeoff to allow sync server components to return plain values and not
1835
- // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
1836
- if (
1837
- typeof chunkValue === 'object' &&
1838
- chunkValue !== null &&
1839
- (isArray(chunkValue) ||
1840
- typeof chunkValue[ASYNC_ITERATOR] === 'function' ||
1841
- chunkValue.$$typeof === REACT_ELEMENT_TYPE) &&
1842
- !chunkValue._debugInfo
1843
- ) {
1844
- // We should maybe use a unique symbol for arrays but this is a React owned array.
1845
- // $FlowFixMe[prop-missing]: This should be added to elements.
1846
- Object.defineProperty((chunkValue: any), '_debugInfo', {
1847
- configurable: false,
1848
- enumerable: false,
1849
- writable: true,
1850
- value: chunk._debugInfo,
1851
- });
1852
- }
1853
- }
1875
+ transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
1876
return chunkValue;
1877
case PENDING:
1878
case BLOCKED:
@@ -2621,7 +2643,7 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
2643
cyclicChunk.status = BLOCKED;
2644
cyclicChunk.value = null;
2645
cyclicChunk.reason = null;
2624
- if (enableProfilerTimer && enableComponentPerformanceTrack) {
2646
+ if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) {
2647
initializingChunk = cyclicChunk;
2648
}
2649
try {
@@ -2650,7 +2672,7 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
2672
resolvedChunk.value = stream;
2673
resolvedChunk.reason = controller;
2674
if (resolveListeners !== null) {
2653
- wakeChunk(resolveListeners, chunk.value);
2675
+ wakeChunk(resolveListeners, chunk.value, chunk);
2676
}
2677
}
2678
packages/react-client/src/__tests__/ReactFlight-test.js
+26
@@ -2957,6 +2957,19 @@ describe('ReactFlight', () => {
2957
transport: expect.arrayContaining([]),
2958
},
2959
},
2960
+ {
2961
+ time: 16,
2962
+ },
2963
+ {
2964
+ env: 'third-party',
2965
+ key: null,
2966
+ name: 'ThirdPartyAsyncIterableComponent',
2967
+ props: {},
2968
+ stack: ' in Object.<anonymous> (at **)',
2969
+ },
2970
+ {
2971
+ time: 16,
2972
+ },
2973
{time: 17},
2974
]
2975
: undefined,
@@ -2976,6 +2989,19 @@ describe('ReactFlight', () => {
2989
children: {},
2990
},
2991
},
2992
+ {
2993
+ time: 19,
2994
+ },
2995
+ {
2996
+ time: 19,
2997
+ },
2998
+ {
2999
+ env: 'third-party',
3000
+ key: null,
3001
+ name: 'ThirdPartyAsyncIterableComponent',
3002
+ props: {},
3003
+ stack: ' in Object.<anonymous> (at **)',
3004
+ },
3005
{time: 19},
3006
]
3007
: undefined,