You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
8.8 KiB
181 lines
8.8 KiB
import { __assign } from "tslib"; |
|
import { Trie } from "@wry/trie"; |
|
import { canUseWeakMap, canUseWeakSet, isNonNullObject as isObjectOrArray, } from "../../utilities/index.js"; |
|
import { isArray } from "./helpers.js"; |
|
function shallowCopy(value) { |
|
if (isObjectOrArray(value)) { |
|
return isArray(value) ? |
|
value.slice(0) |
|
: __assign({ __proto__: Object.getPrototypeOf(value) }, value); |
|
} |
|
return value; |
|
} |
|
// When programmers talk about the "canonical form" of an object, they |
|
// usually have the following meaning in mind, which I've copied from |
|
// https://en.wiktionary.org/wiki/canonical_form: |
|
// |
|
// 1. A standard or normal presentation of a mathematical entity [or |
|
// object]. A canonical form is an element of a set of representatives |
|
// of equivalence classes of forms such that there is a function or |
|
// procedure which projects every element of each equivalence class |
|
// onto that one element, the canonical form of that equivalence |
|
// class. The canonical form is expected to be simpler than the rest of |
|
// the forms in some way. |
|
// |
|
// That's a long-winded way of saying any two objects that have the same |
|
// canonical form may be considered equivalent, even if they are !==, |
|
// which usually means the objects are structurally equivalent (deeply |
|
// equal), but don't necessarily use the same memory. |
|
// |
|
// Like a literary or musical canon, this ObjectCanon class represents a |
|
// collection of unique canonical items (JavaScript objects), with the |
|
// important property that canon.admit(a) === canon.admit(b) if a and b |
|
// are deeply equal to each other. In terms of the definition above, the |
|
// canon.admit method is the "function or procedure which projects every" |
|
// object "onto that one element, the canonical form." |
|
// |
|
// In the worst case, the canonicalization process may involve looking at |
|
// every property in the provided object tree, so it takes the same order |
|
// of time as deep equality checking. Fortunately, already-canonicalized |
|
// objects are returned immediately from canon.admit, so the presence of |
|
// canonical subtrees tends to speed up canonicalization. |
|
// |
|
// Since consumers of canonical objects can check for deep equality in |
|
// constant time, canonicalizing cache results can massively improve the |
|
// performance of application code that skips re-rendering unchanged |
|
// results, such as "pure" UI components in a framework like React. |
|
// |
|
// Of course, since canonical objects may be shared widely between |
|
// unrelated consumers, it's important to think of them as immutable, even |
|
// though they are not actually frozen with Object.freeze in production, |
|
// due to the extra performance overhead that comes with frozen objects. |
|
// |
|
// Custom scalar objects whose internal class name is neither Array nor |
|
// Object can be included safely in the admitted tree, but they will not |
|
// be replaced with a canonical version (to put it another way, they are |
|
// assumed to be canonical already). |
|
// |
|
// If we ignore custom objects, no detection of cycles or repeated object |
|
// references is currently required by the StoreReader class, since |
|
// GraphQL result objects are JSON-serializable trees (and thus contain |
|
// neither cycles nor repeated subtrees), so we can avoid the complexity |
|
// of keeping track of objects we've already seen during the recursion of |
|
// the admit method. |
|
// |
|
// In the future, we may consider adding additional cases to the switch |
|
// statement to handle other common object types, such as "[object Date]" |
|
// objects, as needed. |
|
var ObjectCanon = /** @class */ (function () { |
|
function ObjectCanon() { |
|
// Set of all canonical objects this ObjectCanon has admitted, allowing |
|
// canon.admit to return previously-canonicalized objects immediately. |
|
this.known = new (canUseWeakSet ? WeakSet : Set)(); |
|
// Efficient storage/lookup structure for canonical objects. |
|
this.pool = new Trie(canUseWeakMap); |
|
// Make the ObjectCanon assume this value has already been |
|
// canonicalized. |
|
this.passes = new WeakMap(); |
|
// Arrays that contain the same elements in a different order can share |
|
// the same SortedKeysInfo object, to save memory. |
|
this.keysByJSON = new Map(); |
|
// This has to come last because it depends on keysByJSON. |
|
this.empty = this.admit({}); |
|
} |
|
ObjectCanon.prototype.isKnown = function (value) { |
|
return isObjectOrArray(value) && this.known.has(value); |
|
}; |
|
ObjectCanon.prototype.pass = function (value) { |
|
if (isObjectOrArray(value)) { |
|
var copy = shallowCopy(value); |
|
this.passes.set(copy, value); |
|
return copy; |
|
} |
|
return value; |
|
}; |
|
ObjectCanon.prototype.admit = function (value) { |
|
var _this = this; |
|
if (isObjectOrArray(value)) { |
|
var original = this.passes.get(value); |
|
if (original) |
|
return original; |
|
var proto = Object.getPrototypeOf(value); |
|
switch (proto) { |
|
case Array.prototype: { |
|
if (this.known.has(value)) |
|
return value; |
|
var array = value.map(this.admit, this); |
|
// Arrays are looked up in the Trie using their recursively |
|
// canonicalized elements, and the known version of the array is |
|
// preserved as node.array. |
|
var node = this.pool.lookupArray(array); |
|
if (!node.array) { |
|
this.known.add((node.array = array)); |
|
// Since canonical arrays may be shared widely between |
|
// unrelated consumers, it's important to regard them as |
|
// immutable, even if they are not frozen in production. |
|
if (globalThis.__DEV__ !== false) { |
|
Object.freeze(array); |
|
} |
|
} |
|
return node.array; |
|
} |
|
case null: |
|
case Object.prototype: { |
|
if (this.known.has(value)) |
|
return value; |
|
var proto_1 = Object.getPrototypeOf(value); |
|
var array_1 = [proto_1]; |
|
var keys = this.sortedKeys(value); |
|
array_1.push(keys.json); |
|
var firstValueIndex_1 = array_1.length; |
|
keys.sorted.forEach(function (key) { |
|
array_1.push(_this.admit(value[key])); |
|
}); |
|
// Objects are looked up in the Trie by their prototype (which |
|
// is *not* recursively canonicalized), followed by a JSON |
|
// representation of their (sorted) keys, followed by the |
|
// sequence of recursively canonicalized values corresponding to |
|
// those keys. To keep the final results unambiguous with other |
|
// sequences (such as arrays that just happen to contain [proto, |
|
// keys.json, value1, value2, ...]), the known version of the |
|
// object is stored as node.object. |
|
var node = this.pool.lookupArray(array_1); |
|
if (!node.object) { |
|
var obj_1 = (node.object = Object.create(proto_1)); |
|
this.known.add(obj_1); |
|
keys.sorted.forEach(function (key, i) { |
|
obj_1[key] = array_1[firstValueIndex_1 + i]; |
|
}); |
|
// Since canonical objects may be shared widely between |
|
// unrelated consumers, it's important to regard them as |
|
// immutable, even if they are not frozen in production. |
|
if (globalThis.__DEV__ !== false) { |
|
Object.freeze(obj_1); |
|
} |
|
} |
|
return node.object; |
|
} |
|
} |
|
} |
|
return value; |
|
}; |
|
// It's worthwhile to cache the sorting of arrays of strings, since the |
|
// same initial unsorted arrays tend to be encountered many times. |
|
// Fortunately, we can reuse the Trie machinery to look up the sorted |
|
// arrays in linear time (which is faster than sorting large arrays). |
|
ObjectCanon.prototype.sortedKeys = function (obj) { |
|
var keys = Object.keys(obj); |
|
var node = this.pool.lookupArray(keys); |
|
if (!node.keys) { |
|
keys.sort(); |
|
var json = JSON.stringify(keys); |
|
if (!(node.keys = this.keysByJSON.get(json))) { |
|
this.keysByJSON.set(json, (node.keys = { sorted: keys, json: json })); |
|
} |
|
} |
|
return node.keys; |
|
}; |
|
return ObjectCanon; |
|
}()); |
|
export { ObjectCanon }; |
|
//# sourceMappingURL=object-canon.js.map
|