diff --git a/change/@graphitation-graphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json b/change/@graphitation-graphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json new file mode 100644 index 000000000..2375c81fa --- /dev/null +++ b/change/@graphitation-graphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "add document cache for graphql tag", + "packageName": "@graphitation/graphql-js-tag", + "email": "pavelglac@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/graphql-js-tag/src/__tests__/index.test.ts b/packages/graphql-js-tag/src/__tests__/index.test.ts index f411540d1..99fdfc0cf 100644 --- a/packages/graphql-js-tag/src/__tests__/index.test.ts +++ b/packages/graphql-js-tag/src/__tests__/index.test.ts @@ -63,4 +63,62 @@ describe(graphql, () => { const expected = SomeFragment.definitions[0]; expect(actual).toBe(expected); }); + + it("should return the same document for the same template literal", () => { + // This is important for Apollo Client 3.8+ which uses reference equality. + // When a component renders multiple times, the same template literal + // returns the same TemplateStringsArray, so we get the same cached document. + + const doc1 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + `; + const doc2 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + `; + expect(doc1).toBe(doc2); + }); + + it("should return different documents for fragment definitions with the same name but different content", () => { + const HackyFragment = graphql` + fragment SomeFragment on SomeType { + id + name + } + `; + const doc1 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + `; + const doc2 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${HackyFragment} + `; + expect(doc1).not.toBe(doc2); + }); + + it("should return the same document for the same template literal without any fragments", () => { + const SomeFragment1 = graphql` + fragment SomeFragment on SomeType { + id + name + } + `; + const SomeFragment2 = graphql` + fragment SomeFragment on SomeType { + id + name + } + `; + expect(SomeFragment1).toBe(SomeFragment2); + }); }); diff --git a/packages/graphql-js-tag/src/index.ts b/packages/graphql-js-tag/src/index.ts index 0ae4b857a..9ac1a6aaf 100644 --- a/packages/graphql-js-tag/src/index.ts +++ b/packages/graphql-js-tag/src/index.ts @@ -2,6 +2,11 @@ import { parse } from "graphql"; import type { DocumentNode } from "graphql"; import invariant from "invariant"; +// The similiar cache as Apollo Client's `graphql-tag` package. https://github.com/apollographql/graphql-tag/blob/main/src/index.ts#L10 +// A map docString -> array of cached entries with their fragments +type CacheEntry = { fragments: DocumentNode[]; result: DocumentNode }; +const docCache = new Map(); + /** * This tagged template function is used to capture a single GraphQL document, such as an operation or a fragment. When * a document refers to fragments, those should be interpolated as trailing components, but *no* other interpolation is @@ -49,10 +54,38 @@ export function graphql( document.map((s) => s.trim()).filter((s) => s.length > 0).length === 1, "Interpolations are only allowed at the end of the template.", ); + + const cacheEntries = docCache.get(document[0]); + + // We are also handling the case where the same document is used with different interpolated fragments. + if (cacheEntries) { + for (const entry of cacheEntries) { + const areReferencesEqual = + entry.fragments.length === fragments.length && + entry.fragments.every((frag, i) => frag === fragments[i]); + if (areReferencesEqual) { + return entry.result; + } + } + } + + // Parse and create new document const documentNode = parse(document[0], { noLocation: true }); const definitions = new Set(documentNode.definitions); fragments.forEach((doc) => doc.definitions.forEach((def) => definitions.add(def)), ); - return { kind: "Document", definitions: Array.from(definitions) }; + const result: DocumentNode = { + kind: "Document", + definitions: Array.from(definitions), + }; + + if (cacheEntries) { + // Operation was already cached, but with different fragment interpolations. Add a new cache entry for this combination of fragments. + cacheEntries.push({ fragments, result }); + } else { + docCache.set(document[0], [{ fragments, result }]); + } + + return result; }