@apollo/client
Version:
A fully-featured caching GraphQL client.
109 lines • 5.19 kB
JavaScript
import { __DEV__ } from "@apollo/client/utilities/environment";
import { cacheSizes } from "../caching/sizes.js";
import { AutoCleanedStrongCache } from "./caches.js";
import { registerGlobalCache } from "./getMemoryInternals.js";
/**
* Serializes a value to JSON with object keys in a consistent, sorted order.
*
* @remarks
*
* Unlike `JSON.stringify()`, this function ensures that object keys are always
* serialized in the same alphabetical order, regardless of their original order.
* This makes it suitable for creating consistent cache keys from objects,
* comparing objects by their serialized representation, or generating
* deterministic hashes of objects.
*
* To achieve performant sorting, this function uses a `Map` from JSON-serialized
* arrays of keys (in any order) to sorted arrays of the same keys, with a
* single sorted array reference shared by all permutations of the keys.
*
* As a drawback, this function will add a little more memory for every object
* encountered that has different (more, less, a different order of) keys than
* in the past.
*
* In a typical application, this extra memory usage should not play a
* significant role, as `canonicalStringify` will be called for only a limited
* number of object shapes, and the cache will not grow beyond a certain point.
* But in some edge cases, this could be a problem. Use canonicalStringify.reset()
* as a way to clear the memoization cache.
*
* @param value - The value to stringify
* @returns JSON string with consistently ordered object keys
*
* @example
*
* ```ts
* import { canonicalStringify } from "@apollo/client/utilities";
*
* const obj1 = { b: 2, a: 1 };
* const obj2 = { a: 1, b: 2 };
*
* console.log(canonicalStringify(obj1)); // '{"a":1,"b":2}'
* console.log(canonicalStringify(obj2)); // '{"a":1,"b":2}'
* ```
*/
export const canonicalStringify = Object.assign(function canonicalStringify(value) {
return JSON.stringify(value, stableObjectReplacer);
}, {
reset() {
// Clearing the sortingMap will reclaim all cached memory, without
// affecting the logical results of canonicalStringify, but potentially
// sacrificing performance until the cache is refilled.
sortingMap = new AutoCleanedStrongCache(cacheSizes.canonicalStringify || 1000 /* defaultCacheSizes.canonicalStringify */);
},
});
if (__DEV__) {
registerGlobalCache("canonicalStringify", () => sortingMap.size);
}
// Values are JSON-serialized arrays of object keys (in any order), and values
// are sorted arrays of the same keys.
let sortingMap;
canonicalStringify.reset();
// The JSON.stringify function takes an optional second argument called a
// replacer function. This function is called for each key-value pair in the
// object being stringified, and its return value is used instead of the
// original value. If the replacer function returns a new value, that value is
// stringified as JSON instead of the original value of the property.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
function stableObjectReplacer(key, value) {
if (value && typeof value === "object") {
const proto = Object.getPrototypeOf(value);
// We don't want to mess with objects that are not "plain" objects, which
// means their prototype is either Object.prototype or null. This check also
// prevents needlessly rearranging the indices of arrays.
if (proto === Object.prototype || proto === null) {
const keys = Object.keys(value);
// If keys is already sorted, let JSON.stringify serialize the original
// value instead of creating a new object with keys in the same order.
if (keys.every(everyKeyInOrder))
return value;
const unsortedKey = JSON.stringify(keys);
let sortedKeys = sortingMap.get(unsortedKey);
if (!sortedKeys) {
keys.sort();
const sortedKey = JSON.stringify(keys);
// Checking for sortedKey in the sortingMap allows us to share the same
// sorted array reference for all permutations of the same set of keys.
sortedKeys = sortingMap.get(sortedKey) || keys;
sortingMap.set(unsortedKey, sortedKeys);
sortingMap.set(sortedKey, sortedKeys);
}
const sortedObject = Object.create(proto);
// Reassigning the keys in sorted order will cause JSON.stringify to
// serialize them in sorted order.
sortedKeys.forEach((key) => {
sortedObject[key] = value[key];
});
return sortedObject;
}
}
return value;
}
// Since everything that happens in stableObjectReplacer benefits from being as
// efficient as possible, we use a static function as the callback for
// keys.every in order to test if the provided keys are already sorted without
// allocating extra memory for a callback.
function everyKeyInOrder(key, i, keys) {
return i === 0 || keys[i - 1] <= key;
}
//# sourceMappingURL=canonicalStringify.js.map