Skip to content

Commit

Permalink
Support eviction of specific entity fields.
Browse files Browse the repository at this point in the history
Evicting an entire entity object is often overkill, when all you really
want is to invalidate and refetch specific fields within the entity. This
observation is especially true for the ROOT_QUERY object, which should
never be evicted in its entirety, but whose individual fields often become
stale or need to be recomputed.

A critical nuance here is that fields are evicted according to their
field.name.value, rather than a specific storeFieldName, since it doesn't
make a whole lot of sense to evict the field value associated with a
specific set of arguments. Instead, calling cache.evict(id, fieldName)
will evict *all* values for that field, regardless of the arguments.
  • Loading branch information
benjamn committed Dec 3, 2019
1 parent acd2562 commit d508de6
Show file tree
Hide file tree
Showing 5 changed files with 384 additions and 21 deletions.
6 changes: 5 additions & 1 deletion src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
): void;
public abstract diff<T>(query: Cache.DiffOptions): Cache.DiffResult<T>;
public abstract watch(watch: Cache.WatchOptions): () => void;
public abstract evict(dataId: string): boolean;
public abstract reset(): Promise<void>;

// If called with only one argument, removes the entire entity
// identified by dataId. If called with a fieldName as well, removes all
// fields of the identified entity whose store names match fieldName.
public abstract evict(dataId: string, fieldName?: string): boolean;

