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
4 months ago
|
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
|