Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow silencing broadcast for cache update methods. #6288

Merged
merged 3 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Allow calling cache.evict with Cache.EvictOptions.
  • Loading branch information
benjamn committed May 18, 2020
commit e84a603ce1e34c54ce5c306e5b324b6776b46a6a
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