Skip to content

Commit

Permalink
Allow calling cache.evict with Cache.EvictOptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamn committed May 18, 2020
1 parent 7429d9d commit e84a603
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/cache/core/__tests__/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class TestCache extends ApolloCache<unknown> {
return {};
}

public evict(dataId: string, fieldName?: string): boolean {
public evict(): boolean {
return false;
}

Expand Down
18 changes: 14 additions & 4 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
public abstract watch(watch: Cache.WatchOptions): () => void;
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;
// Remove whole objects from the cache by passing just options.id, or
// specific fields by passing options.field and/or options.args. If no
// options.args are provided, all fields matching options.field (even
// those with arguments) will be removed. Returns true iff any data was
// removed from the cache.
public abstract evict(options: Cache.EvictOptions): boolean;

// For backwards compatibility, evict can also take positional
// arguments. Please prefer the Cache.EvictOptions style (above).
public abstract evict(
id: string,
field?: string,
args?: Record<string, any>,
): boolean;

// intializer / offline / ssr API
/**
Expand Down
6 changes: 6 additions & 0 deletions src/cache/core/types/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export namespace Cache {
callback: WatchCallback;
}

export interface EvictOptions {
id: string;
fieldName?: string;
args?: Record<string, any>;
}

export import DiffResult = DataProxy.DiffResult;
export import WriteQueryOptions = DataProxy.WriteQueryOptions;
export import WriteFragmentOptions = DataProxy.WriteFragmentOptions;
Expand Down
182 changes: 168 additions & 14 deletions src/cache/inmemory/__tests__/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,13 +925,13 @@ describe('EntityStore', () => {
publisherOfBook: MelvilleData,
});

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

expect(cache.extract()).toEqual({
ROOT_QUERY: {
Expand All @@ -951,10 +951,12 @@ describe('EntityStore', () => {
// Nothing to garbage collect yet.
expect(cache.gc()).toEqual([]);

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

expect(cache.extract()).toEqual({
ROOT_QUERY: {
Expand All @@ -970,7 +972,7 @@ describe('EntityStore', () => {
// Melville House has been removed
});

cache.evict("ROOT_QUERY", "publisherOfBook");
cache.evict({ id: "ROOT_QUERY", fieldName: "publisherOfBook" });

function withoutPublisherOfBook(obj: Record<string, any>) {
const clean = { ...obj };
Expand Down Expand Up @@ -1049,10 +1051,10 @@ describe('EntityStore', () => {
name: "Ted Chiang",
};

cache.evict(
cache.identify(tedWithoutHobby)!,
"hobby",
);
cache.evict({
id: cache.identify(tedWithoutHobby)!,
fieldName: "hobby",
});

expect(cache.diff<any>({
query,
Expand Down Expand Up @@ -1082,7 +1084,7 @@ describe('EntityStore', () => {
],
});

cache.evict("ROOT_QUERY", "authorOfBook");
cache.evict({ id: "ROOT_QUERY", fieldName: "authorOfBook"});
expect(cache.gc().sort()).toEqual([
'Author:{"name":"Jenny Odell"}',
'Author:{"name":"Ted Chiang"}',
Expand Down Expand Up @@ -1232,6 +1234,158 @@ describe('EntityStore', () => {
});
});

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

const cache = new InMemoryCache();

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

const IsaacAsimovData = {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
};

const JamesCoreyData = {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
};

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

cache.writeQuery({
query,
data: {
authorOfBook: IsaacAsimovData,
},
variables: {
isbn: "2",
},
});

cache.writeQuery({
query,
data: {
authorOfBook: JamesCoreyData,
},
variables: {},
});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
"authorOfBook({\"isbn\":\"1\"})": {
__typename: "Author",
name: "Ted Chiang",
hobby: "video games",
},
"authorOfBook({\"isbn\":\"2\"})": {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
},
"authorOfBook({})": {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
}
},
});

cache.evict({
id: 'ROOT_QUERY',
fieldName: 'authorOfBook',
args: { isbn: "1" },
});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
"authorOfBook({\"isbn\":\"2\"})": {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
},
"authorOfBook({})": {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
}
},
});

cache.evict({
id: 'ROOT_QUERY',
fieldName: 'authorOfBook',
args: { isbn: '3' },
});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
"authorOfBook({\"isbn\":\"2\"})": {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
},
"authorOfBook({})": {
__typename: "Author",
name: "James S.A. Corey",
hobby: "tabletop games",
}
},
});

cache.evict({
id: 'ROOT_QUERY',
fieldName: 'authorOfBook',
args: {},
});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
"authorOfBook({\"isbn\":\"2\"})": {
__typename: "Author",
name: "Isaac Asimov",
hobby: "chemistry",
},
},
});

cache.evict({
id: 'ROOT_QUERY',
fieldName: 'authorOfBook',
});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
__typename: "Query",
},
});
});

it("supports cache.identify(object)", () => {
const queryWithAliases: DocumentNode = gql`
query {
Expand Down
17 changes: 7 additions & 10 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { canUseWeakMap } from '../../utilities/common/canUse';
import { NormalizedCache, NormalizedCacheObject } from './types';
import { fieldNameFromStoreName } from './helpers';
import { Policies } from './policies';
import { Modifier, Modifiers, SafeReadonly } from '../core/types/common';
import { Modifier, Modifiers, SafeReadonly } from '../core/types/common';
import { Cache } from '../core/types/Cache';

const hasOwn = Object.prototype.hasOwnProperty;

Expand Down Expand Up @@ -197,23 +198,19 @@ export abstract class EntityStore implements NormalizedCache {
return false;
}

public evict(
dataId: string,
fieldName?: string,
args?: Record<string, any>,
): boolean {
public evict(options: Cache.EvictOptions): boolean {
let evicted = false;
if (hasOwn.call(this.data, dataId)) {
evicted = this.delete(dataId, fieldName, args);
if (hasOwn.call(this.data, options.id)) {
evicted = this.delete(options.id, options.fieldName, options.args);
}
if (this instanceof Layer) {
evicted = this.parent.evict(dataId, fieldName, args) || evicted;
evicted = this.parent.evict(options) || evicted;
}
// Always invalidate the field to trigger rereading of watched
// queries, even if no cache data was modified by the eviction,
// because queries may depend on computed fields with custom read
// functions, whose values are not stored in the EntityStore.
this.group.dirty(dataId, fieldName || "__exists");
this.group.dirty(options.id, options.fieldName || "__exists");
return evicted;
}

Expand Down
10 changes: 8 additions & 2 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,17 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
}

public evict(
dataId: string,
idOrOptions: string | Cache.EvictOptions,
fieldName?: string,
args?: Record<string, any>,
): boolean {
const evicted = this.optimisticData.evict(dataId, fieldName, args);
const evicted = this.optimisticData.evict(
typeof idOrOptions === "string" ? {
id: idOrOptions,
fieldName,
args,
} : idOrOptions,
);
this.broadcastWatches();
return evicted;
}
Expand Down

0 comments on commit e84a603

Please sign in to comment.