Skip to content

Commit 8e7094e

Browse files
committed
Dedupe objects written as debugValue in a separate set
1 parent 4f84acc commit 8e7094e

File tree

3 files changed

+89
-25
lines changed

3 files changed

+89
-25
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,11 @@ ReactPromise.prototype.then = function <T>(
266266
initializeModuleChunk(chunk);
267267
break;
268268
}
269-
if (__DEV__ && enableAsyncDebugInfo) {
269+
if (
270+
__DEV__ &&
271+
enableAsyncDebugInfo &&
272+
(typeof resolve !== 'function' || !(resolve: any).isReactInternalListener)
273+
) {
270274
// Because only native Promises get picked up when we're awaiting we need to wrap
271275
// this in a native Promise in DEV. This means that these callbacks are no longer sync
272276
// but the lazy initialization is still sync and the .value can be inspected after,
@@ -1052,6 +1056,10 @@ function waitForReference<T>(
10521056
}
10531057
}
10541058
}
1059+
// Use to avoid the microtask resolution in DEV.
1060+
if (__DEV__ && enableAsyncDebugInfo) {
1061+
(fulfill: any).isReactInternalListener = true;
1062+
}
10551063

10561064
function reject(error: mixed): void {
10571065
if (handler.errored) {

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3328,19 +3328,10 @@ describe('ReactFlight', () => {
33283328
await ReactNoopFlightClient.read(transport);
33293329

33303330
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3331-
// TODO: Support cyclic objects in console encoding.
3332-
// expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
3333-
// const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
3334-
// expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
3335-
// expect(cyclic2.cycle).toBe(cyclic2);
3336-
expect(mockConsoleLog.mock.calls[0][0]).toBe(
3337-
'Unknown Value: React could not send it from the server.',
3338-
);
3339-
expect(mockConsoleLog.mock.calls[0][1].message).toBe(
3340-
'Converting circular structure to JSON\n' +
3341-
" --> starting at object with constructor 'Object'\n" +
3342-
" --- property 'cycle' closes the circle",
3343-
);
3331+
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
3332+
const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic;
3333+
expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned
3334+
expect(cyclic2.cycle).toBe(cyclic2);
33443335
});
33453336

33463337
// @gate !__DEV__ || enableComponentPerformanceTrack

packages/react-server/src/ReactFlightServer.js

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ export type Request = {
448448
environmentName: () => string,
449449
filterStackFrame: (url: string, functionName: string) => boolean,
450450
didWarnForKey: null | WeakSet<ReactComponentInfo>,
451+
writtenDebugObjects: WeakMap<Reference, string>,
451452
};
452453

453454
const {
@@ -567,6 +568,7 @@ function RequestInstance(
567568
? defaultFilterStackFrame
568569
: filterStackFrame;
569570
this.didWarnForKey = null;
571+
this.writtenDebugObjects = new WeakMap();
570572
}
571573

572574
let timeOrigin: number;
@@ -3553,7 +3555,7 @@ function outlineComponentInfo(
35533555
);
35543556
}
35553557

3556-
if (request.writtenObjects.has(componentInfo)) {
3558+
if (request.writtenDebugObjects.has(componentInfo)) {
35573559
// Already written
35583560
return;
35593561
}
@@ -3606,7 +3608,10 @@ function outlineComponentInfo(
36063608
componentDebugInfo.props = componentInfo.props;
36073609

36083610
const id = outlineConsoleValue(request, counter, componentDebugInfo);
3609-
request.writtenObjects.set(componentInfo, serializeByValueID(id));
3611+
const ref = serializeByValueID(id);
3612+
request.writtenDebugObjects.set(componentInfo, ref);
3613+
// We also store this in the main dedupe set so that it can be referenced by inline React Elements.
3614+
request.writtenObjects.set(componentInfo, ref);
36103615
}
36113616

36123617
function emitIOInfoChunk(
@@ -3690,14 +3695,14 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
36903695
owner,
36913696
debugStack,
36923697
);
3693-
request.writtenObjects.set(ioInfo, serializeByValueID(id));
3698+
request.writtenDebugObjects.set(ioInfo, serializeByValueID(id));
36943699
}
36953700

36963701
function serializeIONode(
36973702
request: Request,
36983703
ioNode: IONode | PromiseNode,
36993704
): string {
3700-
const existingRef = request.writtenObjects.get(ioNode);
3705+
const existingRef = request.writtenDebugObjects.get(ioNode);
37013706
if (existingRef !== undefined) {
37023707
// Already written
37033708
return existingRef;
@@ -3740,7 +3745,7 @@ function serializeIONode(
37403745
stack,
37413746
);
37423747
const ref = serializeByValueID(id);
3743-
request.writtenObjects.set(ioNode, ref);
3748+
request.writtenDebugObjects.set(ioNode, ref);
37443749
return ref;
37453750
}
37463751

@@ -3797,6 +3802,8 @@ function serializeEval(source: string): string {
37973802
return '$E' + source;
37983803
}
37993804

3805+
let debugModelRoot: mixed = null;
3806+
let debugNoOutline: mixed = null;
38003807
// This is a forked version of renderModel which should never error, never suspend and is limited
38013808
// in the depth it can encode.
38023809
function renderConsoleValue(
@@ -3840,11 +3847,57 @@ function renderConsoleValue(
38403847
}
38413848
}
38423849

3850+
const writtenDebugObjects = request.writtenDebugObjects;
3851+
const existingDebugReference = writtenDebugObjects.get(value);
3852+
if (existingDebugReference !== undefined) {
3853+
if (debugModelRoot === value) {
3854+
// This is the ID we're currently emitting so we need to write it
3855+
// once but if we discover it again, we refer to it by id.
3856+
debugModelRoot = null;
3857+
} else {
3858+
// We've already emitted this as a debug object. We favor that version if available.
3859+
return existingDebugReference;
3860+
}
3861+
} else if (parentPropertyName.indexOf(':') === -1) {
3862+
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.
3863+
const parentReference = writtenDebugObjects.get(parent);
3864+
if (parentReference !== undefined) {
3865+
// If the parent has a reference, we can refer to this object indirectly
3866+
// through the property name inside that parent.
3867+
let propertyName = parentPropertyName;
3868+
if (isArray(parent) && parent[0] === REACT_ELEMENT_TYPE) {
3869+
// For elements, we've converted it to an array but we'll have converted
3870+
// it back to an element before we read the references so the property
3871+
// needs to be aliased.
3872+
switch (parentPropertyName) {
3873+
case '1':
3874+
propertyName = 'type';
3875+
break;
3876+
case '2':
3877+
propertyName = 'key';
3878+
break;
3879+
case '3':
3880+
propertyName = 'props';
3881+
break;
3882+
case '4':
3883+
propertyName = '_owner';
3884+
break;
3885+
}
3886+
}
3887+
writtenDebugObjects.set(value, parentReference + ':' + propertyName);
3888+
} else if (debugNoOutline !== value) {
3889+
// If this isn't the root object (like meta data) and we don't have an id for it, outline
3890+
// it so that we can dedupe it by reference later.
3891+
const outlinedId = outlineConsoleValue(request, counter, value);
3892+
return serializeByValueID(outlinedId);
3893+
}
3894+
}
3895+
38433896
const writtenObjects = request.writtenObjects;
38443897
const existingReference = writtenObjects.get(value);
38453898
if (existingReference !== undefined) {
3846-
// We've already emitted this as a real object, so we can
3847-
// just refer to that by its existing reference.
3899+
// We've already emitted this as a real object, so we can refer to that by its existing reference.
3900+
// This might be slightly different serialization than what renderConsoleValue would've produced.
38483901
return existingReference;
38493902
}
38503903

@@ -4068,8 +4121,8 @@ function renderConsoleValue(
40684121
}
40694122

40704123
// Serialize the body of the function as an eval so it can be printed.
4071-
const writtenObjects = request.writtenObjects;
4072-
const existingReference = writtenObjects.get(value);
4124+
const writtenDebugObjects = request.writtenDebugObjects;
4125+
const existingReference = writtenDebugObjects.get(value);
40734126
if (existingReference !== undefined) {
40744127
// We've already emitted this function, so we can
40754128
// just refer to that by its existing reference.
@@ -4085,7 +4138,7 @@ function renderConsoleValue(
40854138
const processedChunk = encodeReferenceChunk(request, id, serializedValue);
40864139
request.completedRegularChunks.push(processedChunk);
40874140
const reference = serializeByValueID(id);
4088-
writtenObjects.set(value, reference);
4141+
writtenDebugObjects.set(value, reference);
40894142
return reference;
40904143
}
40914144

@@ -4144,6 +4197,8 @@ function serializeConsoleValue(
41444197
}
41454198
}
41464199

4200+
const prevNoOutline = debugNoOutline;
4201+
debugNoOutline = model;
41474202
try {
41484203
// $FlowFixMe[incompatible-cast] stringify can return null
41494204
return (stringify(model, replacer): string);
@@ -4152,6 +4207,8 @@ function serializeConsoleValue(
41524207
return (stringify(
41534208
'Unknown Value: React could not send it from the server.\n' + x.message,
41544209
): string);
4210+
} finally {
4211+
debugNoOutline = prevNoOutline;
41554212
}
41564213
}
41574214

@@ -4195,6 +4252,13 @@ function outlineConsoleValue(
41954252
}
41964253
}
41974254

4255+
const id = request.nextChunkId++;
4256+
const prevModelRoot = debugModelRoot;
4257+
debugModelRoot = model;
4258+
if (typeof model === 'object' && model !== null) {
4259+
// Future references can refer to this object by id.
4260+
request.writtenDebugObjects.set(model, serializeByValueID(id));
4261+
}
41984262
let json: string;
41994263
try {
42004264
// $FlowFixMe[incompatible-cast] stringify can return null
@@ -4204,10 +4268,11 @@ function outlineConsoleValue(
42044268
json = (stringify(
42054269
'Unknown Value: React could not send it from the server.\n' + x.message,
42064270
): string);
4271+
} finally {
4272+
debugModelRoot = prevModelRoot;
42074273
}
42084274

42094275
request.pendingChunks++;
4210-
const id = request.nextChunkId++;
42114276
const row = id.toString(16) + ':' + json + '\n';
42124277
const processedChunk = stringToChunk(row);
42134278
request.completedRegularChunks.push(processedChunk);

0 commit comments

Comments
 (0)