355
const ABORTED = 3;
356
const ERRORED = 4;
357
358
-// object reference status
359
-const SEEN_BUT_NOT_YET_OUTLINED = -1;
360
-const NEVER_OUTLINED = -2;
361
-
358
type Task = {
359
id: number,
360
status: 0 | 1 | 3 | 4,
388
writtenSymbols: Map<symbol, number>,
389
writtenClientReferences: Map<ClientReferenceKey, number>,
390
writtenServerReferences: Map<ServerReference<any>, number>,
395
- writtenObjects: WeakMap<Reference, number>,
391
+ writtenObjects: WeakMap<Reference, string>,
392
identifierPrefix: string,
393
identifierCount: number,
394
taintCleanupQueue: Array<string | bigint>,
1424
// If we're in some kind of context we can't necessarily reuse this object depending
1425
// what parent components are used.
1426
} else {
1431
- request.writtenObjects.set(model, id);
1427
+ request.writtenObjects.set(model, serializeByValueID(id));
1428
}
1429
}
1430
const task: Task = {
1665
map: Map<ReactClientValue, ReactClientValue>,
1666
): string {
1667
const entries = Array.from(map);
1672
- for (let i = 0; i < entries.length; i++) {
1673
- const key = entries[i][0];
1674
- if (typeof key === 'object' && key !== null) {
1675
- const writtenObjects = request.writtenObjects;
1676
- const existingId = writtenObjects.get(key);
1677
- if (existingId === undefined) {
1678
- writtenObjects.set(key, SEEN_BUT_NOT_YET_OUTLINED);
1679
- }
1680
- }
1681
- }
1668
const id = outlineModel(request, entries);
1669
return '$Q' + id.toString(16);
1670
}
1677
1678
function serializeSet(request: Request, set: Set<ReactClientValue>): string {
1679
const entries = Array.from(set);
1694
- for (let i = 0; i < entries.length; i++) {
1695
- const key = entries[i];
1696
- if (typeof key === 'object' && key !== null) {
1697
- const writtenObjects = request.writtenObjects;
1698
- const existingId = writtenObjects.get(key);
1699
- if (existingId === undefined) {
1700
- writtenObjects.set(key, SEEN_BUT_NOT_YET_OUTLINED);
1701
- }
1702
- }
1703
- }
1680
const id = outlineModel(request, entries);
1681
return '$W' + id.toString(16);
1682
}
1888
switch ((value: any).$$typeof) {
1889
case REACT_ELEMENT_TYPE: {
1890
const writtenObjects = request.writtenObjects;
1915
- const existingId = writtenObjects.get(value);
1916
- if (existingId !== undefined) {
1917
- if (task.keyPath !== null || task.implicitSlot) {
1918
- // If we're in some kind of context we can't reuse the result of this render or
1919
- // previous renders of this element. We only reuse elements if they're not wrapped
1920
- // by another Server Component.
1921
- } else if (modelRoot === value) {
1922
- // This is the ID we're currently emitting so we need to write it
1923
- // once but if we discover it again, we refer to it by id.
1924
- modelRoot = null;
1925
- } else if (existingId === SEEN_BUT_NOT_YET_OUTLINED) {
1926
- // TODO: If we throw here we can treat this as suspending which causes an outline
1927
- // but that is able to reuse the same task if we're already in one but then that
1928
- // will be a lazy future value rather than guaranteed to exist but maybe that's good.
1929
- const newId = outlineModel(request, (value: any));
1930
- return serializeByValueID(newId);
1931
- } else {
1932
- // We've already emitted this as an outlined object, so we can refer to that by its
1933
- // existing ID. TODO: We should use a lazy reference since, unlike plain objects,
1934
- // elements might suspend so it might not have emitted yet even if we have the ID for
1935
- // it. However, this creates an extra wrapper when it's not needed. We should really
1936
- // detect whether this already was emitted and synchronously available. In that
1937
- // case we can refer to it synchronously and only make it lazy otherwise.
1938
- // We currently don't have a data structure that lets us see that though.
1939
- return serializeByValueID(existingId);
1940
- }
1891
+ if (task.keyPath !== null || task.implicitSlot) {
1892
+ // If we're in some kind of context we can't reuse the result of this render or
1893
+ // previous renders of this element. We only reuse elements if they're not wrapped
1894
+ // by another Server Component.
1895
} else {
1942
- // This is the first time we've seen this object. We may never see it again
1943
- // so we'll inline it. Mark it as seen. If we see it again, we'll outline.
1944
- writtenObjects.set(value, SEEN_BUT_NOT_YET_OUTLINED);
1945
- // The element's props are marked as "never outlined" so that they are inlined into
1946
- // the same row as the element itself.
1947
- writtenObjects.set((value: any).props, NEVER_OUTLINED);
1896
+ const existingReference = writtenObjects.get(value);
1897
+ if (existingReference !== undefined) {
1898
+ if (modelRoot === value) {
1899
+ // This is the ID we're currently emitting so we need to write it
1900
+ // once but if we discover it again, we refer to it by id.
1901
+ modelRoot = null;
1902
+ } else {
1903
+ // We've already emitted this as an outlined object, so we can refer to that by its
1904
+ // existing ID. TODO: We should use a lazy reference since, unlike plain objects,
1905
+ // elements might suspend so it might not have emitted yet even if we have the ID for
1906
+ // it. However, this creates an extra wrapper when it's not needed. We should really
1907
+ // detect whether this already was emitted and synchronously available. In that
1908
+ // case we can refer to it synchronously and only make it lazy otherwise.
1909
+ // We currently don't have a data structure that lets us see that though.
1910
+ return existingReference;
1911
+ }
1912
+ } else if (parentPropertyName.indexOf(':') === -1) {
1913
+ // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
1914
+ const parentReference = writtenObjects.get(parent);
1915
+ if (parentReference !== undefined) {
1916
+ // If the parent has a reference, we can refer to this object indirectly
1917
+ // through the property name inside that parent.
1918
+ writtenObjects.set(
1919
+ value,
1920
+ parentReference + ':' + parentPropertyName,
1921
+ );
1922
+ }
1923
+ }
1924
}
1925
1926
const element: ReactElement = (value: any);
2024
}
2025
2026
const writtenObjects = request.writtenObjects;
2051
- const existingId = writtenObjects.get(value);
2027
+ const existingReference = writtenObjects.get(value);
2028
// $FlowFixMe[method-unbinding]
2029
if (typeof value.then === 'function') {
2054
- if (existingId !== undefined) {
2030
+ if (existingReference !== undefined) {
2031
if (task.keyPath !== null || task.implicitSlot) {
2032
// If we're in some kind of context we can't reuse the result of this render or
2033
// previous renders of this element. We only reuse Promises if they're not wrapped
2040
modelRoot = null;
2041
} else {
2042
// We've seen this promise before, so we can just refer to the same result.
2067
- return serializePromiseID(existingId);
2043
+ return existingReference;
2044
}
2045
}
2046
// We assume that any object with a .then property is a "Thenable" type,
2047
// or a Promise type. Either of which can be represented by a Promise.
2048
const promiseId = serializeThenable(request, task, (value: any));
2073
- writtenObjects.set(value, promiseId);
2074
- return serializePromiseID(promiseId);
2049
+ const promiseReference = serializePromiseID(promiseId);
2050
+ writtenObjects.set(value, promiseReference);
2051
+ return promiseReference;
2052
}
2053
2077
- if (existingId !== undefined) {
2054
+ if (existingReference !== undefined) {
2055
if (modelRoot === value) {
2056
// This is the ID we're currently emitting so we need to write it
2057
// once but if we discover it again, we refer to it by id.
2058
modelRoot = null;
2082
- } else if (existingId === SEEN_BUT_NOT_YET_OUTLINED) {
2083
- const newId = outlineModel(request, (value: any));
2084
- return serializeByValueID(newId);
2085
- } else if (existingId !== NEVER_OUTLINED) {
2059
+ } else {
2060
// We've already emitted this as an outlined object, so we can
2061
// just refer to that by its existing ID.
2088
- return serializeByValueID(existingId);
2062
+ return existingReference;
2063
+ }
2064
+ } else if (parentPropertyName.indexOf(':') === -1) {
2065
+ // TODO: If the property name contains a colon, we don't dedupe. Escape instead.
2066
+ const parentReference = writtenObjects.get(parent);
2067
+ if (parentReference !== undefined) {
2068
+ // If the parent has a reference, we can refer to this object indirectly
2069
+ // through the property name inside that parent.
2070
+ let propertyName = parentPropertyName;
2071
+ if (isArray(parent) && parent[0] === REACT_ELEMENT_TYPE) {
2072
+ // For elements, we've converted it to an array but we'll have converted
2073
+ // it back to an element before we read the references so the property
2074
+ // needs to be aliased.
2075
+ switch (parentPropertyName) {
2076
+ case '1':
2077
+ propertyName = 'type';
2078
+ break;
2079
+ case '2':
2080
+ propertyName = 'key';
2081
+ break;
2082
+ case '3':
2083
+ propertyName = 'props';
2084
+ break;
2085
+ }
2086
+ }
2087
+ writtenObjects.set(value, parentReference + ':' + propertyName);
2088
}
2090
- } else {
2091
- // This is the first time we've seen this object. We may never see it again
2092
- // so we'll inline it. Mark it as seen. If we see it again, we'll outline.
2093
- writtenObjects.set(value, SEEN_BUT_NOT_YET_OUTLINED);
2089
}
2090
2091
if (isArray(value)) {
2652
counter.objectCount++;
2653
2654
const writtenObjects = request.writtenObjects;
2660
- const existingId = writtenObjects.get(value);
2655
+ const existingReference = writtenObjects.get(value);
2656
// $FlowFixMe[method-unbinding]
2657
if (typeof value.then === 'function') {
2663
- if (existingId !== undefined) {
2658
+ if (existingReference !== undefined) {
2659
// We've seen this promise before, so we can just refer to the same result.
2665
- return serializePromiseID(existingId);
2660
+ return existingReference;
2661
}
2662
2663
const thenable: Thenable<any> = (value: any);
2693
return serializeInfinitePromise();
2694
}
2695
2701
- if (existingId !== undefined && existingId >= 0) {
2696
+ if (existingReference !== undefined) {
2697
// We've already emitted this as a real object, so we can
2703
- // just refer to that by its existing ID.
2704
- return serializeByValueID(existingId);
2698
+ // just refer to that by its existing reference.
2699
+ return existingReference;
2700
}
2701
2702
if (isArray(value)) {
3088
task.implicitSlot = false;
3089
3090
if (typeof resolvedModel === 'object' && resolvedModel !== null) {
3091
+ // We're not in a contextual place here so we can refer to this object by this ID for
3092
+ // any future references.
3093
+ request.writtenObjects.set(resolvedModel, serializeByValueID(task.id));
3094
+
3095
// Object might contain unresolved values like additional elements.
3096
// This is simulating what the JSON loop would do if this was part of it.
3097
emitChunk(request, task, resolvedModel);