diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 503f79e4cc582..2a5936be0f481 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 + _debugEndTime?: number, // DEV-only _debugIOStarted: boolean, // DEV-only _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only _debugChannel?: void | DebugChannel, // DEV-only @@ -500,6 +501,34 @@ function createErrorChunk( return new ReactPromise(ERRORED, null, error); } +function filterDebugInfo( + response: Response, + value: {_debugInfo: ReactDebugInfo, ...}, +) { + if (response._debugEndTime === null) { + // No end time was defined, so we keep all debug info entries. + return; + } + + // Remove any debug info entries that arrived after the defined end time. + const relativeEndTime = + response._debugEndTime - + // $FlowFixMe[prop-missing] + performance.timeOrigin; + const debugInfo = []; + for (let i = 0; i < value._debugInfo.length; i++) { + const info = value._debugInfo[i]; + if (typeof info.time === 'number' && info.time > relativeEndTime) { + break; + } + if (info.awaited != null && info.awaited.end > relativeEndTime) { + break; + } + debugInfo.push(info); + } + value._debugInfo = debugInfo; +} + function moveDebugInfoFromChunkToInnerValue( chunk: InitializedChunk | InitializedStreamChunk, value: T, @@ -534,7 +563,17 @@ function moveDebugInfoFromChunkToInnerValue( } } +function processChunkDebugInfo( + response: Response, + chunk: InitializedChunk | InitializedStreamChunk, + value: T, +): void { + filterDebugInfo(response, chunk); + moveDebugInfoFromChunkToInnerValue(chunk, value); +} + function wakeChunk( + response: Response, listeners: Array mixed)>, value: T, chunk: InitializedChunk, @@ -544,16 +583,17 @@ function wakeChunk( if (typeof listener === 'function') { listener(value); } else { - fulfillReference(listener, value, chunk); + fulfillReference(response, listener, value, chunk); } } if (__DEV__) { - moveDebugInfoFromChunkToInnerValue(chunk, value); + processChunkDebugInfo(response, chunk, value); } } function rejectChunk( + response: Response, listeners: Array mixed)>, error: mixed, ): void { @@ -562,7 +602,7 @@ function rejectChunk( if (typeof listener === 'function') { listener(error); } else { - rejectReference(listener, error); + rejectReference(response, listener.handler, error); } } } @@ -595,13 +635,14 @@ function resolveBlockedCycle( } function wakeChunkIfInitialized( + response: Response, chunk: SomeChunk, resolveListeners: Array mixed)>, rejectListeners: null | Array mixed)>, ): void { switch (chunk.status) { case INITIALIZED: - wakeChunk(resolveListeners, chunk.value, chunk); + wakeChunk(response, resolveListeners, chunk.value, chunk); break; case BLOCKED: // It is possible that we're blocked on our own chunk if it's a cycle. @@ -615,7 +656,7 @@ function wakeChunkIfInitialized( if (cyclicHandler !== null) { // This reference points back to this chunk. We can resolve the cycle by // using the value from that handler. - fulfillReference(reference, cyclicHandler.value, chunk); + fulfillReference(response, reference, cyclicHandler.value, chunk); resolveListeners.splice(i, 1); i--; if (rejectListeners !== null) { @@ -629,6 +670,7 @@ function wakeChunkIfInitialized( case INITIALIZED: const initializedChunk: InitializedChunk = (chunk: any); wakeChunk( + response, resolveListeners, initializedChunk.value, initializedChunk, @@ -636,7 +678,7 @@ function wakeChunkIfInitialized( return; case ERRORED: if (rejectListeners !== null) { - rejectChunk(rejectListeners, chunk.reason); + rejectChunk(response, rejectListeners, chunk.reason); } return; } @@ -666,7 +708,7 @@ function wakeChunkIfInitialized( break; case ERRORED: if (rejectListeners) { - rejectChunk(rejectListeners, chunk.reason); + rejectChunk(response, rejectListeners, chunk.reason); } break; } @@ -724,7 +766,7 @@ function triggerErrorOnChunk( erroredChunk.status = ERRORED; erroredChunk.reason = error; if (listeners !== null) { - rejectChunk(listeners, error); + rejectChunk(response, listeners, error); } } @@ -832,7 +874,7 @@ function resolveModelChunk( // longer be rendered or might not be the highest pri. initializeModelChunk(resolvedChunk); // The status might have changed after initialization. - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); + wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners); } } @@ -861,12 +903,11 @@ function resolveModuleChunk( } if (resolveListeners !== null) { initializeModuleChunk(resolvedChunk); - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); + wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners); } } type InitializationReference = { - response: Response, // TODO: Remove Response from here and pass it through instead. handler: InitializationHandler, parentObject: Object, key: string, @@ -1005,7 +1046,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { if (typeof listener === 'function') { listener(value); } else { - fulfillReference(listener, value, cyclicChunk); + fulfillReference(response, listener, value, cyclicChunk); } } } @@ -1026,7 +1067,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { initializedChunk.value = value; if (__DEV__) { - moveDebugInfoFromChunkToInnerValue(initializedChunk, value); + processChunkDebugInfo(response, initializedChunk, value); } } catch (error) { const erroredChunk: ErroredChunk = (chunk: any); @@ -1413,11 +1454,12 @@ function getChunk(response: Response, id: number): SomeChunk { } function fulfillReference( + response: Response, reference: InitializationReference, value: any, fulfilledChunk: SomeChunk, ): void { - const {response, handler, parentObject, key, map, path} = reference; + const {handler, parentObject, key, map, path} = reference; for (let i = 1; i < path.length; i++) { while ( @@ -1487,7 +1529,11 @@ function fulfillReference( return; } default: { - rejectReference(reference, referencedChunk.reason); + rejectReference( + response, + reference.handler, + referencedChunk.reason, + ); return; } } @@ -1585,21 +1631,20 @@ function fulfillReference( initializedChunk.value = handler.value; initializedChunk.reason = handler.reason; // Used by streaming chunks if (resolveListeners !== null) { - wakeChunk(resolveListeners, handler.value, initializedChunk); + wakeChunk(response, resolveListeners, handler.value, initializedChunk); } else { if (__DEV__) { - moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value); + processChunkDebugInfo(response, initializedChunk, handler.value); } } } } function rejectReference( - reference: InitializationReference, + response: Response, + handler: InitializationHandler, error: mixed, ): void { - const {handler, response} = reference; - if (handler.errored) { // We've already errored. We could instead build up an AggregateError // but if there are multiple errors we just take the first one like @@ -1690,7 +1735,6 @@ function waitForReference( } const reference: InitializationReference = { - response, handler, parentObject, key, @@ -1838,10 +1882,10 @@ function loadServerReference, T>( initializedChunk.status = INITIALIZED; initializedChunk.value = handler.value; if (resolveListeners !== null) { - wakeChunk(resolveListeners, handler.value, initializedChunk); + wakeChunk(response, resolveListeners, handler.value, initializedChunk); } else { if (__DEV__) { - moveDebugInfoFromChunkToInnerValue(initializedChunk, handler.value); + processChunkDebugInfo(response, initializedChunk, handler.value); } } } @@ -2578,6 +2622,7 @@ function ResponseInstance( replayConsole: boolean, // DEV-only environmentName: void | string, // DEV-only debugStartTime: void | number, // DEV-only + debugEndTime: void | number, // DEV-only debugChannel: void | DebugChannel, // DEV-only ) { const chunks: Map> = new Map(); @@ -2645,6 +2690,7 @@ function ResponseInstance( // and is not considered I/O required to load the stream. setTimeout(markIOStarted.bind(this), 0); } + this._debugEndTime = debugEndTime == null ? null : debugEndTime; this._debugFindSourceMapURL = findSourceMapURL; this._debugChannel = debugChannel; this._blockedConsole = null; @@ -2688,6 +2734,7 @@ export function createResponse( replayConsole: boolean, // DEV-only environmentName: void | string, // DEV-only debugStartTime: void | number, // DEV-only + debugEndTime: void | number, // DEV-only debugChannel: void | DebugChannel, // DEV-only ): WeakResponse { return getWeakResponse( @@ -2704,6 +2751,7 @@ export function createResponse( replayConsole, environmentName, debugStartTime, + debugEndTime, debugChannel, ), ); @@ -3075,10 +3123,10 @@ function resolveStream>( resolvedChunk.value = stream; resolvedChunk.reason = controller; if (resolveListeners !== null) { - wakeChunk(resolveListeners, chunk.value, (chunk: any)); + wakeChunk(response, resolveListeners, chunk.value, (chunk: any)); } else { if (__DEV__) { - moveDebugInfoFromChunkToInnerValue(resolvedChunk, stream); + processChunkDebugInfo(response, resolvedChunk, stream); } } } @@ -3218,7 +3266,12 @@ function startAsyncIterable( initializedChunk.status = INITIALIZED; initializedChunk.value = {done: false, value: value}; if (resolveListeners !== null) { - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); + wakeChunkIfInitialized( + response, + chunk, + resolveListeners, + rejectListeners, + ); } } nextWriteIndex++; diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js index 371f08abc9a62..ee2475287d037 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js @@ -53,6 +53,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, }; function createDebugCallbackFromWritableStream( @@ -107,6 +108,7 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js index 78dce936158eb..38ac43d01315b 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js @@ -58,6 +58,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -116,6 +117,7 @@ function createFromNodeStream( __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js index 0f0141e640988..b304d442046db 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js @@ -132,6 +132,7 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); } @@ -209,6 +210,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, }; export function createFromReadableStream( diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js index 5c8d1023b2373..f58f8534348e7 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js @@ -80,6 +80,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, // For the Edge client we only support a single-direction debug channel. debugChannel?: {readable?: ReadableStream, ...}, }; @@ -111,6 +112,7 @@ function createResponseFromOptions(options?: Options) { __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js index fbc633a175321..a9b82e343fb51 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js @@ -53,6 +53,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -107,6 +108,7 @@ export function createFromNodeStream( __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js index b3d31bd1bbba9..0bf615001982f 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js @@ -52,6 +52,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, }; function createDebugCallbackFromWritableStream( @@ -106,6 +107,7 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js index 5517e0f73cf94..c6dd4ee94ad11 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js @@ -80,6 +80,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, // For the Edge client we only support a single-direction debug channel. debugChannel?: {readable?: ReadableStream, ...}, }; @@ -113,6 +114,7 @@ function createResponseFromOptions(options: Options) { __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js index 6d117929df007..73d79800c9219 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js @@ -61,6 +61,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -118,6 +119,7 @@ function createFromNodeStream( __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index d59f298f99496..646735a54b769 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -86,12 +86,25 @@ describe('ReactFlightDOMNode', () => { ); } - function normalizeCodeLocInfo(str) { + const relativeFilename = path.relative(__dirname, __filename); + + function normalizeCodeLocInfo(str, {preserveLocation = false} = {}) { return ( str && - str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) { - return ' in ' + name + (/\d/.test(m) ? ' (at **)' : ''); - }) + str.replace( + /^ +(?:at|in) ([\S]+) ([^\n]*)/gm, + function (m, name, location) { + return ( + ' in ' + + name + + (/\d/.test(m) + ? preserveLocation + ? ' ' + location.replace(__filename, relativeFilename) + : ' (at **)' + : '') + ); + }, + ) ); } @@ -1169,4 +1182,221 @@ describe('ReactFlightDOMNode', () => { // Must not throw an error. await readable.pipeTo(writable); }); + + describe('with real timers', () => { + // These tests schedule their rendering in a way that requires real timers + // to be used to accurately represent how this interacts with React's + // internal scheduling. + + beforeEach(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + jest.useFakeTimers(); + }); + + it('should use late-arriving I/O debug info to enhance component and owner stacks when aborting a prerender', async () => { + // This test is constructing a scenario where a framework might separate + // I/O into different phases, e.g. runtime I/O and dynamic I/O. The + // framework might choose to define an end time for the Flight client, + // indicating that all I/O info (or any debug info for that matter) that + // arrives after that time should be ignored. When rendering in Fizz is + // then aborted, the late-arriving debug info that's used to enhance the + // owner stack only includes I/O info up to that end time. + let resolveRuntimeData; + let resolveDynamicData; + + async function getRuntimeData() { + return new Promise(resolve => { + resolveRuntimeData = resolve; + }); + } + + async function getDynamicData() { + return new Promise(resolve => { + resolveDynamicData = resolve; + }); + } + + async function Dynamic() { + const runtimeData = await getRuntimeData(); + const dynamicData = await getDynamicData(); + + return ( +

