UNPKG

8.98 kBJavaScriptView Raw
1import { __assign } from "tslib";
2import { Trie } from "@wry/trie";
3import { canUseWeakMap, canUseWeakSet, isNonNullObject as isObjectOrArray, } from "../../utilities/index.js";
4import { isArray } from "./helpers.js";
5function shallowCopy(value) {
6 if (isObjectOrArray(value)) {
7 return isArray(value) ?
8 value.slice(0)
9 : __assign({ __proto__: Object.getPrototypeOf(value) }, value);
10 }
11 return value;
12}
13// When programmers talk about the "canonical form" of an object, they
14// usually have the following meaning in mind, which I've copied from
15// https://en.wiktionary.org/wiki/canonical_form:
16//
17// 1. A standard or normal presentation of a mathematical entity [or
18// object]. A canonical form is an element of a set of representatives
19// of equivalence classes of forms such that there is a function or
20// procedure which projects every element of each equivalence class
21// onto that one element, the canonical form of that equivalence
22// class. The canonical form is expected to be simpler than the rest of
23// the forms in some way.
24//
25// That's a long-winded way of saying any two objects that have the same
26// canonical form may be considered equivalent, even if they are !==,
27// which usually means the objects are structurally equivalent (deeply
28// equal), but don't necessarily use the same memory.
29//
30// Like a literary or musical canon, this ObjectCanon class represents a
31// collection of unique canonical items (JavaScript objects), with the
32// important property that canon.admit(a) === canon.admit(b) if a and b
33// are deeply equal to each other. In terms of the definition above, the
34// canon.admit method is the "function or procedure which projects every"
35// object "onto that one element, the canonical form."
36//
37// In the worst case, the canonicalization process may involve looking at
38// every property in the provided object tree, so it takes the same order
39// of time as deep equality checking. Fortunately, already-canonicalized
40// objects are returned immediately from canon.admit, so the presence of
41// canonical subtrees tends to speed up canonicalization.
42//
43// Since consumers of canonical objects can check for deep equality in
44// constant time, canonicalizing cache results can massively improve the
45// performance of application code that skips re-rendering unchanged
46// results, such as "pure" UI components in a framework like React.
47//
48// Of course, since canonical objects may be shared widely between
49// unrelated consumers, it's important to think of them as immutable, even
50// though they are not actually frozen with Object.freeze in production,
51// due to the extra performance overhead that comes with frozen objects.
52//
53// Custom scalar objects whose internal class name is neither Array nor
54// Object can be included safely in the admitted tree, but they will not
55// be replaced with a canonical version (to put it another way, they are
56// assumed to be canonical already).
57//
58// If we ignore custom objects, no detection of cycles or repeated object
59// references is currently required by the StoreReader class, since
60// GraphQL result objects are JSON-serializable trees (and thus contain
61// neither cycles nor repeated subtrees), so we can avoid the complexity
62// of keeping track of objects we've already seen during the recursion of
63// the admit method.
64//
65// In the future, we may consider adding additional cases to the switch
66// statement to handle other common object types, such as "[object Date]"
67// objects, as needed.
68var ObjectCanon = /** @class */ (function () {
69 function ObjectCanon() {
70 // Set of all canonical objects this ObjectCanon has admitted, allowing
71 // canon.admit to return previously-canonicalized objects immediately.
72 this.known = new (canUseWeakSet ? WeakSet : Set)();
73 // Efficient storage/lookup structure for canonical objects.
74 this.pool = new Trie(canUseWeakMap);
75 // Make the ObjectCanon assume this value has already been
76 // canonicalized.
77 this.passes = new WeakMap();
78 // Arrays that contain the same elements in a different order can share
79 // the same SortedKeysInfo object, to save memory.
80 this.keysByJSON = new Map();
81 // This has to come last because it depends on keysByJSON.
82 this.empty = this.admit({});
83 }
84 ObjectCanon.prototype.isKnown = function (value) {
85 return isObjectOrArray(value) && this.known.has(value);
86 };
87 ObjectCanon.prototype.pass = function (value) {
88 if (isObjectOrArray(value)) {
89 var copy = shallowCopy(value);
90 this.passes.set(copy, value);
91 return copy;
92 }
93 return value;
94 };
95 ObjectCanon.prototype.admit = function (value) {
96 var _this = this;
97 if (isObjectOrArray(value)) {
98 var original = this.passes.get(value);
99 if (original)
100 return original;
101 var proto = Object.getPrototypeOf(value);
102 switch (proto) {
103 case Array.prototype: {
104 if (this.known.has(value))
105 return value;
106 var array = value.map(this.admit, this);
107 // Arrays are looked up in the Trie using their recursively
108 // canonicalized elements, and the known version of the array is
109 // preserved as node.array.
110 var node = this.pool.lookupArray(array);
111 if (!node.array) {
112 this.known.add((node.array = array));
113 // Since canonical arrays may be shared widely between
114 // unrelated consumers, it's important to regard them as
115 // immutable, even if they are not frozen in production.
116 if (globalThis.__DEV__ !== false) {
117 Object.freeze(array);
118 }
119 }
120 return node.array;
121 }
122 case null:
123 case Object.prototype: {
124 if (this.known.has(value))
125 return value;
126 var proto_1 = Object.getPrototypeOf(value);
127 var array_1 = [proto_1];
128 var keys = this.sortedKeys(value);
129 array_1.push(keys.json);
130 var firstValueIndex_1 = array_1.length;
131 keys.sorted.forEach(function (key) {
132 array_1.push(_this.admit(value[key]));
133 });
134 // Objects are looked up in the Trie by their prototype (which
135 // is *not* recursively canonicalized), followed by a JSON
136 // representation of their (sorted) keys, followed by the
137 // sequence of recursively canonicalized values corresponding to
138 // those keys. To keep the final results unambiguous with other
139 // sequences (such as arrays that just happen to contain [proto,
140 // keys.json, value1, value2, ...]), the known version of the
141 // object is stored as node.object.
142 var node = this.pool.lookupArray(array_1);
143 if (!node.object) {
144 var obj_1 = (node.object = Object.create(proto_1));
145 this.known.add(obj_1);
146 keys.sorted.forEach(function (key, i) {
147 obj_1[key] = array_1[firstValueIndex_1 + i];
148 });
149 // Since canonical objects may be shared widely between
150 // unrelated consumers, it's important to regard them as
151 // immutable, even if they are not frozen in production.
152 if (globalThis.__DEV__ !== false) {
153 Object.freeze(obj_1);
154 }
155 }
156 return node.object;
157 }
158 }
159 }
160 return value;
161 };
162 // It's worthwhile to cache the sorting of arrays of strings, since the
163 // same initial unsorted arrays tend to be encountered many times.
164 // Fortunately, we can reuse the Trie machinery to look up the sorted
165 // arrays in linear time (which is faster than sorting large arrays).
166 ObjectCanon.prototype.sortedKeys = function (obj) {
167 var keys = Object.keys(obj);
168 var node = this.pool.lookupArray(keys);
169 if (!node.keys) {
170 keys.sort();
171 var json = JSON.stringify(keys);
172 if (!(node.keys = this.keysByJSON.get(json))) {
173 this.keysByJSON.set(json, (node.keys = { sorted: keys, json: json }));
174 }
175 }
176 return node.keys;
177 };
178 return ObjectCanon;
179}());
180export { ObjectCanon };
181//# sourceMappingURL=object-canon.js.map
\No newline at end of file