1 | import { __assign } from "tslib";
|
2 | import { Trie } from "@wry/trie";
|
3 | import { canUseWeakMap, canUseWeakSet, isNonNullObject as isObjectOrArray, } from "../../utilities/index.js";
|
4 | import { isArray } from "./helpers.js";
|
5 | function 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.
|
68 | var 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 | }());
|
180 | export { ObjectCanon };
|
181 | //# sourceMappingURL=object-canon.js.map |
\ | No newline at end of file |