Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 41 additions & 19 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ type InitializationHandler = {
};
let initializingHandler: null | InitializationHandler = null;
let initializingChunk: null | BlockedChunk<any> = null;
let isInitializingDebugInfo: boolean = false;

function initializeDebugChunk(
response: Response,
Expand All @@ -951,6 +952,8 @@ function initializeDebugChunk(
const debugChunk = chunk._debugChunk;
if (debugChunk !== null) {
const debugInfo = chunk._debugInfo;
const prevIsInitializingDebugInfo = isInitializingDebugInfo;
isInitializingDebugInfo = true;
try {
if (debugChunk.status === RESOLVED_MODEL) {
// Find the index of this debug info by walking the linked list.
Expand Down Expand Up @@ -1015,6 +1018,8 @@ function initializeDebugChunk(
}
} catch (error) {
triggerErrorOnChunk(response, chunk, error);
} finally {
isInitializingDebugInfo = prevIsInitializingDebugInfo;
}
}
}
Expand Down Expand Up @@ -1632,7 +1637,9 @@ function fulfillReference(
const element: any = handler.value;
switch (key) {
case '3':
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
if (__DEV__) {
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
}
element.props = mappedValue;
break;
case '4':
Expand All @@ -1648,7 +1655,9 @@ function fulfillReference(
}
break;
default:
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
if (__DEV__) {
transferReferencedDebugInfo(handler.chunk, fulfilledChunk);
}
break;
}
} else if (__DEV__ && !reference.isDebug) {
Expand Down Expand Up @@ -2086,7 +2095,7 @@ function getOutlinedModel<T>(
response,
map,
path.slice(i - 1),
false,
isInitializingDebugInfo,
);
}
case HALTED: {
Expand Down Expand Up @@ -2158,14 +2167,21 @@ function getOutlinedModel<T>(
}

const chunkValue = map(response, value, parentObject, key);
if (
parentObject[0] === REACT_ELEMENT_TYPE &&
(key === '4' || key === '5')
) {
// If we're resolving the "owner" or "stack" slot of an Element array, we don't call
// transferReferencedDebugInfo because this reference is to a debug chunk.
} else {
transferReferencedDebugInfo(initializingChunk, chunk);
if (__DEV__) {
if (
parentObject[0] === REACT_ELEMENT_TYPE &&
(key === '4' || key === '5')
) {
// If we're resolving the "owner" or "stack" slot of an Element array,
// we don't call transferReferencedDebugInfo because this reference is
// to a debug chunk.
} else if (isInitializingDebugInfo) {
// If we're resolving references as part of debug info resolution, we
// don't call transferReferencedDebugInfo because these references are
// to debug chunks.
} else {
transferReferencedDebugInfo(initializingChunk, chunk);
}
}
return chunkValue;
case PENDING:
Expand All @@ -2177,7 +2193,7 @@ function getOutlinedModel<T>(
response,
map,
path,
false,
isInitializingDebugInfo,
);
case HALTED: {
// Add a dependency that will never resolve.
Expand Down Expand Up @@ -4264,15 +4280,21 @@ function resolveIOInfo(
): void {
const chunks = response._chunks;
let chunk = chunks.get(id);
if (!chunk) {
chunk = createResolvedModelChunk(response, model);
chunks.set(id, chunk);
initializeModelChunk(chunk);
} else {
resolveModelChunk(response, chunk, model);
if (chunk.status === RESOLVED_MODEL) {
const prevIsInitializingDebugInfo = isInitializingDebugInfo;
isInitializingDebugInfo = true;
try {
if (!chunk) {
chunk = createResolvedModelChunk(response, model);
chunks.set(id, chunk);
initializeModelChunk(chunk);
} else {
resolveModelChunk(response, chunk, model);
if (chunk.status === RESOLVED_MODEL) {
initializeModelChunk(chunk);
}
}
} finally {
isInitializingDebugInfo = prevIsInitializingDebugInfo;
}
if (chunk.status === INITIALIZED) {
initializeIOInfo(response, chunk.value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3633,4 +3633,40 @@ describe('ReactFlightAsyncDebugInfo', () => {
`);
}
});

it('should not exponentially accumulate debug info on outlined debug chunks', async () => {
// Regression test: Each Level wraps its received `context` prop in a new
// object before passing it down. This creates props deduplication
// references to the parent's outlined chunk alongside the owner reference,
// giving 2 references per level to the direct parent's chunk. Without
// skipping transferReferencedDebugInfo during debug info resolution, this
// test would fail with an infinite loop detection error.
async function Level({depth, context}) {
await delay(0);
if (depth === 0) {
return <div>Hello, World!</div>;
}
const newContext = {prev: context, id: depth};
return ReactServer.createElement(Level, {
depth: depth - 1,
context: newContext,
});
}

const stream = ReactServerDOMServer.renderToPipeableStream(
ReactServer.createElement(Level, {depth: 20, context: {root: true}}),
);

const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);

const resolved = await result;
expect(resolved.type).toBe('div');

await finishLoadingStream(readable);
});
});
Loading