Skip to content

Commit

Permalink
Implement InMemoryCache#evict method.
Browse files Browse the repository at this point in the history
Eviction always succeeds if the given options.rootId is contained by the
cache, but it does not automatically trigger garbage collection, since the
developer might want to perform serveral evictions before triggering a
single garbage collection.

Resolves apollographql/apollo-feature-requests#4.
Supersedes #4681.
  • Loading branch information
benjamn committed Sep 11, 2019
1 parent 3e7eba1 commit 3977af1
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 5 deletions.
180 changes: 180 additions & 0 deletions packages/apollo-cache-inmemory/src/__tests__/entityCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,4 +569,184 @@ describe('EntityCache', () => {

expect(cache.gc()).toEqual([]);
});

it('allows cache eviction', () => {
const { cache, query } = newBookAuthorCache();

cache.writeQuery({
query,
data: {
book: {
__typename: "Book",
isbn: "031648637X",
title: "The Cuckoo's Calling",
author: {
__typename: "Author",
name: "Robert Galbraith",
},
},
},
});

expect(cache.evict({
rootId: "Author:J.K. Rowling",
query,
})).toEqual({
success: false,
});

const bookAuthorFragment = gql`
fragment BookAuthor on Book {
author {
name
}
}
`;

const fragmentResult = cache.readFragment({
id: "Book:031648637X",
fragment: bookAuthorFragment,
});

expect(fragmentResult).toEqual({
__typename: "Book",
author: {
__typename: "Author",
name: "Robert Galbraith",
},
});

cache.recordOptimisticTransaction(proxy => {
proxy.writeFragment({
id: "Book:031648637X",
fragment: bookAuthorFragment,
data: {
...fragmentResult,
author: {
__typename: "Author",
name: "J.K. Rowling",
},
},
});
}, "real name");

const snapshotWithBothNames = {
ROOT_QUERY: {
book: {
__ref: "Book:031648637X",
},
},
"Book:031648637X": {
__typename: "Book",
author: {
__ref: "Author:J.K. Rowling",
},
title: "The Cuckoo's Calling",
},
"Author:Robert Galbraith": {
__typename: "Author",
name: "Robert Galbraith",
},
"Author:J.K. Rowling": {
__typename: "Author",
name: "J.K. Rowling",
},
};

expect(cache.extract(true)).toEqual(snapshotWithBothNames);

expect(cache.gc()).toEqual([]);

expect(cache.retain('Author:Robert Galbraith')).toBe(1);

expect(cache.gc()).toEqual([]);

expect(cache.evict({
rootId: 'Author:Robert Galbraith',
query,
})).toEqual({
success: true,
});

expect(cache.gc()).toEqual([]);

cache.removeOptimistic("real name");

expect(cache.extract(true)).toEqual({
ROOT_QUERY: {
book: {
__ref: "Book:031648637X",
},
},
"Book:031648637X": {
__typename: "Book",
author: {
__ref: "Author:Robert Galbraith",
},
title: "The Cuckoo's Calling",
},
"Author:Robert Galbraith": {
__typename: "Author",
name: "Robert Galbraith",
},
});

cache.writeFragment({
id: "Book:031648637X",
fragment: bookAuthorFragment,
data: {
...fragmentResult,
author: {
__typename: "Author",
name: "J.K. Rowling",
},
},
});

expect(cache.extract(true)).toEqual(snapshotWithBothNames);

expect(cache.retain("Author:Robert Galbraith")).toBe(2);

expect(cache.gc()).toEqual([]);

expect(cache.release("Author:Robert Galbraith")).toBe(1);
expect(cache.release("Author:Robert Galbraith")).toBe(0);

expect(cache.gc()).toEqual([
"Author:Robert Galbraith",
]);

// If you're ever tempted to do this, you probably want to use cache.clear()
// instead, but evicting the ROOT_QUERY should work at least.
expect(cache.evict({
rootId: "ROOT_QUERY",
query,
})).toEqual({
success: true,
});

expect(cache.extract(true)).toEqual({
"Book:031648637X": {
__typename: "Book",
author: {
__ref: "Author:J.K. Rowling",
},
title: "The Cuckoo's Calling",
},
"Author:J.K. Rowling": {
__typename: "Author",
name: "J.K. Rowling",
},
});

// The book has been retained a couple of times since we've written it
// directly, but J.K. has never been directly written.
expect(cache.release("Book:031648637X")).toBe(1);
expect(cache.release("Book:031648637X")).toBe(0);

expect(cache.gc().sort()).toEqual([
"Author:J.K. Rowling",
"Book:031648637X",
]);
});
});
26 changes: 23 additions & 3 deletions packages/apollo-cache-inmemory/src/entityCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export abstract class EntityCache implements NormalizedCache {
return { ...this.data };
}

public has(dataId: string): boolean {
return hasOwn.call(this.data, dataId);
}

public get(dataId: string): StoreObject {
if (this.depend) this.depend(dataId);
return this.data[dataId]!;
Expand All @@ -60,9 +64,7 @@ export abstract class EntityCache implements NormalizedCache {
}

public delete(dataId: string): void {
if (this instanceof Layer) {
this.data[dataId] = void 0;
} else delete this.data[dataId];
delete this.data[dataId];
delete this.refs[dataId];
if (this.depend) this.depend.dirty(dataId);
}
Expand Down Expand Up @@ -252,6 +254,24 @@ class Layer extends EntityCache {
};
}

public has(dataId: string): boolean {
// Because the Layer implementation of the delete method uses void 0 to
// indicate absence, that's what we need to check for here, rather than
// calling super.has(dataId).
if (hasOwn.call(this.data, dataId) && this.data[dataId] === void 0) {
return false;
}
return this.parent.has(dataId);
}

public delete(dataId: string): void {
super.delete(dataId);
// In case this.parent (or one of its ancestors) has an entry for this ID,
// we need to shadow it with an undefined value, or it might be inherited
// by the Layer#get method.
this.data[dataId] = void 0;
}

// All the other inherited accessor methods work as-is, but the get method
// needs to fall back to this.parent.get when accessing a missing dataId.
public get(dataId: string): StoreObject {
Expand Down
10 changes: 8 additions & 2 deletions packages/apollo-cache-inmemory/src/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { StoreReader } from './readFromStore';
import { StoreWriter } from './writeToStore';
import { EntityCache, supportsResultCaching } from './entityCache';
import { KeyTrie } from 'optimism';
import { InvariantError } from 'ts-invariant';

export interface InMemoryCacheConfig extends ApolloReducerConfig {
resultCaching?: boolean;
Expand Down Expand Up @@ -202,7 +201,14 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
}

public evict(query: Cache.EvictOptions): Cache.EvictionResult {
throw new InvariantError(`eviction is not implemented on InMemory Cache`);
if (this.optimisticData.has(query.rootId)) {
// Note that this deletion does not trigger a garbage collection, which
// is convenient in cases where you want to evict multiple entities before
// performing a single garbage collection.
this.optimisticData.delete(query.rootId);
return { success: !this.optimisticData.has(query.rootId) };
}
return { success: false };
}

public reset(): Promise<void> {
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-cache-inmemory/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export declare type IdGetter = (
* StoreObjects from the cache
*/
export interface NormalizedCache {
has(dataId: string): boolean;
get(dataId: string): StoreObject;
set(dataId: string, value: StoreObject): void;
delete(dataId: string): void;
Expand Down

0 comments on commit 3977af1

Please sign in to comment.