@samitouri / QOS-React / commits / 253abc78a1

[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,