Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "add document cache for graphql tag",
"packageName": "@graphitation/graphql-js-tag",
"email": "pavelglac@gmail.com",
"dependentChangeType": "patch"
}
58 changes: 58 additions & 0 deletions packages/graphql-js-tag/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
35 changes: 34 additions & 1 deletion packages/graphql-js-tag/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, CacheEntry[]>();

/**
* 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
Expand Down Expand Up @@ -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;
}