Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

(core) - Fix formatDocument edge case and add caching (#1186)

* Fix case where __typename isn't added when it's aliased

* Add changeset

* Add caching to formatDocument to avoid duplicate work

* Golf code size of collectTypesFromResponse

* Update preserve custom property test

* Replace KeyedDocumentNode cache with Map

Changed files
+73 -23
.changeset
packages
+5
.changeset/nice-rivers-run.md
···
+
---
+
'@urql/core': patch
+
---
+
+
Fix edge case in `formatDocument`, which fails to add a `__typename` field if it has been aliased to a different name.
+5
.changeset/serious-gorillas-develop.md
···
+
---
+
'@urql/core': patch
+
---
+
+
Cache results of `formatDocument` by the input document's key.
+21 -14
packages/core/src/utils/request.ts
···
import { stringifyVariables } from './stringifyVariables';
import { GraphQLRequest } from '../types';
-
interface Documents {
-
[key: number]: DocumentNode;
+
export interface KeyedDocumentNode extends DocumentNode {
+
__key: number;
}
const hashQuery = (q: string): number =>
hash(q.replace(/([\s,]|#[^\n\r]+)+/g, ' ').trim());
-
const docs: Documents = Object.create(null);
+
const docs = new Map<number, KeyedDocumentNode>();
-
export const createRequest = <Data = any, Variables = object>(
-
q: string | DocumentNode | TypedDocumentNode<Data, Variables>,
-
vars?: Variables
-
): GraphQLRequest<Data, Variables> => {
+
export const keyDocument = (
+
q: string | DocumentNode | TypedDocumentNode
+
): KeyedDocumentNode => {
let key: number;
let query: DocumentNode;
if (typeof q === 'string') {
key = hashQuery(q);
-
query =
-
docs[key] !== undefined ? docs[key] : parse(q, { noLocation: true });
-
} else if ((q as any).__key !== undefined) {
+
query = docs.get(key) || parse(q, { noLocation: true });
+
} else if ((q as any).__key != null) {
key = (q as any).__key;
query = q;
} else {
key = hashQuery(print(q));
-
query = docs[key] !== undefined ? docs[key] : q;
+
query = docs.get(key) || q;
}
-
docs[key] = query;
-
(query as any).__key = key;
+
(query as KeyedDocumentNode).__key = key;
+
docs.set(key, query as KeyedDocumentNode);
+
return query as KeyedDocumentNode;
+
};
+
export const createRequest = <Data = any, Variables = object>(
+
q: string | DocumentNode | TypedDocumentNode<Data, Variables>,
+
vars?: Variables
+
): GraphQLRequest<Data, Variables> => {
+
const query = keyDocument(q);
return {
-
key: vars ? phash(key, stringifyVariables(vars)) >>> 0 : key,
+
key: vars
+
? phash(query.__key, stringifyVariables(vars)) >>> 0
+
: query.__key,
query,
variables: vars || ({} as Variables),
};
+21 -1
packages/core/src/utils/typenames.test.ts
···
});
it('preserves custom properties', () => {
-
const doc = parse(`{ id todos { id } }`) as any;
+
const doc = parse(`{ todos { id } }`) as any;
doc.documentId = '123';
expect((formatDocument(doc) as any).documentId).toBe(doc.documentId);
});
···
"{
todos {
id
+
__typename
+
}
+
}
+
"
+
`);
+
});
+
+
it('does add typenames when it is aliased', () => {
+
expect(
+
formatTypeNames(`{
+
todos {
+
id
+
typename: __typename
+
}
+
}`)
+
).toMatchInlineSnapshot(`
+
"{
+
todos {
+
id
+
typename: __typename
__typename
}
}
+21 -8
packages/core/src/utils/typenames.ts
···
visit,
} from 'graphql';
+
import { KeyedDocumentNode, keyDocument } from './request';
+
interface EntityLike {
[key: string]: EntityLike | EntityLike[] | any;
__typename: string | null | void;
···
if (
node.selectionSet &&
!node.selectionSet.selections.some(
-
node => node.kind === Kind.FIELD && node.name.value === '__typename'
+
node =>
+
node.kind === Kind.FIELD &&
+
node.name.value === '__typename' &&
+
!node.alias
)
) {
return {
···
}
};
+
const formattedDocs = new Map<number, KeyedDocumentNode>();
+
export const formatDocument = <T extends DocumentNode>(node: T): T => {
-
const result = visit(node, {
-
Field: formatNode,
-
InlineFragment: formatNode,
-
});
+
const query = keyDocument(node);
+
+
let result = formattedDocs.get(query.__key);
+
if (!result) {
+
result = visit(query, {
+
Field: formatNode,
+
InlineFragment: formatNode,
+
}) as KeyedDocumentNode;
+
// Ensure that the hash of the resulting document won't suddenly change
+
result.__key = query.__key;
+
formattedDocs.set(query.__key, result);
+
}
-
// Ensure that the hash of the resulting document won't suddenly change
-
result.__key = (node as any).__key;
-
return result;
+
return (result as unknown) as T;
};