Skip to content

Commit

Permalink
Support indirect subtype chains in possibleTypes.
Browse files Browse the repository at this point in the history
In principle, the subtypes found in the possibleTypes map could have
subtypes of their own! Instead of ignoring this possibility, we should
embrace it fully and efficiently.

Subtype indirection is not necessarily a mistake, since defining an
abstract type once and then referring to it by name in multiple other
places tends to be more space-efficient than inlining all the concrete
subtypes into every possibleTypes array.
  • Loading branch information
benjamn committed Jul 18, 2019
1 parent 450107a commit 2b7c13e
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 2 deletions.
48 changes: 48 additions & 0 deletions packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,54 @@ describe('fragment matching', () => {
expect(cache.readQuery({ query })).toEqual(data);
});

it('can match indirect subtypes while avoiding cycles', () => {
const cache = new InMemoryCache({
addTypename: true,
possibleTypes: {
Animal: ['Animal', 'Bug', 'Mammal'],
Bug: ['Ant', 'Spider', 'RolyPoly'],
Mammal: ['Dog', 'Cat', 'Human'],
Cat: ['Calico', 'Siamese', 'Sphynx', 'Tabby'],
},
});

const query = gql`
query {
animals {
... on Mammal {
hasFur
bodyTemperature
}
... on Bug {
isVenomous
}
}
}
`;

const data = {
animals: [
{
__typename: 'Sphynx',
hasFur: false,
bodyTemperature: 99,
},
{
__typename: 'Dog',
hasFur: true,
bodyTemperature: 102,
},
{
__typename: 'Spider',
isVenomous: 'maybe',
},
],
};

cache.writeQuery({ query, data });
expect(cache.readQuery({ query })).toEqual(data);
});

it('can match against the root Query', () => {
const cache = new InMemoryCache({
addTypename: true,
Expand Down
22 changes: 20 additions & 2 deletions packages/apollo-cache-inmemory/src/fragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,26 @@ export function fragmentMatches(
}

if (possibleTypes) {
const subtypes = possibleTypes[typeCondition];
return !!subtypes && subtypes.indexOf(typename) >= 0;
const workQueue = [possibleTypes[typeCondition]];
// It's important to keep evaluating workQueue.length each time through the
// loop, because the queue can grow while we're iterating over it.
for (let i = 0; i < workQueue.length; ++i) {
const subtypes = workQueue[i];
if (subtypes) {
if (subtypes.indexOf(typename) >= 0) return true;
for (let { length } = subtypes, j = 0; j < length; ++j) {
const subsubtypes = possibleTypes[subtypes[j]];
// If this subtype has subtypes of its own, and we haven't considered
// this array of sub-subtypes before, add them the queue.
if (subsubtypes && workQueue.indexOf(subsubtypes) < 0) {
workQueue.push(subsubtypes);
}
}
}
}
// When possibleTypes is defined, we always either return true from the loop
// above or return false here (never 'heuristic' below).
return false;
}

return 'heuristic';
Expand Down

0 comments on commit 2b7c13e

Please sign in to comment.