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
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Array [
"MissingFieldError",
"Policies",
"cacheSlot",
"canonicalStringify",
"defaultDataIdFromObject",
"fieldNameFromStoreName",
"isReference",
Expand Down
4 changes: 4 additions & 0 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ export {
Policies,
} from './inmemory/policies';

export {
canonicalStringify,
} from './inmemory/object-canon';

export * from './inmemory/types';
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 { canonicalStringify } 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() {
canonicalStringify.reset();
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();
canonicalStringify.reset();
return Promise.resolve();
}

Expand Down
34 changes: 31 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,31 @@ type SortedKeysInfo = {
sorted: string[];
json: 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 const canonicalStringify = Object.assign(function (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);
}, {
reset() {
stringifyCanon = new ObjectCanon;
},
});

// Can be reset by calling canonicalStringify.reset().
let stringifyCanon = new ObjectCanon;

// Needs no resetting, thanks to weakness.
const stringifyCache = new WeakMap<object, string>();
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
29 changes: 25 additions & 4 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 @@ -177,7 +176,7 @@ const KNOWN_DIRECTIVES: string[] = [
'export',
];

export function getStoreKeyName(
export const getStoreKeyName = Object.assign(function (
fieldName: string,
args?: Record<string, any> | null,
directives?: Directives,
Expand All @@ -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,14 +223,36 @@ 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}`;
}
});
}

return completeFieldName;
}, {
setStringify(s: typeof stringify) {
const previous = stringify;
stringify = s;
return previous;
},
});

// Default stable JSON.stringify implementation. Can be updated/replaced with
// something better by calling getStoreKeyName.setStringify.
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;
}, {} as Record<string, any>);
}
return value;
}

export function argumentsObjectFromField(
Expand Down