Skip to content

Commit ac2c1a5

Browse files
[Flight] Ensure blocked debug info is handled properly (#34524)
This PR ensures that server components are reliably included in the DevTools component tree, even if debug info is received delayed, e.g. when using a debug channel. The fix consists of three parts: - We must not unset the debug chunk before all debug info entries are resolved. - We must ensure that the "RSC Stream" IO debug info entry is pushed last, after all other entries were resolved. - We need to transfer the debug info from blocked element chunks onto the lazy node and the element. Ideally, we wouldn't even create a lazy node for blocked elements that are at the root of the JSON payload, because that would basically wrap a lazy in a lazy. This optimization that ensures that everything around the blocked element can proceed is only needed for nested elements. However, we also need it for resolving deduped references in blocked root elements, unless we adapt that logic, which would be a bigger lift. When reloading the Flight fixture, the component tree is now displayed deterministically. Previously, it would sometimes omit synchronous server components. <img width="306" height="565" alt="complete" src="https://github.com/user-attachments/assets/db61aa10-1816-43e6-9903-0e585190cdf1" /> --------- Co-authored-by: Sebastian Markbage <[email protected]>
1 parent c44fbf4 commit ac2c1a5

File tree

5 files changed

+341
-113
lines changed

5 files changed

+341
-113
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 149 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -499,10 +499,44 @@ function createErrorChunk<T>(
499499
return new ReactPromise(ERRORED, null, error);
500500
}
501501

502+
function moveDebugInfoFromChunkToInnerValue<T>(
503+
chunk: InitializedChunk<T>,
504+
value: T,
505+
): void {
506+
// Remove the debug info from the initialized chunk, and add it to the inner
507+
// value instead. This can be a React element, an array, or an uninitialized
508+
// Lazy.
509+
const resolvedValue = resolveLazy(value);
510+
if (
511+
typeof resolvedValue === 'object' &&
512+
resolvedValue !== null &&
513+
(isArray(resolvedValue) ||
514+
typeof resolvedValue[ASYNC_ITERATOR] === 'function' ||
515+
resolvedValue.$$typeof === REACT_ELEMENT_TYPE ||
516+
resolvedValue.$$typeof === REACT_LAZY_TYPE)
517+
) {
518+
const debugInfo = chunk._debugInfo.splice(0);
519+
if (isArray(resolvedValue._debugInfo)) {
520+
// $FlowFixMe[method-unbinding]
521+
resolvedValue._debugInfo.unshift.apply(
522+
resolvedValue._debugInfo,
523+
debugInfo,
524+
);
525+
} else {
526+
Object.defineProperty((resolvedValue: any), '_debugInfo', {
527+
configurable: false,
528+
enumerable: false,
529+
writable: true,
530+
value: debugInfo,
531+
});
532+
}
533+
}
534+
}
535+
502536
function wakeChunk<T>(
503537
listeners: Array<InitializationReference | (T => mixed)>,
504538
value: T,
505-
chunk: SomeChunk<T>,
539+
chunk: InitializedChunk<T>,
506540
): void {
507541
for (let i = 0; i < listeners.length; i++) {
508542
const listener = listeners[i];
@@ -512,6 +546,10 @@ function wakeChunk<T>(
512546
fulfillReference(listener, value, chunk);
513547
}
514548
}
549+
550+
if (__DEV__) {
551+
moveDebugInfoFromChunkToInnerValue(chunk, value);
552+
}
515553
}
516554

517555
function rejectChunk(
@@ -649,7 +687,6 @@ function triggerErrorOnChunk<T>(
649687
}
650688
try {
651689
initializeDebugChunk(response, chunk);
652-
chunk._debugChunk = null;
653690
if (initializingHandler !== null) {
654691
if (initializingHandler.errored) {
655692
// Ignore error parsing debug info, we'll report the original error instead.
@@ -932,9 +969,9 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
932969
}
933970

934971
if (__DEV__) {
935-
// Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
972+
// Initialize any debug info and block the initializing chunk on any
973+
// unresolved entries.
936974
initializeDebugChunk(response, chunk);
937-
chunk._debugChunk = null;
938975
}
939976

940977
try {
@@ -946,7 +983,14 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
946983
if (resolveListeners !== null) {
947984
cyclicChunk.value = null;
948985
cyclicChunk.reason = null;
949-
wakeChunk(resolveListeners, value, cyclicChunk);
986+
for (let i = 0; i < resolveListeners.length; i++) {
987+
const listener = resolveListeners[i];
988+
if (typeof listener === 'function') {
989+
listener(value);
990+
} else {
991+
fulfillReference(listener, value, cyclicChunk);
992+
}
993+
}
950994
}
951995
if (initializingHandler !== null) {
952996
if (initializingHandler.errored) {
@@ -963,6 +1007,10 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
9631007
const initializedChunk: InitializedChunk<T> = (chunk: any);
9641008
initializedChunk.status = INITIALIZED;
9651009
initializedChunk.value = value;
1010+
1011+
if (__DEV__) {
1012+
moveDebugInfoFromChunkToInnerValue(initializedChunk, value);
1013+
}
9661014
} catch (error) {
9671015
const erroredChunk: ErroredChunk<T> = (chunk: any);
9681016
erroredChunk.status = ERRORED;
@@ -1079,7 +1127,7 @@ function getTaskName(type: mixed): string {
10791127
function initializeElement(
10801128
response: Response,
10811129
element: any,
1082-
lazyType: null | LazyComponent<
1130+
lazyNode: null | LazyComponent<
10831131
React$Element<any>,
10841132
SomeChunk<React$Element<any>>,
10851133
>,
@@ -1151,15 +1199,33 @@ function initializeElement(
11511199
initializeFakeStack(response, owner);
11521200
}
11531201

1154-
// In case the JSX runtime has validated the lazy type as a static child, we
1155-
// need to transfer this information to the element.
1156-
if (
1157-
lazyType &&
1158-
lazyType._store &&
1159-
lazyType._store.validated &&
1160-
!element._store.validated
1161-
) {
1162-
element._store.validated = lazyType._store.validated;
1202+
if (lazyNode !== null) {
1203+
// In case the JSX runtime has validated the lazy type as a static child, we
1204+
// need to transfer this information to the element.
1205+
if (
1206+
lazyNode._store &&
1207+
lazyNode._store.validated &&
1208+
!element._store.validated
1209+
) {
1210+
element._store.validated = lazyNode._store.validated;
1211+
}
1212+
1213+
// If the lazy node is initialized, we move its debug info to the inner
1214+
// value.
1215+
if (lazyNode._payload.status === INITIALIZED && lazyNode._debugInfo) {
1216+
const debugInfo = lazyNode._debugInfo.splice(0);
1217+
if (element._debugInfo) {
1218+
// $FlowFixMe[method-unbinding]
1219+
element._debugInfo.unshift.apply(element._debugInfo, debugInfo);
1220+
} else {
1221+
Object.defineProperty(element, '_debugInfo', {
1222+
configurable: false,
1223+
enumerable: false,
1224+
writable: true,
1225+
value: debugInfo,
1226+
});
1227+
}
1228+
}
11631229
}
11641230

11651231
// TODO: We should be freezing the element but currently, we might write into
@@ -1279,13 +1345,13 @@ function createElement(
12791345
createBlockedChunk(response);
12801346
handler.value = element;
12811347
handler.chunk = blockedChunk;
1282-
const lazyType = createLazyChunkWrapper(blockedChunk, validated);
1348+
const lazyNode = createLazyChunkWrapper(blockedChunk, validated);
12831349
if (__DEV__) {
12841350
// After we have initialized any blocked references, initialize stack etc.
1285-
const init = initializeElement.bind(null, response, element, lazyType);
1351+
const init = initializeElement.bind(null, response, element, lazyNode);
12861352
blockedChunk.then(init, init);
12871353
}
1288-
return lazyType;
1354+
return lazyNode;
12891355
}
12901356
}
12911357
if (__DEV__) {
@@ -1466,7 +1532,7 @@ function fulfillReference(
14661532
const element: any = handler.value;
14671533
switch (key) {
14681534
case '3':
1469-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
1535+
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
14701536
element.props = mappedValue;
14711537
break;
14721538
case '4':
@@ -1482,11 +1548,11 @@ function fulfillReference(
14821548
}
14831549
break;
14841550
default:
1485-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
1551+
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
14861552
break;
14871553
}
14881554
} else if (__DEV__ && !reference.isDebug) {
1489-
transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue);
1555+
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
14901556
}
14911557

14921558
handler.deps--;
@@ -1808,47 +1874,34 @@ function loadServerReference<A: Iterable<any>, T>(
18081874
return (null: any);
18091875
}
18101876

1877+
function resolveLazy(value: any): mixed {
1878+
while (
1879+
typeof value === 'object' &&
1880+
value !== null &&
1881+
value.$$typeof === REACT_LAZY_TYPE
1882+
) {
1883+
const payload: SomeChunk<any> = value._payload;
1884+
if (payload.status === INITIALIZED) {
1885+
value = payload.value;
1886+
continue;
1887+
}
1888+
break;
1889+
}
1890+
1891+
return value;
1892+
}
1893+
18111894
function transferReferencedDebugInfo(
18121895
parentChunk: null | SomeChunk<any>,
18131896
referencedChunk: SomeChunk<any>,
1814-
referencedValue: mixed,
18151897
): void {
18161898
if (__DEV__) {
1817-
const referencedDebugInfo = referencedChunk._debugInfo;
1818-
// If we have a direct reference to an object that was rendered by a synchronous
1819-
// server component, it might have some debug info about how it was rendered.
1820-
// We forward this to the underlying object. This might be a React Element or
1821-
// an Array fragment.
1822-
// If this was a string / number return value we lose the debug info. We choose
1823-
// that tradeoff to allow sync server components to return plain values and not
1824-
// use them as React Nodes necessarily. We could otherwise wrap them in a Lazy.
1825-
if (
1826-
typeof referencedValue === 'object' &&
1827-
referencedValue !== null &&
1828-
(isArray(referencedValue) ||
1829-
typeof referencedValue[ASYNC_ITERATOR] === 'function' ||
1830-
referencedValue.$$typeof === REACT_ELEMENT_TYPE)
1831-
) {
1832-
// We should maybe use a unique symbol for arrays but this is a React owned array.
1833-
// $FlowFixMe[prop-missing]: This should be added to elements.
1834-
const existingDebugInfo: ?ReactDebugInfo =
1835-
(referencedValue._debugInfo: any);
1836-
if (existingDebugInfo == null) {
1837-
Object.defineProperty((referencedValue: any), '_debugInfo', {
1838-
configurable: false,
1839-
enumerable: false,
1840-
writable: true,
1841-
value: referencedDebugInfo.slice(0), // Clone so that pushing later isn't going into the original
1842-
});
1843-
} else {
1844-
// $FlowFixMe[method-unbinding]
1845-
existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo);
1846-
}
1847-
}
1848-
// We also add the debug info to the initializing chunk since the resolution of that promise is
1849-
// also blocked by the referenced debug info. By adding it to both we can track it even if the array/element
1850-
// is extracted, or if the root is rendered as is.
1899+
// We add the debug info to the initializing chunk since the resolution of
1900+
// that promise is also blocked by the referenced debug info. By adding it
1901+
// to both we can track it even if the array/element/lazy is extracted, or
1902+
// if the root is rendered as is.
18511903
if (parentChunk !== null) {
1904+
const referencedDebugInfo = referencedChunk._debugInfo;
18521905
const parentDebugInfo = parentChunk._debugInfo;
18531906
for (let i = 0; i < referencedDebugInfo.length; ++i) {
18541907
const debugInfoEntry = referencedDebugInfo[i];
@@ -1999,7 +2052,7 @@ function getOutlinedModel<T>(
19992052
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
20002053
// transferReferencedDebugInfo because this reference is to a debug chunk.
20012054
} else {
2002-
transferReferencedDebugInfo(initializingChunk, chunk, chunkValue);
2055+
transferReferencedDebugInfo(initializingChunk, chunk);
20032056
}
20042057
return chunkValue;
20052058
case PENDING:
@@ -2709,14 +2762,47 @@ function incrementChunkDebugInfo(
27092762
}
27102763
}
27112764

2765+
function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
2766+
const value = resolveLazy(chunk.value);
2767+
if (
2768+
typeof value === 'object' &&
2769+
value !== null &&
2770+
(isArray(value) ||
2771+
typeof value[ASYNC_ITERATOR] === 'function' ||
2772+
value.$$typeof === REACT_ELEMENT_TYPE ||
2773+
value.$$typeof === REACT_LAZY_TYPE)
2774+
) {
2775+
if (isArray(value._debugInfo)) {
2776+
// $FlowFixMe[method-unbinding]
2777+
value._debugInfo.push.apply(value._debugInfo, debugInfo);
2778+
} else {
2779+
Object.defineProperty((value: any), '_debugInfo', {
2780+
configurable: false,
2781+
enumerable: false,
2782+
writable: true,
2783+
value: debugInfo,
2784+
});
2785+
}
2786+
} else {
2787+
// $FlowFixMe[method-unbinding]
2788+
chunk._debugInfo.push.apply(chunk._debugInfo, debugInfo);
2789+
}
2790+
}
2791+
27122792
function resolveChunkDebugInfo(
27132793
streamState: StreamState,
27142794
chunk: SomeChunk<any>,
27152795
): void {
27162796
if (__DEV__ && enableAsyncDebugInfo) {
2717-
// Push the currently resolving chunk's debug info representing the stream on the Promise
2718-
// that was waiting on the stream.
2719-
chunk._debugInfo.push({awaited: streamState._debugInfo});
2797+
// Add the currently resolving chunk's debug info representing the stream
2798+
// to the Promise that was waiting on the stream, or its underlying value.
2799+
const debugInfo: ReactDebugInfo = [{awaited: streamState._debugInfo}];
2800+
if (chunk.status === PENDING || chunk.status === BLOCKED) {
2801+
const boundAddDebugInfo = addDebugInfo.bind(null, chunk, debugInfo);
2802+
chunk.then(boundAddDebugInfo, boundAddDebugInfo);
2803+
} else {
2804+
addDebugInfo(chunk, debugInfo);
2805+
}
27202806
}
27212807
}
27222808

@@ -2909,7 +2995,8 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
29092995
const resolveListeners = chunk.value;
29102996

29112997
if (__DEV__) {
2912-
// Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
2998+
// Initialize any debug info and block the initializing chunk on any
2999+
// unresolved entries.
29133000
if (chunk._debugChunk != null) {
29143001
const prevHandler = initializingHandler;
29153002
const prevChunk = initializingChunk;
@@ -2923,7 +3010,6 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
29233010
}
29243011
try {
29253012
initializeDebugChunk(response, chunk);
2926-
chunk._debugChunk = null;
29273013
if (initializingHandler !== null) {
29283014
if (initializingHandler.errored) {
29293015
// Ignore error parsing debug info, we'll report the original error instead.
@@ -2947,7 +3033,7 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
29473033
resolvedChunk.value = stream;
29483034
resolvedChunk.reason = controller;
29493035
if (resolveListeners !== null) {
2950-
wakeChunk(resolveListeners, chunk.value, chunk);
3036+
wakeChunk(resolveListeners, chunk.value, (chunk: any));
29513037
}
29523038
}
29533039

0 commit comments

Comments
 (0)