@samitouri / QOS-React / commits / 28d4bc496b

[Flight] Make debug info and console log resolve in predictable order (#33665)

This resolves an outstanding issue where it was possible for debug info and console logs to become out of order if they up blocked. E.g. by a future reference or a client reference that hasn't loaded yet. Such as if you console.log a client reference followed by one that doesn't. This encodes the order similar to how the stream chunks work. This also blocks the main chunk from resolving until the last debug info has fully loaded, including future references and client references. This also ensures that we could send some of that data in a different stream, since then it can come out of order.

Sebastian Markbåge committed Jul 19, 2025 at 20:13 UTC 28d4bc496b9c0dd2178caf894054ffce600311d3
4 files changed +375 -120
packages/react-client/src/ReactFlightClient.js
+328 -104
@@ -10,11 +10,10 @@
10 import type {
11 Thenable,
12 ReactDebugInfo,
13 + ReactDebugInfoEntry,
14 ReactComponentInfo,
14 - ReactEnvironmentInfo,
15 ReactAsyncInfo,
16 ReactIOInfo,
17 - ReactTimeInfo,
17 ReactStackTrace,
18 ReactFunctionLocation,
19 ReactErrorInfoDev,
@@ -168,7 +167,8 @@ type PendingChunk<T> = {
167 value: null | Array<InitializationReference | (T => mixed)>,
168 reason: null | Array<InitializationReference | (mixed => mixed)>,
169 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
171 - _debugInfo?: null | ReactDebugInfo, // DEV-only
170 + _debugChunk: null | SomeChunk<ReactDebugInfoEntry>, // DEV-only
171 + _debugInfo: null | ReactDebugInfo, // DEV-only
172 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
173 };
174 type BlockedChunk<T> = {
@@ -176,7 +176,8 @@ type BlockedChunk<T> = {
176 value: null | Array<InitializationReference | (T => mixed)>,
177 reason: null | Array<InitializationReference | (mixed => mixed)>,
178 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
179 - _debugInfo?: null | ReactDebugInfo, // DEV-only
179 + _debugChunk: null, // DEV-only
180 + _debugInfo: null | ReactDebugInfo, // DEV-only
181 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
182 };
183 type ResolvedModelChunk<T> = {
@@ -184,7 +185,8 @@ type ResolvedModelChunk<T> = {
185 value: UninitializedModel,
186 reason: Response,
187 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
187 - _debugInfo?: null | ReactDebugInfo, // DEV-only
188 + _debugChunk: null | SomeChunk<ReactDebugInfoEntry>, // DEV-only
189 + _debugInfo: null | ReactDebugInfo, // DEV-only
190 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
191 };
192 type ResolvedModuleChunk<T> = {
@@ -192,7 +194,8 @@ type ResolvedModuleChunk<T> = {
194 value: ClientReference<T>,
195 reason: null,
196 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
195 - _debugInfo?: null | ReactDebugInfo, // DEV-only
197 + _debugChunk: null, // DEV-only
198 + _debugInfo: null | ReactDebugInfo, // DEV-only
199 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
200 };
201 type InitializedChunk<T> = {
@@ -200,7 +203,8 @@ type InitializedChunk<T> = {
203 value: T,
204 reason: null | FlightStreamController,
205 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
203 - _debugInfo?: null | ReactDebugInfo, // DEV-only
206 + _debugChunk: null, // DEV-only
207 + _debugInfo: null | ReactDebugInfo, // DEV-only
208 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
209 };
210 type InitializedStreamChunk<
@@ -210,7 +214,8 @@ type InitializedStreamChunk<
214 value: T,
215 reason: FlightStreamController,
216 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
213 - _debugInfo?: null | ReactDebugInfo, // DEV-only
217 + _debugChunk: null, // DEV-only
218 + _debugInfo: null | ReactDebugInfo, // DEV-only
219 then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
220 };
221 type ErroredChunk<T> = {
@@ -218,7 +223,8 @@ type ErroredChunk<T> = {
223 value: null,
224 reason: mixed,
225 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
221 - _debugInfo?: null | ReactDebugInfo, // DEV-only
226 + _debugChunk: null, // DEV-only
227 + _debugInfo: null | ReactDebugInfo, // DEV-only
228 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
229 };
230 type HaltedChunk<T> = {
@@ -226,7 +232,8 @@ type HaltedChunk<T> = {
232 value: null,
233 reason: null,
234 _children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
229 - _debugInfo?: null | ReactDebugInfo, // DEV-only
235 + _debugChunk: null, // DEV-only
236 + _debugInfo: null | ReactDebugInfo, // DEV-only
237 then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
238 };
239 type SomeChunk<T> =
@@ -247,6 +254,7 @@ function ReactPromise(status: any, value: any, reason: any) {
254 this._children = [];
255 }
256 if (__DEV__) {
257 + this._debugChunk = null;
258 this._debugInfo = null;
259 }
260 }
@@ -354,6 +362,7 @@ type Response = {
362 _debugRootTask?: null | ConsoleTask, // DEV-only
363 _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
364 _debugChannel?: void | DebugChannelCallback, // DEV-only
365 + _blockedConsole?: null | SomeChunk<ConsoleEntry>, // DEV-only
366 _replayConsole: boolean, // DEV-only
367 _rootEnvironmentName: string, // DEV-only, the requested environment name.
368 };
@@ -616,6 +625,39 @@ function triggerErrorOnChunk<T>(
625 }
626 releasePendingChunk(response, chunk);
627 const listeners = chunk.reason;
628 +
629 + if (__DEV__ && chunk.status === PENDING) {
630 + // Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
631 + if (chunk._debugChunk != null) {
632 + const prevHandler = initializingHandler;
633 + const prevChunk = initializingChunk;
634 + initializingHandler = null;
635 + const cyclicChunk: BlockedChunk<T> = (chunk: any);
636 + cyclicChunk.status = BLOCKED;
637 + cyclicChunk.value = null;
638 + cyclicChunk.reason = null;
639 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
640 + initializingChunk = cyclicChunk;
641 + }
642 + try {
643 + initializeDebugChunk(response, chunk);
644 + chunk._debugChunk = null;
645 + if (initializingHandler !== null) {
646 + if (initializingHandler.errored) {
647 + // Ignore error parsing debug info, we'll report the original error instead.
648 + } else if (initializingHandler.deps > 0) {
649 + // TODO: Block the resolution of the error until all the debug info has loaded.
650 + // We currently don't have a way to throw an error after all dependencies have
651 + // loaded because we currently treat errors as immediately cancelling the handler.
652 + }
653 + }
654 + } finally {
655 + initializingHandler = prevHandler;
656 + initializingChunk = prevChunk;
657 + }
658 + }
659 + }
660 +
661 const erroredChunk: ErroredChunk<T> = (chunk: any);
662 erroredChunk.status = ERRORED;
663 erroredChunk.reason = error;
@@ -747,6 +789,10 @@ function resolveModuleChunk<T>(
789 const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
790 resolvedChunk.status = RESOLVED_MODULE;
791 resolvedChunk.value = value;
792 + if (__DEV__) {
793 + // We don't expect to have any debug info for this row.
794 + resolvedChunk._debugInfo = null;
795 + }
796 if (resolveListeners !== null) {
797 initializeModuleChunk(resolvedChunk);
798 wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
@@ -770,12 +816,86 @@ type InitializationHandler = {
816 parent: null | InitializationHandler,
817 chunk: null | BlockedChunk<any>,
818 value: any,
819 + reason: any,
820 deps: number,
821 errored: boolean,
822 };
823 let initializingHandler: null | InitializationHandler = null;
824 let initializingChunk: null | BlockedChunk<any> = null;
825
826 +function initializeDebugChunk(
827 + response: Response,
828 + chunk: ResolvedModelChunk<any> | PendingChunk<any>,
829 +): void {
830 + const debugChunk = chunk._debugChunk;
831 + if (debugChunk !== null) {
832 + const debugInfo = chunk._debugInfo || (chunk._debugInfo = []);
833 + try {
834 + if (debugChunk.status === RESOLVED_MODEL) {
835 + // Find the index of this debug info by walking the linked list.
836 + let idx = debugInfo.length;
837 + let c = debugChunk._debugChunk;
838 + while (c !== null) {
839 + if (c.status !== INITIALIZED) {
840 + idx++;
841 + }
842 + c = c._debugChunk;
843 + }
844 + // Initializing the model for the first time.
845 + initializeModelChunk(debugChunk);
846 + const initializedChunk = ((debugChunk: any): SomeChunk<any>);
847 + switch (initializedChunk.status) {
848 + case INITIALIZED: {
849 + debugInfo[idx] = initializeDebugInfo(
850 + response,
851 + initializedChunk.value,
852 + );
853 + break;
854 + }
855 + case BLOCKED:
856 + case PENDING: {
857 + waitForReference(
858 + initializedChunk,
859 + debugInfo,
860 + '' + idx,
861 + response,
862 + initializeDebugInfo,
863 + [''], // path
864 + );
865 + break;
866 + }
867 + default:
868 + throw initializedChunk.reason;
869 + }
870 + } else {
871 + switch (debugChunk.status) {
872 + case INITIALIZED: {
873 + // Already done.
874 + break;
875 + }
876 + case BLOCKED:
877 + case PENDING: {
878 + // Signal to the caller that we need to wait.
879 + waitForReference(
880 + debugChunk,
881 + {}, // noop, since we'll have already added an entry to debug info
882 + '', // noop
883 + response,
884 + initializeDebugInfo,
885 + [''], // path
886 + );
887 + break;
888 + }
889 + default:
890 + throw debugChunk.reason;
891 + }
892 + }
893 + } catch (error) {
894 + triggerErrorOnChunk(response, chunk, error);
895 + }
896 + }
897 +}
898 +
899 function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
900 const prevHandler = initializingHandler;
901 const prevChunk = initializingChunk;
@@ -796,6 +916,12 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
916 initializingChunk = cyclicChunk;
917 }
918
919 + if (__DEV__) {
920 + // Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
921 + initializeDebugChunk(response, chunk);
922 + chunk._debugChunk = null;
923 + }
924 +
925 try {
926 const value: T = parseModel(response, resolvedModel);
927 // Invoke any listeners added while resolving this model. I.e. cyclic
@@ -809,7 +935,7 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
935 }
936 if (initializingHandler !== null) {
937 if (initializingHandler.errored) {
812 - throw initializingHandler.value;
938 + throw initializingHandler.reason;
939 }
940 if (initializingHandler.deps > 0) {
941 // We discovered new dependencies on modules that are not yet resolved.
@@ -1083,7 +1209,7 @@ function createElement(
1209 // into a Lazy so that we can still render up until that Lazy is rendered.
1210 const erroredChunk: ErroredChunk<React$Element<any>> = createErrorChunk(
1211 response,
1086 - handler.value,
1212 + handler.reason,
1213 );
1214 if (__DEV__) {
1215 initializeElement(response, element);
@@ -1140,7 +1266,7 @@ function createLazyChunkWrapper<T>(
1266 if (__DEV__) {
1267 // Ensure we have a live array to track future debug info.
1268 const chunkDebugInfo: ReactDebugInfo =
1143 - chunk._debugInfo || (chunk._debugInfo = []);
1269 + chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo));
1270 lazyType._debugInfo = chunkDebugInfo;
1271 }
1272 return lazyType;
@@ -1287,6 +1413,7 @@ function fulfillReference(
1413 const initializedChunk: InitializedChunk<any> = (chunk: any);
1414 initializedChunk.status = INITIALIZED;
1415 initializedChunk.value = handler.value;
1416 + initializedChunk.reason = handler.reason; // Used by streaming chunks
1417 if (resolveListeners !== null) {
1418 wakeChunk(resolveListeners, handler.value);
1419 }
@@ -1307,7 +1434,8 @@ function rejectReference(
1434 }
1435 const blockedValue = handler.value;
1436 handler.errored = true;
1310 - handler.value = error;
1437 + handler.value = null;
1438 + handler.reason = error;
1439 const chunk = handler.chunk;
1440 if (chunk === null || chunk.status !== BLOCKED) {
1441 return;
@@ -1382,6 +1510,7 @@ function waitForReference<T>(
1510 parent: null,
1511 chunk: null,
1512 value: null,
1513 + reason: null,
1514 deps: 1,
1515 errored: false,
1516 };
@@ -1468,6 +1597,7 @@ function loadServerReference<A: Iterable<any>, T>(
1597 parent: null,
1598 chunk: null,
1599 value: null,
1600 + reason: null,
1601 deps: 1,
1602 errored: false,
1603 };
@@ -1546,7 +1676,8 @@ function loadServerReference<A: Iterable<any>, T>(
1676 }
1677 const blockedValue = handler.value;
1678 handler.errored = true;
1549 - handler.value = error;
1679 + handler.value = null;
1680 + handler.reason = error;
1681 const chunk = handler.chunk;
1682 if (chunk === null || chunk.status !== BLOCKED) {
1683 return;
@@ -1656,6 +1787,7 @@ function getOutlinedModel<T>(
1787 parent: null,
1788 chunk: null,
1789 value: null,
1790 + reason: null,
1791 deps: 1,
1792 errored: false,
1793 };
@@ -1667,12 +1799,14 @@ function getOutlinedModel<T>(
1799 // an initialization handler so that we can catch it at the nearest Element.
1800 if (initializingHandler) {
1801 initializingHandler.errored = true;
1670 - initializingHandler.value = referencedChunk.reason;
1802 + initializingHandler.value = null;
1803 + initializingHandler.reason = referencedChunk.reason;
1804 } else {
1805 initializingHandler = {
1806 parent: null,
1807 chunk: null,
1675 - value: referencedChunk.reason,
1808 + value: null,
1809 + reason: referencedChunk.reason,
1810 deps: 0,
1811 errored: true,
1812 };
@@ -1726,6 +1860,7 @@ function getOutlinedModel<T>(
1860 parent: null,
1861 chunk: null,
1862 value: null,
1863 + reason: null,
1864 deps: 1,
1865 errored: false,
1866 };
@@ -1737,12 +1872,14 @@ function getOutlinedModel<T>(
1872 // an initialization handler so that we can catch it at the nearest Element.
1873 if (initializingHandler) {
1874 initializingHandler.errored = true;
1740 - initializingHandler.value = chunk.reason;
1875 + initializingHandler.value = null;
1876 + initializingHandler.reason = chunk.reason;
1877 } else {
1878 initializingHandler = {
1879 parent: null,
1880 chunk: null,
1745 - value: chunk.reason,
1881 + value: null,
1882 + reason: chunk.reason,
1883 deps: 0,
1884 errored: true,
1885 };
@@ -1850,6 +1987,7 @@ function parseModelString(
1987 parent: initializingHandler,
1988 chunk: null,
1989 value: null,
1990 + reason: null,
1991 deps: 0,
1992 errored: false,
1993 };
@@ -2214,6 +2352,7 @@ function ResponseInstance(
2352 }
2353 this._debugFindSourceMapURL = findSourceMapURL;
2354 this._debugChannel = debugChannel;
2355 + this._blockedConsole = null;
2356 this._replayConsole = replayConsole;
2357 this._rootEnvironmentName = rootEnv;
2358 if (debugChannel) {
@@ -2428,7 +2567,43 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>(
2567 return;
2568 }
2569 releasePendingChunk(response, chunk);
2570 +
2571 const resolveListeners = chunk.value;
2572 +
2573 + if (__DEV__) {
2574 + // Lazily initialize any debug info and block the initializing chunk on any unresolved entries.
2575 + if (chunk._debugChunk != null) {
2576 + const prevHandler = initializingHandler;
2577 + const prevChunk = initializingChunk;
2578 + initializingHandler = null;
2579 + const cyclicChunk: BlockedChunk<T> = (chunk: any);
2580 + cyclicChunk.status = BLOCKED;
2581 + cyclicChunk.value = null;
2582 + cyclicChunk.reason = null;
2583 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
2584 + initializingChunk = cyclicChunk;
2585 + }
2586 + try {
2587 + initializeDebugChunk(response, chunk);
2588 + chunk._debugChunk = null;
2589 + if (initializingHandler !== null) {
2590 + if (initializingHandler.errored) {
2591 + // Ignore error parsing debug info, we'll report the original error instead.
2592 + } else if (initializingHandler.deps > 0) {
2593 + // Leave blocked until we can resolve all the debug info.
2594 + initializingHandler.value = stream;
2595 + initializingHandler.reason = controller;
2596 + initializingHandler.chunk = cyclicChunk;
2597 + return;
2598 + }
2599 + }
2600 + } finally {
2601 + initializingHandler = prevHandler;
2602 + initializingChunk = prevChunk;
2603 + }
2604 + }
2605 + }
2606 +
2607 const resolvedChunk: InitializedStreamChunk<T> = (chunk: any);
2608 resolvedChunk.status = INITIALIZED;
2609 resolvedChunk.value = stream;
@@ -2807,6 +2982,29 @@ function resolvePostponeDev(
2982 }
2983 }
2984
2985 +function resolveErrorModel(
2986 + response: Response,
2987 + id: number,
2988 + row: UninitializedModel,
2989 +): void {
2990 + const chunks = response._chunks;
2991 + const chunk = chunks.get(id);
2992 + const errorInfo = JSON.parse(row);
2993 + let error;
2994 + if (__DEV__) {
2995 + error = resolveErrorDev(response, errorInfo);
2996 + } else {
2997 + error = resolveErrorProd(response);
2998 + }
2999 + (error: any).digest = errorInfo.digest;
3000 + const errorWithDigest: ErrorWithDigest = (error: any);
3001 + if (!chunk) {
3002 + chunks.set(id, createErrorChunk(response, errorWithDigest));
3003 + } else {
3004 + triggerErrorOnChunk(response, chunk, errorWithDigest);
3005 + }
3006 +}
3007 +
3008 function resolveHint<Code: HintCode>(
3009 response: Response,
3010 code: Code,
@@ -3202,20 +3400,15 @@ function initializeFakeStack(
3400 }
3401 }
3402
3205 -function resolveDebugInfo(
3403 +function initializeDebugInfo(
3404 response: Response,
3207 - id: number,
3208 - debugInfo:
3209 - | ReactComponentInfo
3210 - | ReactEnvironmentInfo
3211 - | ReactAsyncInfo
3212 - | ReactTimeInfo,
3213 -): void {
3405 + debugInfo: ReactDebugInfoEntry,
3406 +): ReactDebugInfoEntry {
3407 if (!__DEV__) {
3408 // These errors should never make it into a build so we don't need to encode them in codes.json
3409 // eslint-disable-next-line react-internal/prod-error-codes
3410 throw new Error(
3218 - 'resolveDebugInfo should never be called in production mode. This is a bug in React.',
3411 + 'initializeDebugInfo should never be called in production mode. This is a bug in React.',
3412 );
3413 }
3414 if (debugInfo.stack !== undefined) {
@@ -3258,11 +3451,54 @@ function resolveDebugInfo(
3451 };
3452 }
3453 }
3454 + return debugInfo;
3455 +}
3456
3262 - const chunk = getChunk(response, id);
3263 - const chunkDebugInfo: ReactDebugInfo =
3264 - chunk._debugInfo || (chunk._debugInfo = []);
3265 - chunkDebugInfo.push(debugInfo);
3457 +function resolveDebugModel(
3458 + response: Response,
3459 + id: number,
3460 + json: UninitializedModel,
3461 +): void {
3462 + const parentChunk = getChunk(response, id);
3463 + if (
3464 + parentChunk.status === INITIALIZED ||
3465 + parentChunk.status === ERRORED ||
3466 + parentChunk.status === HALTED ||
3467 + parentChunk.status === BLOCKED
3468 + ) {
3469 + // We shouldn't really get debug info late. It's too late to add it after we resolved.
3470 + return;
3471 + }
3472 + if (parentChunk.status === RESOLVED_MODULE) {
3473 + // We don't expect to get debug info on modules.
3474 + return;
3475 + }
3476 + const previousChunk = parentChunk._debugChunk;
3477 + const debugChunk: ResolvedModelChunk<ReactDebugInfoEntry> =
3478 + createResolvedModelChunk(response, json);
3479 + debugChunk._debugChunk = previousChunk; // Linked list of the debug chunks
3480 + parentChunk._debugChunk = debugChunk;
3481 + initializeDebugChunk(response, parentChunk);
3482 + if (
3483 + __DEV__ &&
3484 + ((debugChunk: any): SomeChunk<any>).status === BLOCKED &&
3485 + // TODO: This should check for the existence of the "readable" side, not the "writable".
3486 + response._debugChannel === undefined
3487 + ) {
3488 + if (json[0] === '"' && json[1] === '$') {
3489 + const path = json.slice(2, json.length - 1).split(':');
3490 + const outlinedId = parseInt(path[0], 16);
3491 + const chunk = getChunk(response, outlinedId);
3492 + if (chunk.status === PENDING) {
3493 + // We expect the debug chunk to have been emitted earlier in the stream. It might be
3494 + // blocked on other things but chunk should no longer be pending.
3495 + // If it's still pending that suggests that it was referencing an object in the debug
3496 + // channel, but no debug channel was wired up so it's missing. In this case we can just
3497 + // drop the debug info instead of halting the whole stream.
3498 + parentChunk._debugChunk = null;
3499 + }
3500 + }
3501 + }
3502 }
3503
3504 let currentOwnerInDEV: null | ReactComponentInfo = null;
@@ -3280,12 +3516,14 @@ function getCurrentStackInDEV(): string {
3516 const replayConsoleWithCallStack = {
3517 react_stack_bottom_frame: function (
3518 response: Response,
3283 - methodName: string,
3284 - stackTrace: ReactStackTrace,
3285 - owner: null | ReactComponentInfo,
3286 - env: string,
3287 - args: Array<mixed>,
3519 + payload: ConsoleEntry,
3520 ): void {
3521 + const methodName = payload[0];
3522 + const stackTrace = payload[1];
3523 + const owner = payload[2];
3524 + const env = payload[3];
3525 + const args = payload.slice(4);
3526 +
3527 // There really shouldn't be anything else on the stack atm.
3528 const prevStack = ReactSharedInternals.getCurrentStack;
3529 ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
@@ -3323,11 +3561,7 @@ const replayConsoleWithCallStack = {
3561
3562 const replayConsoleWithCallStackInDEV: (
3563 response: Response,
3326 - methodName: string,
3327 - stackTrace: ReactStackTrace,
3328 - owner: null | ReactComponentInfo,
3329 - env: string,
3330 - args: Array<mixed>,
3564 + payload: ConsoleEntry,
3565 ) => void = __DEV__
3566 ? // We use this technique to trick minifiers to preserve the function name.
3567 (replayConsoleWithCallStack.react_stack_bottom_frame.bind(
@@ -3335,9 +3569,17 @@ const replayConsoleWithCallStackInDEV: (
3569 ): any)
3570 : (null: any);
3571
3572 +type ConsoleEntry = [
3573 + string,
3574 + ReactStackTrace,
3575 + null | ReactComponentInfo,
3576 + string,
3577 + mixed,
3578 +];
3579 +
3580 function resolveConsoleEntry(
3581 response: Response,
3340 - value: UninitializedModel,
3582 + json: UninitializedModel,
3583 ): void {
3584 if (!__DEV__) {
3585 // These errors should never make it into a build so we don't need to encode them in codes.json
@@ -3351,27 +3593,47 @@ function resolveConsoleEntry(
3593 return;
3594 }
3595
3354 - const payload: [
3355 - string,
3356 - ReactStackTrace,
3357 - null | ReactComponentInfo,
3358 - string,
3359 - mixed,
3360 - ] = parseModel(response, value);
3361 - const methodName = payload[0];
3362 - const stackTrace = payload[1];
3363 - const owner = payload[2];
3364 - const env = payload[3];
3365 - const args = payload.slice(4);
3366 -
3367 - replayConsoleWithCallStackInDEV(
3368 - response,
3369 - methodName,
3370 - stackTrace,
3371 - owner,
3372 - env,
3373 - args,
3374 - );
3596 + const blockedChunk = response._blockedConsole;
3597 + if (blockedChunk == null) {
3598 + // If we're not blocked on any other chunks, we can try to eagerly initialize
3599 + // this as a fast-path to avoid awaiting them.
3600 + const chunk: ResolvedModelChunk<ConsoleEntry> = createResolvedModelChunk(
3601 + response,
3602 + json,
3603 + );
3604 + initializeModelChunk(chunk);
3605 + const initializedChunk: SomeChunk<ConsoleEntry> = chunk;
3606 + if (initializedChunk.status === INITIALIZED) {
3607 + replayConsoleWithCallStackInDEV(response, initializedChunk.value);
3608 + } else {
3609 + chunk.then(
3610 + v => replayConsoleWithCallStackInDEV(response, v),
3611 + e => {
3612 + // Ignore console errors for now. Unnecessary noise.
3613 + },
3614 + );
3615 + response._blockedConsole = chunk;
3616 + }
3617 + } else {
3618 + // We're still waiting on a previous chunk so we can't enqueue quite yet.
3619 + const chunk: SomeChunk<ConsoleEntry> = createPendingChunk(response);
3620 + chunk.then(
3621 + v => replayConsoleWithCallStackInDEV(response, v),
3622 + e => {
3623 + // Ignore console errors for now. Unnecessary noise.
3624 + },
3625 + );
3626 + response._blockedConsole = chunk;
3627 + const unblock = () => {
3628 + if (response._blockedConsole === chunk) {
3629 + // We were still the last chunk so we can now clear the queue and return
3630 + // to synchronous emitting.
3631 + response._blockedConsole = null;
3632 + }
3633 + resolveModelChunk(response, chunk, json);
3634 + };
3635 + blockedChunk.then(unblock, unblock);
3636 + }
3637 }
3638
3639 function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
@@ -3879,22 +4141,7 @@ function processFullStringRow(
4141 return;
4142 }
4143 case 69 /* "E" */: {
3882 - const errorInfo = JSON.parse(row);
3883 - let error;
3884 - if (__DEV__) {
3885 - error = resolveErrorDev(response, errorInfo);
3886 - } else {
3887 - error = resolveErrorProd(response);
3888 - }
3889 - (error: any).digest = errorInfo.digest;
3890 - const errorWithDigest: ErrorWithDigest = (error: any);
3891 - const chunks = response._chunks;
3892 - const chunk = chunks.get(id);
3893 - if (!chunk) {
3894 - chunks.set(id, createErrorChunk(response, errorWithDigest));
3895 - } else {
3896 - triggerErrorOnChunk(response, chunk, errorWithDigest);
3897 - }
4144 + resolveErrorModel(response, id, row);
4145 return;
4146 }
4147 case 84 /* "T" */: {
@@ -3916,30 +4163,7 @@ function processFullStringRow(
4163 }
4164 case 68 /* "D" */: {
4165 if (__DEV__) {
3919 - const chunk: ResolvedModelChunk<
3920 - | ReactComponentInfo
3921 - | ReactEnvironmentInfo
3922 - | ReactAsyncInfo
3923 - | ReactTimeInfo,
3924 - > = createResolvedModelChunk(response, row);
3925 - initializeModelChunk(chunk);
3926 - const initializedChunk: SomeChunk<
3927 - | ReactComponentInfo
3928 - | ReactEnvironmentInfo
3929 - | ReactAsyncInfo
3930 - | ReactTimeInfo,
3931 - > = chunk;
3932 - if (initializedChunk.status === INITIALIZED) {
3933 - resolveDebugInfo(response, id, initializedChunk.value);
3934 - } else {
3935 - // TODO: This is not going to resolve in the right order if there's more than one.
3936 - chunk.then(
3937 - v => resolveDebugInfo(response, id, v),
3938 - e => {
3939 - // Ignore debug info errors for now. Unnecessary noise.
3940 - },
3941 - );
3942 - }
4166 + resolveDebugModel(response, id, row);
4167 return;
4168 }
4169 // Fallthrough to share the error with Console entries.
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+32
@@ -2661,4 +2661,36 @@ describe('ReactFlightDOMBrowser', () => {
2661 '{"shared":{"id":42},"map":[[42,{"id":42}]]}',
2662 );
2663 });
2664 +
2665 + it('should resolve a cycle between debug info and the value it produces', async () => {
2666 + function Inner({style}) {
2667 + return <div style={style} />;
2668 + }
2669 +
2670 + function Component({style}) {
2671 + return <Inner style={style} />;
2672 + }
2673 +
2674 + const style = {};
2675 + const element = <Component style={style} />;
2676 + style.element = element;
2677 +
2678 + const stream = await serverAct(() =>
2679 + ReactServerDOMServer.renderToReadableStream(element, webpackMap),
2680 + );
2681 +
2682 + function ClientRoot({response}) {
2683 + return use(response);
2684 + }
2685 +
2686 + const response = ReactServerDOMClient.createFromReadableStream(stream);
2687 + const container = document.createElement('div');
2688 + const root = ReactDOMClient.createRoot(container);
2689 +
2690 + await act(() => {
2691 + root.render(<ClientRoot response={response} />);
2692 + });
2693 +
2694 + expect(container.innerHTML).toBe('<div></div>');
2695 + });
2696 });
packages/react-server/src/ReactFlightServer.js
+8 -13
@@ -58,11 +58,10 @@ import type {
58 FulfilledThenable,
59 RejectedThenable,
60 ReactDebugInfo,
61 + ReactDebugInfoEntry,
62 ReactComponentInfo,
62 - ReactEnvironmentInfo,
63 ReactIOInfo,
64 ReactAsyncInfo,
65 - ReactTimeInfo,
65 ReactStackTrace,
66 ReactCallSite,
67 ReactFunctionLocation,
@@ -1193,12 +1192,6 @@ function serializeAsyncIterable(
1192 __DEV__ ? task.debugTask : null,
1193 );
1194
1196 - // The task represents the Stop row. This adds a Start row.
1197 - request.pendingChunks++;
1198 - const startStreamRow =
1199 - streamTask.id.toString(16) + ':' + (isIterator ? 'x' : 'X') + '\n';
1200 - request.completedRegularChunks.push(stringToChunk(startStreamRow));
1201 -
1195 if (__DEV__) {
1196 const debugInfo: ?ReactDebugInfo = (iterable: any)._debugInfo;
1197 if (debugInfo) {
@@ -1206,6 +1199,12 @@ function serializeAsyncIterable(
1199 }
1200 }
1201
1202 + // The task represents the Stop row. This adds a Start row.
1203 + request.pendingChunks++;
1204 + const startStreamRow =
1205 + streamTask.id.toString(16) + ':' + (isIterator ? 'x' : 'X') + '\n';
1206 + request.completedRegularChunks.push(stringToChunk(startStreamRow));
1207 +
1208 function progress(
1209 entry:
1210 | {done: false, +value: ReactClientValue, ...}
@@ -4078,11 +4077,7 @@ function emitDebugHaltChunk(request: Request, id: number): void {
4077 function emitDebugChunk(
4078 request: Request,
4079 id: number,
4081 - debugInfo:
4082 - | ReactComponentInfo
4083 - | ReactAsyncInfo
4084 - | ReactEnvironmentInfo
4085 - | ReactTimeInfo,
4080 + debugInfo: ReactDebugInfoEntry,
4081 ): void {
4082 if (!__DEV__) {
4083 // These errors should never make it into a build so we don't need to encode them in codes.json
packages/shared/ReactTypes.js
+7 -3
@@ -259,9 +259,13 @@ export type ReactTimeInfo = {
259 +time: number, // performance.now
260 };
261
262 -export type ReactDebugInfo = Array<
263 - ReactComponentInfo | ReactEnvironmentInfo | ReactAsyncInfo | ReactTimeInfo,
264 ->;
262 +export type ReactDebugInfoEntry =
263 + | ReactComponentInfo
264 + | ReactEnvironmentInfo
265 + | ReactAsyncInfo
266 + | ReactTimeInfo;
267 +
268 +export type ReactDebugInfo = Array<ReactDebugInfoEntry>;
269
270 // Intrinsic ViewTransitionInstance. This type varies by Environment whether a particular
271 // renderer supports it.