+ {runtimeData} {dynamicData} +

+ ); + } + + function App() { + return ReactServer.createElement( + 'html', + null, + ReactServer.createElement( + 'body', + null, + ReactServer.createElement(Dynamic), + ), + ); + } + + const stream = await ReactServerDOMServer.renderToPipeableStream( + ReactServer.createElement(App), + webpackMap, + {filterStackFrame}, + ); + + const initialChunks = []; + const dynamicChunks = []; + let isDynamic = false; + + const passThrough = new Stream.PassThrough(streamOptions); + stream.pipe(passThrough); + + passThrough.on('data', chunk => { + if (isDynamic) { + dynamicChunks.push(chunk); + } else { + initialChunks.push(chunk); + } + }); + + let endTime; + + await new Promise(resolve => { + setTimeout(() => { + resolveRuntimeData('Hi'); + }); + setTimeout(() => { + isDynamic = true; + endTime = performance.now() + performance.timeOrigin; + resolveDynamicData('Josh'); + resolve(); + }); + }); + + await new Promise(resolve => { + passThrough.on('end', resolve); + }); + + // Create a new Readable and push all initial chunks immediately. + const readable = new Stream.Readable({...streamOptions, read() {}}); + for (let i = 0; i < initialChunks.length; i++) { + readable.push(initialChunks[i]); + } + + const abortController = new AbortController(); + + // When prerendering is aborted, push all dynamic chunks. They won't be + // considered for rendering, but they include debug info we want to use. + abortController.signal.addEventListener( + 'abort', + () => { + for (let i = 0; i < dynamicChunks.length; i++) { + readable.push(dynamicChunks[i]); + } + }, + {once: true}, + ); + + const response = ReactServerDOMClient.createFromNodeStream( + readable, + { + serverConsumerManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + { + // Debug info arriving after this end time will be ignored, e.g. the + // I/O info for the dynamic data. + endTime, + }, + ); + + function ClientRoot() { + return use(response); + } + + let componentStack; + let ownerStack; + + const {prelude} = await new Promise(resolve => { + let result; + + setTimeout(() => { + result = ReactDOMFizzStatic.prerenderToNodeStream( + React.createElement(ClientRoot), + { + signal: abortController.signal, + onError(error, errorInfo) { + componentStack = errorInfo.componentStack; + ownerStack = React.captureOwnerStack + ? React.captureOwnerStack() + : null; + }, + }, + ); + }); + + setTimeout(() => { + abortController.abort(); + resolve(result); + }); + }); + + const prerenderHTML = await readResult(prelude); + + expect(prerenderHTML).toBe(''); + + if (__DEV__) { + expect( + normalizeCodeLocInfo(componentStack, {preserveLocation: true}), + ).toBe( + '\n' + + ' in Dynamic' + + (gate(flags => flags.enableAsyncDebugInfo) + ? ' (file://ReactFlightDOMNode-test.js:1223:33)\n' + : '\n') + + ' in body\n' + + ' in html\n' + + ' in App (file://ReactFlightDOMNode-test.js:1240:25)\n' + + ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)', + ); + } else { + expect( + normalizeCodeLocInfo(componentStack, {preserveLocation: true}), + ).toBe( + '\n' + + ' in body\n' + + ' in html\n' + + ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)', + ); + } + + if (__DEV__) { + if (gate(flags => flags.enableAsyncDebugInfo)) { + expect( + normalizeCodeLocInfo(ownerStack, {preserveLocation: true}), + ).toBe( + '\n' + + ' in Dynamic (file://ReactFlightDOMNode-test.js:1223:33)\n' + + ' in App (file://ReactFlightDOMNode-test.js:1240:25)', + ); + } else { + expect( + normalizeCodeLocInfo(ownerStack, {preserveLocation: true}), + ).toBe( + '' + + '\n' + + ' in App (file://ReactFlightDOMNode-test.js:1240:25)', + ); + } + } else { + expect(ownerStack).toBeNull(); + } + }); + }); }); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js index b3d31bd1bbba9..0bf615001982f 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js @@ -52,6 +52,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, }; function createDebugCallbackFromWritableStream( @@ -106,6 +107,7 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js index bc4caac767fb8..2cf668f679d33 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js @@ -80,6 +80,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, // For the Edge client we only support a single-direction debug channel. debugChannel?: {readable?: ReadableStream, ...}, }; @@ -113,6 +114,7 @@ function createResponseFromOptions(options: Options) { __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); } diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js index 6d117929df007..73d79800c9219 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js @@ -61,6 +61,7 @@ export type Options = { replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, + endTime?: number, // For the Node.js client we only support a single-direction debug channel. debugChannel?: Readable, }; @@ -118,6 +119,7 @@ function createFromNodeStream( __DEV__ && options && options.startTime != null ? options.startTime : undefined, + __DEV__ && options && options.endTime != null ? options.endTime : undefined, debugChannel, ); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 07408d64e8817..5913cf8eb6446 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -1004,14 +1004,6 @@ function pushHaltedAwaitOnComponentStack( if (debugInfo != null) { for (let i = debugInfo.length - 1; i >= 0; i--) { const info = debugInfo[i]; - if (typeof info.name === 'string') { - // This is a Server Component. Any awaits in previous Server Components already resolved. - break; - } - if (typeof info.time === 'number') { - // This had an end time. Any awaits before this must have already resolved. - break; - } if (info.awaited != null) { const asyncInfo: ReactAsyncInfo = (info: any); const bestStack = @@ -4653,10 +4645,36 @@ function abortTask(task: Task, request: Request, error: mixed): void { // If the task is not rendering, then this is an async abort. Conceptually it's as if // the abort happened inside the async gap. The abort reason's stack frame won't have that // on the stack so instead we use the owner stack and debug task of any halted async debug info. - const node: any = task.node; + let node: any = task.node; if (node !== null && typeof node === 'object') { // Push a fake component stack frame that represents the await. - pushHaltedAwaitOnComponentStack(task, node._debugInfo); + let debugInfo = node._debugInfo; + // First resolve lazy nodes to find debug info that has been transferred + // to the inner value. + while ( + typeof node === 'object' && + node !== null && + node.$$typeof === REACT_LAZY_TYPE + ) { + const payload = node._payload; + if (payload.status === 'fulfilled') { + node = payload.value; + continue; + } + break; + } + if ( + typeof node === 'object' && + node !== null && + (isArray(node) || + typeof node[ASYNC_ITERATOR] === 'function' || + node.$$typeof === REACT_ELEMENT_TYPE || + node.$$typeof === REACT_LAZY_TYPE) && + isArray(node._debugInfo) + ) { + debugInfo = node._debugInfo; + } + pushHaltedAwaitOnComponentStack(task, debugInfo); /* if (task.thenableState !== null) { // TODO: If we were stalled inside use() of a Client Component then we should