Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove dependency on fast-json-stable-stringify #8222

Merged
merged 8 commits into from
May 18, 2021
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
"@wry/context": "^0.6.0",
"@wry/equality": "^0.4.0",
"@wry/trie": "^0.3.0",
"fast-json-stable-stringify": "^2.0.0",
"graphql-tag": "^2.12.3",
"hoist-non-react-statics": "^3.3.2",
"optimism": "^0.16.1",
Expand Down
3 changes: 3 additions & 0 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
TypePolicies,
} from './policies';
import { hasOwn } from './helpers';
import { resetStringifyCanon } from './object-canon';

export interface InMemoryCacheConfig extends ApolloReducerConfig {
resultCaching?: boolean;
Expand Down Expand Up @@ -262,6 +263,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {

// Request garbage collection of unreachable normalized entities.
public gc() {
resetStringifyCanon();
return this.optimisticData.gc();
}

Expand Down Expand Up @@ -322,6 +324,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
public reset(): Promise<void> {
this.init();
this.broadcastWatches();
resetStringifyCanon();
return Promise.resolve();
}

Expand Down
30 changes: 27 additions & 3 deletions src/cache/inmemory/object-canon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Trie } from "@wry/trie";
import { canUseWeakMap } from "../../utilities";
import { objToStr } from "./helpers";

function isObjectOrArray(value: any): boolean {
function isObjectOrArray(value: any): value is object {
return !!value && typeof value === "object";
}

Expand Down Expand Up @@ -109,7 +109,7 @@ export class ObjectCanon {
switch (objToStr.call(value)) {
case "[object Array]": {
if (this.known.has(value)) return value;
const array: any[] = value.map(this.admit, this);
const array: any[] = (value as any[]).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.
Expand All @@ -134,7 +134,7 @@ export class ObjectCanon {
array.push(keys.json);
const firstValueIndex = array.length;
keys.sorted.forEach(key => {
array.push(this.admit(value[key]));
array.push(this.admit((value as any)[key]));
});
// Objects are looked up in the Trie by their prototype (which
// is *not* recursively canonicalized), followed by a JSON
Expand Down Expand Up @@ -193,3 +193,27 @@ type SortedKeysInfo = {
sorted: string[];
json: string;
};

let stringifyCanon = new ObjectCanon;
export function resetStringifyCanon() {
stringifyCanon = new ObjectCanon;
}
const stringifyCache = new WeakMap<object, string>();

// Since the keys of canonical objects are always created in lexicographically
// sorted order, we can use the ObjectCanon to implement a fast and stable
// version of JSON.stringify, which automatically sorts object keys.
benjamn marked this conversation as resolved.
Show resolved Hide resolved
export function canonicalStringify(value: any): string {
if (isObjectOrArray(value)) {
const canonical = stringifyCanon.admit(value);
let json = stringifyCache.get(canonical);
if (json === void 0) {
stringifyCache.set(
canonical,
json = JSON.stringify(canonical),
);
}
return json;
}
return JSON.stringify(value);
}
6 changes: 6 additions & 0 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ import {
} from '../core/types/common';
import { WriteContext } from './writeToStore';

// Upgrade to a faster version of the default stable JSON.stringify function
// used by getStoreKeyName. This function is used when computing storeFieldName
// strings (when no keyArgs has been configured for a field).
import { canonicalStringify } from './object-canon';
getStoreKeyName.setStringify(canonicalStringify);

export type TypePolicies = {
[__typename: string]: TypePolicy;
}
Expand Down
27 changes: 24 additions & 3 deletions src/utilities/graphql/storeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
SelectionSetNode,
} from 'graphql';

import stringify from 'fast-json-stable-stringify';
import { InvariantError } from 'ts-invariant';
import { FragmentMap, getFragmentFromSelection } from './fragments';

Expand Down Expand Up @@ -202,7 +201,7 @@ export function getStoreKeyName(
filteredArgs[key] = args[key];
});

return `${directives['connection']['key']}(${JSON.stringify(
return `${directives['connection']['key']}(${stringify(
filteredArgs,
)})`;
} else {
Expand All @@ -224,7 +223,7 @@ export function getStoreKeyName(
Object.keys(directives).forEach(key => {
if (KNOWN_DIRECTIVES.indexOf(key) !== -1) return;
if (directives[key] && Object.keys(directives[key]).length) {
completeFieldName += `@${key}(${JSON.stringify(directives[key])})`;
completeFieldName += `@${key}(${stringify(directives[key])})`;
} else {
completeFieldName += `@${key}`;
}
Expand All @@ -234,6 +233,28 @@ export function getStoreKeyName(
return completeFieldName;
}

export namespace getStoreKeyName {
brainkim marked this conversation as resolved.
Show resolved Hide resolved
export function setStringify(s: typeof stringify) {
const previous = stringify;
stringify = s;
return previous;
}
}

let stringify = function defaultStringify(value: any): string {
benjamn marked this conversation as resolved.
Show resolved Hide resolved
return JSON.stringify(value, stringifyReplacer);
};

function stringifyReplacer(_key: string, value: any): any {
if (value && typeof value === "object" && !Array.isArray(value)) {
value = Object.keys(value).sort().reduce((copy, key) => {
copy[key] = value[key];
return copy;
}, Object.create(Object.getPrototypeOf(value)));
benjamn marked this conversation as resolved.
Show resolved Hide resolved
}
return value;
}

export function argumentsObjectFromField(
field: FieldNode | DirectiveNode,
variables?: Record<string, any>,
Expand Down