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

Configure Feed

Select the types of activity you want to include in your feed.

(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

authored by

Phil Pluckthun and committed by
GitHub
564ed1b7 b608a41b

+73 -23
+5
.changeset/nice-rivers-run.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + 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
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Cache results of `formatDocument` by the input document's key.
+21 -14
packages/core/src/utils/request.ts
··· 4 4 import { stringifyVariables } from './stringifyVariables'; 5 5 import { GraphQLRequest } from '../types'; 6 6 7 - interface Documents { 8 - [key: number]: DocumentNode; 7 + export interface KeyedDocumentNode extends DocumentNode { 8 + __key: number; 9 9 } 10 10 11 11 const hashQuery = (q: string): number => 12 12 hash(q.replace(/([\s,]|#[^\n\r]+)+/g, ' ').trim()); 13 13 14 - const docs: Documents = Object.create(null); 14 + const docs = new Map<number, KeyedDocumentNode>(); 15 15 16 - export const createRequest = <Data = any, Variables = object>( 17 - q: string | DocumentNode | TypedDocumentNode<Data, Variables>, 18 - vars?: Variables 19 - ): GraphQLRequest<Data, Variables> => { 16 + export const keyDocument = ( 17 + q: string | DocumentNode | TypedDocumentNode 18 + ): KeyedDocumentNode => { 20 19 let key: number; 21 20 let query: DocumentNode; 22 21 if (typeof q === 'string') { 23 22 key = hashQuery(q); 24 - query = 25 - docs[key] !== undefined ? docs[key] : parse(q, { noLocation: true }); 26 - } else if ((q as any).__key !== undefined) { 23 + query = docs.get(key) || parse(q, { noLocation: true }); 24 + } else if ((q as any).__key != null) { 27 25 key = (q as any).__key; 28 26 query = q; 29 27 } else { 30 28 key = hashQuery(print(q)); 31 - query = docs[key] !== undefined ? docs[key] : q; 29 + query = docs.get(key) || q; 32 30 } 33 31 34 - docs[key] = query; 35 - (query as any).__key = key; 32 + (query as KeyedDocumentNode).__key = key; 33 + docs.set(key, query as KeyedDocumentNode); 34 + return query as KeyedDocumentNode; 35 + }; 36 36 37 + export const createRequest = <Data = any, Variables = object>( 38 + q: string | DocumentNode | TypedDocumentNode<Data, Variables>, 39 + vars?: Variables 40 + ): GraphQLRequest<Data, Variables> => { 41 + const query = keyDocument(q); 37 42 return { 38 - key: vars ? phash(key, stringifyVariables(vars)) >>> 0 : key, 43 + key: vars 44 + ? phash(query.__key, stringifyVariables(vars)) >>> 0 45 + : query.__key, 39 46 query, 40 47 variables: vars || ({} as Variables), 41 48 };
+21 -1
packages/core/src/utils/typenames.test.ts
··· 50 50 }); 51 51 52 52 it('preserves custom properties', () => { 53 - const doc = parse(`{ id todos { id } }`) as any; 53 + const doc = parse(`{ todos { id } }`) as any; 54 54 doc.documentId = '123'; 55 55 expect((formatDocument(doc) as any).documentId).toBe(doc.documentId); 56 56 }); ··· 79 79 "{ 80 80 todos { 81 81 id 82 + __typename 83 + } 84 + } 85 + " 86 + `); 87 + }); 88 + 89 + it('does add typenames when it is aliased', () => { 90 + expect( 91 + formatTypeNames(`{ 92 + todos { 93 + id 94 + typename: __typename 95 + } 96 + }`) 97 + ).toMatchInlineSnapshot(` 98 + "{ 99 + todos { 100 + id 101 + typename: __typename 82 102 __typename 83 103 } 84 104 }
+21 -8
packages/core/src/utils/typenames.ts
··· 7 7 visit, 8 8 } from 'graphql'; 9 9 10 + import { KeyedDocumentNode, keyDocument } from './request'; 11 + 10 12 interface EntityLike { 11 13 [key: string]: EntityLike | EntityLike[] | any; 12 14 __typename: string | null | void; ··· 38 40 if ( 39 41 node.selectionSet && 40 42 !node.selectionSet.selections.some( 41 - node => node.kind === Kind.FIELD && node.name.value === '__typename' 43 + node => 44 + node.kind === Kind.FIELD && 45 + node.name.value === '__typename' && 46 + !node.alias 42 47 ) 43 48 ) { 44 49 return { ··· 60 65 } 61 66 }; 62 67 68 + const formattedDocs = new Map<number, KeyedDocumentNode>(); 69 + 63 70 export const formatDocument = <T extends DocumentNode>(node: T): T => { 64 - const result = visit(node, { 65 - Field: formatNode, 66 - InlineFragment: formatNode, 67 - }); 71 + const query = keyDocument(node); 72 + 73 + let result = formattedDocs.get(query.__key); 74 + if (!result) { 75 + result = visit(query, { 76 + Field: formatNode, 77 + InlineFragment: formatNode, 78 + }) as KeyedDocumentNode; 79 + // Ensure that the hash of the resulting document won't suddenly change 80 + result.__key = query.__key; 81 + formattedDocs.set(query.__key, result); 82 + } 68 83 69 - // Ensure that the hash of the resulting document won't suddenly change 70 - result.__key = (node as any).__key; 71 - return result; 84 + return (result as unknown) as T; 72 85 };