// intializer / offline / ssr API
/**
* Replaces existing state in the cache (if any) with the values expressed by
Expand Down
299 changes: 299 additions & 0 deletions src/cache/inmemory/__tests__/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,305 @@ describe('EntityStore', () => {
]);
});

it("allows evicting specific fields", () => {
const query: DocumentNode = gql`
query {
authorOfBook(isbn: $isbn) {
name
hobby
}
publisherOfBook(isbn: $isbn) {
name
yearOfFounding
}
}
`;

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
authorOfBook: {
keyArgs: ["isbn"],
},
},
},
Author: {
keyFields: ["name"],
},
Publisher: {
keyFields: ["name"],
},
},
});

const TedChiangData = {
__typename: "Author",
name: "Ted Chiang",
hobby: "video games",
};

const KnopfData = {
__typename: "Publisher",
name: "Alfred A. Knopf",
yearOfFounding: 1915,
};

cache.writeQuery({
query,
data: {
authorOfBook: TedChiangData,
publisherOfBook: KnopfData,
},
variables: {
isbn: "1529014514",
},
});

const justTedRootQueryData = {
__typename: "Query",
'authorOfBook:{"isbn":"1529014514"}': {
__ref: 'Author:{"name":"Ted Chiang"}',
},
// This storeFieldName format differs slightly from that of
// authorOfBook because we did not define keyArgs for the
// publisherOfBook field, so the legacy storeKeyNameFromField
// function was used instead.
'publisherOfBook({"isbn":"1529014514"})': {
__ref: 'Publisher:{"name":"Alfred A. Knopf"}',
},
};

expect(cache.extract()).toEqual({
ROOT_QUERY: justTedRootQueryData,
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': KnopfData,
});

const JennyOdellData = {
__typename: "Author",
name: "Jenny Odell",
hobby: "birding",
};

const MelvilleData = {
__typename: "Publisher",
name: "Melville House",
yearOfFounding: 2001,
};

cache.writeQuery({
query,
data: {
authorOfBook: JennyOdellData,
publisherOfBook: MelvilleData,
},
variables: {
isbn: "1760641790",
},
});

const justJennyRootQueryData = {
__typename: "Query",
'authorOfBook:{"isbn":"1760641790"}': {
__ref: 'Author:{"name":"Jenny Odell"}',
},
'publisherOfBook({"isbn":"1760641790"})': {
__ref: 'Publisher:{"name":"Melville House"}',
},
};

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...justTedRootQueryData,
...justJennyRootQueryData,
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': KnopfData,
'Author:{"name":"Jenny Odell"}': JennyOdellData,
'Publisher:{"name":"Melville House"}': MelvilleData,
});

const fullTedResult = cache.readQuery<any>({
query,
variables: {
isbn: "1529014514",
},
});

expect(fullTedResult).toEqual({
authorOfBook: TedChiangData,
publisherOfBook: KnopfData,
});

const fullJennyResult = cache.readQuery<any>({
query,
variables: {
isbn: "1760641790",
},
});

expect(fullJennyResult).toEqual({
authorOfBook: JennyOdellData,
publisherOfBook: MelvilleData,
});

cache.evict(
cache.identify({
__typename: "Publisher",
name: "Alfred A. Knopf",
}),
"yearOfFounding",
);

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...justTedRootQueryData,
...justJennyRootQueryData,
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': {
__typename: "Publisher",
name: "Alfred A. Knopf",
// yearOfFounding has been removed
},
'Author:{"name":"Jenny Odell"}': JennyOdellData,
'Publisher:{"name":"Melville House"}': MelvilleData,
});

// Nothing to garbage collect yet.
expect(cache.gc()).toEqual([]);

cache.evict(cache.identify({
__typename: "Publisher",
name: "Melville House",
}));

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...justTedRootQueryData,
...justJennyRootQueryData,
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': {
__typename: "Publisher",
name: "Alfred A. Knopf",
},
'Author:{"name":"Jenny Odell"}': JennyOdellData,
// Melville House has been removed
});

cache.evict("ROOT_QUERY", "publisherOfBook");

function withoutPublisherOfBook(obj: object) {
const clean = { ...obj };
Object.keys(obj).forEach(key => {
if (key.startsWith("publisherOfBook")) {
delete clean[key];
}
});
return clean;
}

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...withoutPublisherOfBook(justTedRootQueryData),
...withoutPublisherOfBook(justJennyRootQueryData),
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Publisher:{"name":"Alfred A. Knopf"}': {
__typename: "Publisher",
name: "Alfred A. Knopf",
},
'Author:{"name":"Jenny Odell"}': JennyOdellData,
});

expect(cache.gc()).toEqual([
'Publisher:{"name":"Alfred A. Knopf"}',
]);

expect(cache.extract()).toEqual({
ROOT_QUERY: {
...withoutPublisherOfBook(justTedRootQueryData),
...withoutPublisherOfBook(justJennyRootQueryData),
},
'Author:{"name":"Ted Chiang"}': TedChiangData,
'Author:{"name":"Jenny Odell"}': JennyOdellData,
});

const partialTedResult = cache.diff<any>({
query,
returnPartialData: true,
optimistic: false, // required but not important
variables: {
isbn: "1529014514",
},
});
expect(partialTedResult.complete).toBe(false);
expect(partialTedResult.result).toEqual({
authorOfBook: TedChiangData,
});
// The result caching system preserves the referential identity of
// unchanged nested result objects.
expect(
partialTedResult.result.authorOfBook,
).toBe(fullTedResult.authorOfBook);

const partialJennyResult = cache.diff<any>({
query,
returnPartialData: true,
optimistic: true, // required but not important
variables: {
isbn: "1760641790",
},
});
expect(partialJennyResult.complete).toBe(false);
expect(partialJennyResult.result).toEqual({
authorOfBook: JennyOdellData,
});
// The result caching system preserves the referential identity of
// unchanged nested result objects.
expect(
partialJennyResult.result.authorOfBook,
).toBe(fullJennyResult.authorOfBook);

const tedWithoutHobby = {
__typename: "Author",
name: "Ted Chiang",
};

cache.evict(
cache.identify(tedWithoutHobby),
"hobby",
);

expect(cache.diff<any>({
query,
returnPartialData: true,
optimistic: false, // required but not important
variables: {
isbn: "1529014514",
},
})).toEqual({
complete: false,
result: {
authorOfBook: tedWithoutHobby,
},
});

cache.evict("ROOT_QUERY", "authorOfBook");
expect(cache.gc().sort()).toEqual([
'Author:{"name":"Jenny Odell"}',
'Author:{"name":"Ted Chiang"}',
]);
expect(cache.extract()).toEqual({
ROOT_QUERY: {
// Everything else has been removed.
__typename: "Query",
},
});
});

it("supports cache.identify(object)", () => {
const queryWithAliases: DocumentNode = gql`
query {
Expand Down
Loading

0 comments on commit d508de6

Please sign in to comment.