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