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
99 changes: 72 additions & 27 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ type Response = {
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugStartTime: number, // DEV-only
_debugIOStarted: boolean, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
_debugChannel?: void | DebugChannel, // DEV-only
_blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
Expand Down Expand Up @@ -500,7 +501,7 @@ function createErrorChunk<T>(
}

function moveDebugInfoFromChunkToInnerValue<T>(
chunk: InitializedChunk<T>,
chunk: InitializedChunk<T> | InitializedStreamChunk<any>,
value: T,
): void {
// Remove the debug info from the initialized chunk, and add it to the inner
Expand Down Expand Up @@ -1569,6 +1570,10 @@ function fulfillReference(
initializedChunk.reason = handler.reason; // Used by streaming chunks
if (resolveListeners !== null) {
wakeChunk(resolveListeners, handler.value, initializedChunk);
} else {
if (__DEV__) {
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
}
}
}
}
Expand Down Expand Up @@ -1818,6 +1823,10 @@ function loadServerReference<A: Iterable<any>, T>(
initializedChunk.value = handler.value;
if (resolveListeners !== null) {
wakeChunk(resolveListeners, handler.value, initializedChunk);
} else {
if (__DEV__) {
moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value);
}
}
}
}
Expand Down Expand Up @@ -2536,6 +2545,10 @@ function missingCall() {
);
}

function markIOStarted(this: Response) {
this._debugIOStarted = true;
}

function ResponseInstance(
this: $FlowFixMe,
bundlerConfig: ServerConsumerModuleMap,
Expand Down Expand Up @@ -2609,6 +2622,10 @@ function ResponseInstance(
// where as if you use createFromReadableStream from the body of the fetch
// then the start time is when the headers resolved.
this._debugStartTime = performance.now();
this._debugIOStarted = false;
// We consider everything before the first setTimeout task to be cached data
// and is not considered I/O required to load the stream.
setTimeout(markIOStarted.bind(this), 0);
}
this._debugFindSourceMapURL = findSourceMapURL;
this._debugChannel = debugChannel;
Expand Down Expand Up @@ -2762,7 +2779,7 @@ function incrementChunkDebugInfo(
}
}

