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

Require Cache.EvictOptions when calling cache.evict. #6364

Merged
merged 3 commits into from
May 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@
- `InMemoryCache` now supports tracing garbage collection and eviction. Note that the signature of the `evict` method has been simplified in a potentially backwards-incompatible way. <br/>
[@benjamn](https://github.com/benjamn) in [#5310](https://github.com/apollographql/apollo-client/pull/5310)

- The `cache.evict` method can optionally take an arguments object as its third parameter (following the entity ID and field name), to delete only those field values with specific arguments. <br/>
- **[beta-BREAKING]** Please note that the `cache.evict` method now requires `Cache.EvictOptions`, though it previously supported positional arguments as well. <br/>
[@danReynolds](https://github.com/danReynolds) in [#6141](https://github.com/apollographql/apollo-client/pull/6141)
[@benjamn](https://github.com/benjamn) in [#6364](https://github.com/apollographql/apollo-client/pull/6364)

- Cache methods that would normally trigger a broadcast, like `cache.evict`, `cache.writeQuery`, and `cache.writeFragment`, can now be called with a named options object, which supports a `broadcast: boolean` property that can be used to silence the broadcast, for situations where you want to update the cache multiple times without triggering a broadcast each time. <br/>
[@benjamn](https://github.com/benjamn) in [#6288](https://github.com/apollographql/apollo-client/pull/6288)
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2913,7 +2913,7 @@ describe('@connection', () => {
expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez);

// Now invalidate the ROOT_QUERY.a field.
client.cache.evict("ROOT_QUERY", "a");
client.cache.evict({ fieldName: "a" });
await wait();

// The results are structurally the same, but the result objects have
Expand Down Expand Up @@ -2957,7 +2957,7 @@ describe('@connection', () => {
checkLastResult(abResults, a456bOyez);
checkLastResult(cResults, { c: "saw" });

client.cache.evict("ROOT_QUERY", "c");
client.cache.evict({ fieldName: "c" });
await wait();

checkLastResult(aResults, a456);
Expand Down
8 changes: 0 additions & 8 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,6 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
// 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;
Comment on lines -31 to -37
Copy link
Member Author

@benjamn benjamn May 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was a bit of a tell, in hindsight.


// intializer / offline / ssr API
/**
* Replaces existing state in the cache (if any) with the values expressed by
Expand Down
2 changes: 1 addition & 1 deletion src/cache/core/types/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export namespace Cache {
}

export interface EvictOptions {
id: string;
id?: string;
fieldName?: string;
args?: Record<string, any>;
broadcast?: boolean;
Expand Down
65 changes: 48 additions & 17 deletions src/cache/inmemory/__tests__/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ describe('EntityStore', () => {
},
});

expect(cache.evict("Author:J.K. Rowling")).toBe(false);
expect(cache.evict({ id: "Author:J.K. Rowling" })).toBe(false);

const bookAuthorFragment = gql`
fragment BookAuthor on Book {
Expand Down Expand Up @@ -689,7 +689,7 @@ describe('EntityStore', () => {

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

expect(cache.evict("Author:Robert Galbraith")).toBe(true);
expect(cache.evict({ id: "Author:Robert Galbraith" })).toBe(true);

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

Expand Down Expand Up @@ -754,9 +754,27 @@ describe('EntityStore', () => {

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

// 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("ROOT_QUERY")).toBe(true);
function checkFalsyEvictId(id: any) {
expect(id).toBeFalsy();
expect(cache.evict({
// Accidentally passing a falsy/undefined options.id to
// cache.evict (perhaps because cache.identify failed) should
// *not* cause the ROOT_QUERY object to be evicted! In order for
// cache.evict to default to ROOT_QUERY, the options.id property
// must be *absent* (not just undefined).
id,
})).toBe(false);
}
checkFalsyEvictId(void 0);
checkFalsyEvictId(null);
checkFalsyEvictId(false);
checkFalsyEvictId(0);
checkFalsyEvictId("");

// In other words, this is how you evict the entire ROOT_QUERY
// object. If you're ever tempted to do this, you probably want to use
// cache.clear() instead, but evicting the ROOT_QUERY should work.
expect(cache.evict({})).toBe(true);

expect(cache.extract(true)).toEqual({
"Book:031648637X": {
Expand Down Expand Up @@ -1177,7 +1195,10 @@ describe('EntityStore', () => {
},
});

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

expect(cache.extract()).toEqual({
ROOT_QUERY: {
Expand All @@ -1195,7 +1216,10 @@ describe('EntityStore', () => {
},
});

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

expect(cache.extract()).toEqual({
ROOT_QUERY: {
Expand All @@ -1213,7 +1237,10 @@ describe('EntityStore', () => {
},
});

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

expect(cache.extract()).toEqual({
ROOT_QUERY: {
Expand All @@ -1226,7 +1253,9 @@ describe('EntityStore', () => {
},
});

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

expect(cache.extract()).toEqual({
ROOT_QUERY: {
Expand Down Expand Up @@ -1506,12 +1535,14 @@ describe('EntityStore', () => {
query: queryWithoutAliases,
})).toBe(resultWithoutAliases);

cache.evict(cache.identify({
__typename: "ABCs",
a: "ay",
b: "bee",
c: "see",
})!);
cache.evict({
id: cache.identify({
__typename: "ABCs",
a: "ay",
b: "bee",
c: "see",
}),
});

expect(cache.extract()).toEqual({
ROOT_QUERY: {
Expand Down Expand Up @@ -1590,7 +1621,7 @@ describe('EntityStore', () => {
id: 2,
})!;

expect(cache.evict(authorId)).toBe(true);
expect(cache.evict({ id: authorId })).toBe(true);

expect(cache.extract(true)).toEqual({
"Book:1": {
Expand All @@ -1604,7 +1635,7 @@ describe('EntityStore', () => {
},
});

expect(cache.evict(authorId)).toBe(false);
expect(cache.evict({ id: authorId })).toBe(false);

const missing = [
new MissingFieldError(
Expand Down
4 changes: 3 additions & 1 deletion src/cache/inmemory/__tests__/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2513,7 +2513,9 @@ describe("type policies", function () {

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

expect(cache.evict("ROOT_QUERY", "book")).toBe(true);
expect(cache.evict({
fieldName: "book",
})).toBe(true);

expect(cache.gc().sort()).toEqual([
'Book:{"isbn":"0393354326"}',
Expand Down
22 changes: 12 additions & 10 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,19 @@ export abstract class EntityStore implements NormalizedCache {

public evict(options: Cache.EvictOptions): boolean {
let evicted = false;
if (hasOwn.call(this.data, options.id)) {
evicted = this.delete(options.id, options.fieldName, options.args);
}
if (this instanceof Layer) {
evicted = this.parent.evict(options) || evicted;
if (options.id) {
if (hasOwn.call(this.data, options.id)) {
evicted = this.delete(options.id, options.fieldName, options.args);
}
if (this instanceof Layer) {
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(options.id, options.fieldName || "__exists");
}
// 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(options.id, options.fieldName || "__exists");
return evicted;
}

Expand Down
26 changes: 11 additions & 15 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,28 +236,24 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
return this.policies.identify(object)[0];
}

public evict(
idOrOptions: string | Cache.EvictOptions,
fieldName?: string,
args?: Record<string, any>,
): boolean {
public evict(options: Cache.EvictOptions): boolean {
if (!options.id) {
if (hasOwn.call(options, "id")) {
// See comment in modify method about why we return false when
// options.id exists but is falsy/undefined.
return false;
}
options = { ...options, id: "ROOT_QUERY" };
}
Comment on lines +239 to +247
Copy link
Member Author

@benjamn benjamn May 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is important now that options.id can be omitted as a way to default to the ROOT_QUERY ID.

Here's the equivalent logic from cache.modify:

  public modify(options: ModifyOptions): boolean {
    if (hasOwn.call(options, "id") && !options.id) {
      // To my knowledge, TypeScript does not currently provide a way to
      // enforce that an optional property?:type must *not* be undefined
      // when present. That ability would be useful here, because we want
      // options.id to default to ROOT_QUERY only when no options.id was
      // provided. If the caller attempts to pass options.id with a
      // falsy/undefined value (perhaps because cache.identify failed), we
      // should not assume the goal was to modify the ROOT_QUERY object.
      // We could throw, but it seems natural to return false to indicate
      // that nothing was modified.
      return false;
    }

try {
// It's unlikely that the eviction will end up invoking any other
// cache update operations while it's running, but {in,de}crementing
// this.txCount still seems like a good idea, for uniformity with
// the other update methods.
++this.txCount;
return this.optimisticData.evict(
typeof idOrOptions === "string" ? {
id: idOrOptions,
fieldName,
args,
} : idOrOptions,
);
return this.optimisticData.evict(options);
} finally {
if (!--this.txCount &&
(typeof idOrOptions === "string" ||
idOrOptions.broadcast !== false)) {
if (!--this.txCount && options.broadcast !== false) {
this.broadcastWatches();
}
}
Expand Down