diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 701d9df33cdd9..1c2151362d89b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -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, // DEV-only @@ -500,7 +501,7 @@ function createErrorChunk( } function moveDebugInfoFromChunkToInnerValue( - chunk: InitializedChunk, + chunk: InitializedChunk | InitializedStreamChunk, value: T, ): void { // Remove the debug info from the initialized chunk, and add it to the inner @@ -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); + } } } } @@ -1818,6 +1823,10 @@ function loadServerReference, T>( initializedChunk.value = handler.value; if (resolveListeners !== null) { wakeChunk(resolveListeners, handler.value, initializedChunk); + } else { + if (__DEV__) { + moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value); + } } } } @@ -2536,6 +2545,10 @@ function missingCall() { ); } +function markIOStarted(this: Response) { + this._debugIOStarted = true; +} + function ResponseInstance( this: $FlowFixMe, bundlerConfig: ServerConsumerModuleMap, @@ -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; @@ -2762,7 +2779,7 @@ function incrementChunkDebugInfo( } } -function addDebugInfo(chunk: SomeChunk, debugInfo: ReactDebugInfo): void { +function addAsyncInfo(chunk: SomeChunk, asyncInfo: ReactAsyncInfo): void { const value = resolveLazy(chunk.value); if ( typeof value === 'object' && @@ -2774,34 +2791,39 @@ function addDebugInfo(chunk: SomeChunk, 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, ): 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); + } } } } @@ -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); } @@ -2869,7 +2891,7 @@ function resolveText( } const newChunk = createInitializedTextChunk(response, text); if (__DEV__) { - resolveChunkDebugInfo(streamState, newChunk); + resolveChunkDebugInfo(response, streamState, newChunk); } chunks.set(id, newChunk); } @@ -2895,7 +2917,7 @@ function resolveBuffer( } const newChunk = createInitializedBufferChunk(response, buffer); if (__DEV__) { - resolveChunkDebugInfo(streamState, newChunk); + resolveChunkDebugInfo(response, streamState, newChunk); } chunks.set(id, newChunk); } @@ -2942,7 +2964,7 @@ function resolveModule( blockedChunk.status = BLOCKED; } if (__DEV__) { - resolveChunkDebugInfo(streamState, blockedChunk); + resolveChunkDebugInfo(response, streamState, blockedChunk); } promise.then( () => resolveModuleChunk(response, blockedChunk, clientReference), @@ -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. @@ -2978,13 +3000,13 @@ function resolveStream>( 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. @@ -3034,6 +3056,10 @@ function resolveStream>( resolvedChunk.reason = controller; if (resolveListeners !== null) { wakeChunk(resolveListeners, chunk.value, (chunk: any)); + } else { + if (__DEV__) { + moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream); + } } } @@ -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); } @@ -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); } @@ -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++) { diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index d5280c091c953..3638852c20b72 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -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
{children}
; + } + + function App({hidden}) { + return ( + <> + + + inside Activity + + + + + inside Suspense + + {hidden ? : null} + + + ); + } + + await actAsync(() => { + render(