function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
function addAsyncInfo(chunk: SomeChunk<any>, asyncInfo: ReactAsyncInfo): void {
const value = resolveLazy(chunk.value);
if (
typeof value === 'object' &&
Expand All @@ -2774,34 +2791,39 @@ function addDebugInfo(chunk: SomeChunk<any>, debugInfo: ReactDebugInfo): void {
) {
if (isArray(value._debugInfo)) {
// $FlowFixMe[method-unbinding]
value._debugInfo.push.apply(value._debugInfo, debugInfo);
value._debugInfo.push(asyncInfo);
} else {
Object.defineProperty((value: any), '_debugInfo', {
configurable: false,
enumerable: false,
writable: true,
value: debugInfo,
value: [asyncInfo],
});
}
} else {
// $FlowFixMe[method-unbinding]
chunk._debugInfo.push.apply(chunk._debugInfo, debugInfo);
chunk._debugInfo.push(asyncInfo);
}
}

function resolveChunkDebugInfo(
response: Response,
streamState: StreamState,
chunk: SomeChunk<any>,
): void {
if (__DEV__ && enableAsyncDebugInfo) {
// Add the currently resolving chunk's debug info representing the stream
// to the Promise that was waiting on the stream, or its underlying value.
const debugInfo: ReactDebugInfo = [{awaited: streamState._debugInfo}];
if (chunk.status === PENDING || chunk.status === BLOCKED) {
const boundAddDebugInfo = addDebugInfo.bind(null, chunk, debugInfo);
chunk.then(boundAddDebugInfo, boundAddDebugInfo);
} else {
addDebugInfo(chunk, debugInfo);
// Only include stream information after a macrotask. Any chunk processed
// before that is considered cached data.
if (response._debugIOStarted) {
// Add the currently resolving chunk's debug info representing the stream
// to the Promise that was waiting on the stream, or its underlying value.
const asyncInfo: ReactAsyncInfo = {awaited: streamState._debugInfo};
if (chunk.status === PENDING || chunk.status === BLOCKED) {
const boundAddAsyncInfo = addAsyncInfo.bind(null, chunk, asyncInfo);
chunk.then(boundAddAsyncInfo, boundAddAsyncInfo);
} else {
addAsyncInfo(chunk, asyncInfo);
}
}
}
}
Expand Down Expand Up @@ -2837,12 +2859,12 @@ function resolveModel(
model,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
resolveModelChunk(response, chunk, model);
}
Expand All @@ -2869,7 +2891,7 @@ function resolveText(
}
const newChunk = createInitializedTextChunk(response, text);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
}
Expand All @@ -2895,7 +2917,7 @@ function resolveBuffer(
}
const newChunk = createInitializedBufferChunk(response, buffer);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
}
Expand Down Expand Up @@ -2942,7 +2964,7 @@ function resolveModule(
blockedChunk.status = BLOCKED;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, blockedChunk);
resolveChunkDebugInfo(response, streamState, blockedChunk);
}
promise.then(
() => resolveModuleChunk(response, blockedChunk, clientReference),
Expand All @@ -2952,12 +2974,12 @@ function resolveModule(
if (!chunk) {
const newChunk = createResolvedModuleChunk(response, clientReference);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
// This can't actually happen because we don't have any forward
// references to modules.
Expand All @@ -2978,13 +3000,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
if (!chunk) {
const newChunk = createInitializedStreamChunk(response, stream, controller);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
return;
}
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
if (chunk.status !== PENDING) {
// We already resolved. We didn't expect to see this.
Expand Down Expand Up @@ -3034,6 +3056,10 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
resolvedChunk.reason = controller;
if (resolveListeners !== null) {
wakeChunk(resolveListeners, chunk.value, (chunk: any));
} else {
if (__DEV__) {
moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream);
}
}
}

Expand Down Expand Up @@ -3433,12 +3459,12 @@ function resolvePostponeDev(
postponeInstance,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
triggerErrorOnChunk(response, chunk, postponeInstance);
}
Expand Down Expand Up @@ -3467,12 +3493,12 @@ function resolveErrorModel(
errorWithDigest,
);
if (__DEV__) {
resolveChunkDebugInfo(streamState, newChunk);
resolveChunkDebugInfo(response, streamState, newChunk);
}
chunks.set(id, newChunk);
} else {
if (__DEV__) {
resolveChunkDebugInfo(streamState, chunk);
resolveChunkDebugInfo(response, streamState, chunk);
}
triggerErrorOnChunk(response, chunk, errorWithDigest);
}
Expand Down Expand Up @@ -4318,7 +4344,26 @@ function flushComponentPerformance(

// First find the start time of the first component to know if it was running
// in parallel with the previous.
const debugInfo = __DEV__ && root._debugInfo;
let debugInfo = null;
if (__DEV__) {
debugInfo = root._debugInfo;
if (debugInfo.length === 0 && root.status === 'fulfilled') {
const resolvedValue = resolveLazy(root.value);
if (
typeof resolvedValue === 'object' &&
resolvedValue !== null &&
(isArray(resolvedValue) ||
typeof resolvedValue[ASYNC_ITERATOR] === 'function' ||
resolvedValue.$$typeof === REACT_ELEMENT_TYPE ||
resolvedValue.$$typeof === REACT_LAZY_TYPE) &&
isArray(resolvedValue._debugInfo)
) {
// It's possible that the value has been given the debug info.
// In that case we need to look for it on the resolved value.
debugInfo = resolvedValue._debugInfo;
}
}
}
if (debugInfo) {
let startTime = 0;
for (let i = 0; i < debugInfo.length; i++) {
Expand Down
101 changes: 101 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3142,4 +3142,105 @@ describe('Store', () => {
await actAsync(() => render(null));
expect(store).toMatchInlineSnapshot(``);
});

// @reactVersion >= 19
it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => {
const Activity = React.Activity || React.unstable_Activity;

const never = new Promise(() => {});
function Never() {
readValue(never);
return null;
}
function Component({children}) {
return <div>{children}</div>;
}

function App({hidden}) {
return (
<>
<Activity mode={hidden ? 'hidden' : 'visible'}>
<React.Suspense name="inside-activity">
<Component key="inside-activity">inside Activity</Component>
</React.Suspense>
</Activity>
<React.Suspense name="outer-suspense">
<React.Suspense name="inner-suspense">
<Component key="inside-suspense">inside Suspense</Component>
</React.Suspense>
{hidden ? <Never /> : null}
</React.Suspense>
</>
);
}

await actAsync(() => {
render(<App hidden={true} />);
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Activity>
<Suspense name="outer-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}]}
<Suspense name="outer-suspense" rects={null}>
`);

// mount as visible
await actAsync(() => {
render(null);
});
await actAsync(() => {
render(<App hidden={false} />);
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Activity>
▾ <Suspense name="inside-activity">
<Component key="inside-activity">
▾ <Suspense name="outer-suspense">
▾ <Suspense name="inner-suspense">
<Component key="inside-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
`);

await actAsync(() => {
render(<App hidden={true} />);
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Activity>
<Suspense name="outer-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
`);

await actAsync(() => {
render(<App hidden={false} />);
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Activity>
▾ <Suspense name="inside-activity">
<Component key="inside-activity">
▾ <Suspense name="outer-suspense">
▾ <Suspense name="inner-suspense">
<Component key="inside-suspense">
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
`);
});
});
Loading
Loading