Skip to content

Commit 1003fbb

Browse files
committed
Encode references to existing objects by property path
1 parent 46abd7b commit 1003fbb

File tree

3 files changed

+104
-98
lines changed

3 files changed

+104
-98
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ function createModelResolver<T>(
661661
cyclic: boolean,
662662
response: Response,
663663
map: (response: Response, model: any) => T,
664+
path: Array<string>,
664665
): (value: any) => void {
665666
let blocked;
666667
if (initializingChunkBlockedModel) {
@@ -675,6 +676,9 @@ function createModelResolver<T>(
675676
};
676677
}
677678
return value => {
679+
for (let i = 1; i < path.length; i++) {
680+
value = value[path[i]];
681+
}
678682
parentObject[key] = map(response, value);
679683

680684
// If this is the root object for a model reference, where `blocked.value`
@@ -733,11 +737,13 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
733737

734738
function getOutlinedModel<T>(
735739
response: Response,
736-
id: number,
740+
reference: string,
737741
parentObject: Object,
738742
key: string,
739743
map: (response: Response, model: any) => T,
740744
): T {
745+
const path = reference.split(':');
746+
const id = parseInt(path[0], 16);
741747
const chunk = getChunk(response, id);
742748
switch (chunk.status) {
743749
case RESOLVED_MODEL:
@@ -750,7 +756,11 @@ function getOutlinedModel<T>(
750756
// The status might have changed after initialization.
751757
switch (chunk.status) {
752758
case INITIALIZED:
753-
const chunkValue = map(response, chunk.value);
759+
let value = chunk.value;
760+
for (let i = 1; i < path.length; i++) {
761+
value = value[path[i]];
762+
}
763+
const chunkValue = map(response, value);
754764
if (__DEV__ && chunk._debugInfo) {
755765
// If we have a direct reference to an object that was rendered by a synchronous
756766
// server component, it might have some debug info about how it was rendered.
@@ -790,6 +800,7 @@ function getOutlinedModel<T>(
790800
chunk.status === CYCLIC,
791801
response,
792802
map,
803+
path,
793804
),
794805
createModelReject(parentChunk),
795806
);
@@ -874,10 +885,10 @@ function parseModelString(
874885
}
875886
case 'F': {
876887
// Server Reference
877-
const id = parseInt(value.slice(2), 16);
888+
const ref = value.slice(2);
878889
return getOutlinedModel(
879890
response,
880-
id,
891+
ref,
881892
parentObject,
882893
key,
883894
createServerReferenceProxy,
@@ -897,39 +908,39 @@ function parseModelString(
897908
}
898909
case 'Q': {
899910
// Map
900-
const id = parseInt(value.slice(2), 16);
901-
return getOutlinedModel(response, id, parentObject, key, createMap);
911+
const ref = value.slice(2);
912+
return getOutlinedModel(response, ref, parentObject, key, createMap);
902913
}
903914
case 'W': {
904915
// Set
905-
const id = parseInt(value.slice(2), 16);
906-
return getOutlinedModel(response, id, parentObject, key, createSet);
916+
const ref = value.slice(2);
917+
return getOutlinedModel(response, ref, parentObject, key, createSet);
907918
}
908919
case 'B': {
909920
// Blob
910921
if (enableBinaryFlight) {
911-
const id = parseInt(value.slice(2), 16);
912-
return getOutlinedModel(response, id, parentObject, key, createBlob);
922+
const ref = value.slice(2);
923+
return getOutlinedModel(response, ref, parentObject, key, createBlob);
913924
}
914925
return undefined;
915926
}
916927
case 'K': {
917928
// FormData
918-
const id = parseInt(value.slice(2), 16);
929+
const ref = value.slice(2);
919930
return getOutlinedModel(
920931
response,
921-
id,
932+
ref,
922933
parentObject,
923934
key,
924935
createFormData,
925936
);
926937
}
927938
case 'i': {
928939
// Iterator
929-
const id = parseInt(value.slice(2), 16);
940+
const ref = value.slice(2);
930941
return getOutlinedModel(
931942
response,
932-
id,
943+
ref,
933944
parentObject,
934945
key,
935946
extractIterator,
@@ -981,8 +992,8 @@ function parseModelString(
981992
}
982993
default: {
983994
// We assume that anything else is a reference ID.
984-
const id = parseInt(value.slice(1), 16);
985-
return getOutlinedModel(response, id, parentObject, key, createModel);
995+
const ref = value.slice(1);
996+
return getOutlinedModel(response, ref, parentObject, key, createModel);
986997
}
987998
}
988999
}

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ describe('ReactFlightDOMEdge', () => {
231231
const [stream1, stream2] = passThrough(stream).tee();
232232

233233
const serializedContent = await readResult(stream1);
234-
expect(serializedContent.length).toBeLessThan(400);
234+
expect(serializedContent.length).toBeLessThan(470);
235235

236236
const result = await ReactServerDOMClient.createFromReadableStream(
237237
stream2,

packages/react-server/src/ReactFlightServer.js

Lines changed: 76 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,6 @@ const COMPLETED = 1;
266266
const ABORTED = 3;
267267
const ERRORED = 4;
268268

269-
// object reference status
270-
const SEEN_BUT_NOT_YET_OUTLINED = -1;
271-
const NEVER_OUTLINED = -2;
272-
273269
type Task = {
274270
id: number,
275271
status: 0 | 1 | 3 | 4,
@@ -303,7 +299,7 @@ export type Request = {
303299
writtenSymbols: Map<symbol, number>,
304300
writtenClientReferences: Map<ClientReferenceKey, number>,
305301
writtenServerReferences: Map<ServerReference<any>, number>,
306-
writtenObjects: WeakMap<Reference, number>,
302+
writtenObjects: WeakMap<Reference, string>,
307303
identifierPrefix: string,
308304
identifierCount: number,
309305
taintCleanupQueue: Array<string | bigint>,
@@ -1270,7 +1266,7 @@ function createTask(
12701266
// If we're in some kind of context we can't necessarily reuse this object depending
12711267
// what parent components are used.
12721268
} else {
1273-
request.writtenObjects.set(model, id);
1269+
request.writtenObjects.set(model, serializeByValueID(id));
12741270
}
12751271
}
12761272
const task: Task = {
@@ -1511,16 +1507,6 @@ function serializeMap(
15111507
map: Map<ReactClientValue, ReactClientValue>,
15121508
): string {
15131509
const entries = Array.from(map);
1514-
for (let i = 0; i < entries.length; i++) {
1515-
const key = entries[i][0];
1516-
if (typeof key === 'object' && key !== null) {
1517-
const writtenObjects = request.writtenObjects;
1518-
const existingId = writtenObjects.get(key);
1519-
if (existingId === undefined) {
1520-
writtenObjects.set(key, SEEN_BUT_NOT_YET_OUTLINED);
1521-
}
1522-
}
1523-
}
15241510
const id = outlineModel(request, entries);
15251511
return '$Q' + id.toString(16);
15261512
}
@@ -1533,16 +1519,6 @@ function serializeFormData(request: Request, formData: FormData): string {
15331519

15341520
function serializeSet(request: Request, set: Set<ReactClientValue>): string {
15351521
const entries = Array.from(set);
1536-
for (let i = 0; i < entries.length; i++) {
1537-
const key = entries[i];
1538-
if (typeof key === 'object' && key !== null) {
1539-
const writtenObjects = request.writtenObjects;
1540-
const existingId = writtenObjects.get(key);
1541-
if (existingId === undefined) {
1542-
writtenObjects.set(key, SEEN_BUT_NOT_YET_OUTLINED);
1543-
}
1544-
}
1545-
}
15461522
const id = outlineModel(request, entries);
15471523
return '$W' + id.toString(16);
15481524
}
@@ -1754,42 +1730,42 @@ function renderModelDestructive(
17541730
switch ((value: any).$$typeof) {
17551731
case REACT_ELEMENT_TYPE: {
17561732
const writtenObjects = request.writtenObjects;
1757-
const existingId = writtenObjects.get(value);
1758-
if (existingId !== undefined) {
1759-
if (
1760-
enableServerComponentKeys &&
1761-
(task.keyPath !== null || task.implicitSlot)
1762-
) {
1763-
// If we're in some kind of context we can't reuse the result of this render or
1764-
// previous renders of this element. We only reuse elements if they're not wrapped
1765-
// by another Server Component.
1766-
} else if (modelRoot === value) {
1767-
// This is the ID we're currently emitting so we need to write it
1768-
// once but if we discover it again, we refer to it by id.
1769-
modelRoot = null;
1770-
} else if (existingId === SEEN_BUT_NOT_YET_OUTLINED) {
1771-
// TODO: If we throw here we can treat this as suspending which causes an outline
1772-
// but that is able to reuse the same task if we're already in one but then that
1773-
// will be a lazy future value rather than guaranteed to exist but maybe that's good.
1774-
const newId = outlineModel(request, (value: any));
1775-
return serializeByValueID(newId);
1776-
} else {
1777-
// We've already emitted this as an outlined object, so we can refer to that by its
1778-
// existing ID. TODO: We should use a lazy reference since, unlike plain objects,
1779-
// elements might suspend so it might not have emitted yet even if we have the ID for
1780-
// it. However, this creates an extra wrapper when it's not needed. We should really
1781-
// detect whether this already was emitted and synchronously available. In that
1782-
// case we can refer to it synchronously and only make it lazy otherwise.
1783-
// We currently don't have a data structure that lets us see that though.
1784-
return serializeByValueID(existingId);
1785-
}
1733+
if (
1734+
enableServerComponentKeys &&
1735+
(task.keyPath !== null || task.implicitSlot)
1736+
) {
1737+
// If we're in some kind of context we can't reuse the result of this render or
1738+
// previous renders of this element. We only reuse elements if they're not wrapped
1739+
// by another Server Component.
17861740
} else {
1787-
// This is the first time we've seen this object. We may never see it again
1788-
// so we'll inline it. Mark it as seen. If we see it again, we'll outline.
1789-
writtenObjects.set(value, SEEN_BUT_NOT_YET_OUTLINED);
1790-
// The element's props are marked as "never outlined" so that they are inlined into
1791-
// the same row as the element itself.
1792-
writtenObjects.set((value: any).props, NEVER_OUTLINED);
1741+
const existingReference = writtenObjects.get(value);
1742+
if (existingReference !== undefined) {
1743+
if (modelRoot === value) {
1744+
// This is the ID we're currently emitting so we need to write it
1745+
// once but if we discover it again, we refer to it by id.
1746+
modelRoot = null;
1747+
} else {
1748+
// We've already emitted this as an outlined object, so we can refer to that by its
1749+
// existing ID. TODO: We should use a lazy reference since, unlike plain objects,
1750+
// elements might suspend so it might not have emitted yet even if we have the ID for
1751+
// it. However, this creates an extra wrapper when it's not needed. We should really
1752+
// detect whether this already was emitted and synchronously available. In that
1753+
// case we can refer to it synchronously and only make it lazy otherwise.
1754+
// We currently don't have a data structure that lets us see that though.
1755+
return existingReference;
1756+
}
1757+
} else if (parentPropertyName.indexOf(':') === -1) {
1758+
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.
1759+
const parentReference = writtenObjects.get(parent);
1760+
if (parentReference !== undefined) {
1761+
// If the parent has a reference, we can refer to this object indirectly
1762+
// through the property name inside that parent.
1763+
writtenObjects.set(
1764+
value,
1765+
parentReference + ':' + parentPropertyName,
1766+
);
1767+
}
1768+
}
17931769
}
17941770

17951771
const element: ReactElement = (value: any);
@@ -1885,10 +1861,10 @@ function renderModelDestructive(
18851861
}
18861862

18871863
const writtenObjects = request.writtenObjects;
1888-
const existingId = writtenObjects.get(value);
1864+
const existingReference = writtenObjects.get(value);
18891865
// $FlowFixMe[method-unbinding]
18901866
if (typeof value.then === 'function') {
1891-
if (existingId !== undefined) {
1867+
if (existingReference !== undefined) {
18921868
if (
18931869
enableServerComponentKeys &&
18941870
(task.keyPath !== null || task.implicitSlot)
@@ -1904,33 +1880,48 @@ function renderModelDestructive(
19041880
modelRoot = null;
19051881
} else {
19061882
// We've seen this promise before, so we can just refer to the same result.
1907-
return serializePromiseID(existingId);
1883+
return '$@' + existingReference.slice(1);
19081884
}
19091885
}
19101886
// We assume that any object with a .then property is a "Thenable" type,
19111887
// or a Promise type. Either of which can be represented by a Promise.
19121888
const promiseId = serializeThenable(request, task, (value: any));
1913-
writtenObjects.set(value, promiseId);
1889+
writtenObjects.set(value, serializeByValueID(promiseId));
19141890
return serializePromiseID(promiseId);
19151891
}
19161892

1917-
if (existingId !== undefined) {
1893+
if (existingReference !== undefined) {
19181894
if (modelRoot === value) {
19191895
// This is the ID we're currently emitting so we need to write it
19201896
// once but if we discover it again, we refer to it by id.
19211897
modelRoot = null;
1922-
} else if (existingId === SEEN_BUT_NOT_YET_OUTLINED) {
1923-
const newId = outlineModel(request, (value: any));
1924-
return serializeByValueID(newId);
1925-
} else if (existingId !== NEVER_OUTLINED) {
1898+
} else {
19261899
// We've already emitted this as an outlined object, so we can
19271900
// just refer to that by its existing ID.
1928-
return serializeByValueID(existingId);
1901+
return existingReference;
1902+
}
1903+
} else if (parentPropertyName.indexOf(':') === -1) {
1904+
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.
1905+
const parentReference = writtenObjects.get(parent);
1906+
if (parentReference !== undefined) {
1907+
// If the parent has a reference, we can refer to this object indirectly
1908+
// through the property name inside that parent.
1909+
let propertyName = parentPropertyName;
1910+
if (isArray(parent) && parent[0] === REACT_ELEMENT_TYPE) {
1911+
// For elements, we've converted it to an array but we'll have converted
1912+
// it back to an element before we read the references so the property
1913+
// needs to be aliased.
1914+
switch (parentPropertyName) {
1915+
case '1':
1916+
propertyName = 'type';
1917+
case '2':
1918+
propertyName = 'key';
1919+
case '3':
1920+
propertyName = 'props';
1921+
}
1922+
}
1923+
writtenObjects.set(value, parentReference + ':' + propertyName);
19291924
}
1930-
} else {
1931-
// This is the first time we've seen this object. We may never see it again
1932-
// so we'll inline it. Mark it as seen. If we see it again, we'll outline.
1933-
writtenObjects.set(value, SEEN_BUT_NOT_YET_OUTLINED);
19341925
}
19351926

19361927
if (isArray(value)) {
@@ -2497,12 +2488,12 @@ function renderConsoleValue(
24972488
counter.objectCount++;
24982489

24992490
const writtenObjects = request.writtenObjects;
2500-
const existingId = writtenObjects.get(value);
2491+
const existingReference = writtenObjects.get(value);
25012492
// $FlowFixMe[method-unbinding]
25022493
if (typeof value.then === 'function') {
2503-
if (existingId !== undefined) {
2494+
if (existingReference !== undefined) {
25042495
// We've seen this promise before, so we can just refer to the same result.
2505-
return serializePromiseID(existingId);
2496+
return '$@' + existingReference.slice(1);
25062497
}
25072498

25082499
const thenable: Thenable<any> = (value: any);
@@ -2538,10 +2529,10 @@ function renderConsoleValue(
25382529
return serializeInfinitePromise();
25392530
}
25402531

2541-
if (existingId !== undefined && existingId >= 0) {
2532+
if (existingReference !== undefined) {
25422533
// We've already emitted this as a real object, so we can
2543-
// just refer to that by its existing ID.
2544-
return serializeByValueID(existingId);
2534+
// just refer to that by its existing reference.
2535+
return existingReference;
25452536
}
25462537

25472538
if (isArray(value)) {
@@ -2933,6 +2924,10 @@ function retryTask(request: Request, task: Task): void {
29332924
task.implicitSlot = false;
29342925

29352926
if (typeof resolvedModel === 'object' && resolvedModel !== null) {
2927+
// We're not in a contextual place here so we can refer to this object by this ID for
2928+
// any future references.
2929+
request.writtenObjects.set(resolvedModel, serializeByValueID(task.id));
2930+
29362931
// Object might contain unresolved values like additional elements.
29372932
// This is simulating what the JSON loop would do if this was part of it.
29382933
emitChunk(request, task, resolvedModel);

0 commit comments

Comments
 (0)