[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.