diff --git a/CHANGELOG.md b/CHANGELOG.md index e22d3222d5f..b3745ac3300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,10 @@ - Allow `options.nextFetchPolicy` to be a function that takes the current `FetchPolicy` and returns a new (or the same) `FetchPolicy`, making `nextFetchPolicy` more suitable for global use in `defaultOptions.watchQuery`.
[@benjamn](https://github.com/benjamn) in [#6893](https://github.com/apollographql/apollo-client/pull/6893) -- Disable feud-stopping logic after any cache eviction.
- [@benjamn](https://github.com/benjamn) in [#6817](https://github.com/apollographql/apollo-client/pull/6817) +- Disable feud-stopping logic after any `cache.evict` or `cache.modify` operation.
+ [@benjamn](https://github.com/benjamn) in + [#6817](https://github.com/apollographql/apollo-client/pull/6817) and + [#6898](https://github.com/apollographql/apollo-client/pull/6898) - Prevent full reobservation of queries affected by optimistic mutation updates, while still delivering results from the cache.
[@benjamn](https://github.com/benjamn) in [#6854](https://github.com/apollographql/apollo-client/pull/6854) diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 803e9ec826e..c02b3fbdf34 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -25,10 +25,30 @@ export type QueryStoreValue = Pick; -const cacheEvictCounts = new ( +const destructiveMethodCounts = new ( canUseWeakMap ? WeakMap : Map ), number>(); +function wrapDestructiveCacheMethod( + cache: ApolloCache, + methodName: keyof ApolloCache, +) { + const original = cache[methodName]; + if (typeof original === "function") { + cache[methodName] = function () { + destructiveMethodCounts.set( + cache, + // The %1e15 allows the count to wrap around to 0 safely every + // quadrillion evictions, so there's no risk of overflow. To be + // clear, this is more of a pedantic principle than something + // that matters in any conceivable practical scenario. + (destructiveMethodCounts.get(cache)! + 1) % 1e15, + ); + return original.apply(this, arguments); + }; + } +} + // A QueryInfo object represents a single query managed by the // QueryManager, which tracks all QueryInfo objects by queryId in its // this.queries Map. QueryInfo objects store the latest results and errors @@ -57,20 +77,9 @@ export class QueryInfo { // causing shouldWrite to return true. Wrapping the cache.evict method // is a bit of a hack, but it saves us from having to make eviction // counting an official part of the ApolloCache API. - if (!cacheEvictCounts.has(cache) && cache.evict) { - cacheEvictCounts.set(cache, 0); - const originalEvict = cache.evict; - cache.evict = function evict() { - cacheEvictCounts.set( - cache, - // The %1e15 allows the count to wrap around to 0 safely every - // quadrillion evictions, so there's no risk of overflow. To be - // clear, this is more of a pedantic principle than something - // that matters in any conceivable practical scenario. - (cacheEvictCounts.get(cache)! + 1) % 1e15, - ); - return originalEvict.apply(this, arguments); - }; + if (!destructiveMethodCounts.has(cache)) { + wrapDestructiveCacheMethod(cache, "evict"); + wrapDestructiveCacheMethod(cache, "modify"); } } @@ -246,7 +255,7 @@ export class QueryInfo { private lastWrite?: { result: FetchResult; variables: WatchQueryOptions["variables"]; - evictCount: number | undefined; + dmCount: number | undefined; }; private shouldWrite( @@ -259,7 +268,7 @@ export class QueryInfo { // If cache.evict has been called since the last time we wrote this // data into the cache, there's a chance writing this result into // the cache will repair what was evicted. - lastWrite.evictCount === cacheEvictCounts.get(this.cache) && + lastWrite.dmCount === destructiveMethodCounts.get(this.cache) && equal(variables, lastWrite.variables) && equal(result.data, lastWrite.result.data) ); @@ -303,7 +312,7 @@ export class QueryInfo { this.lastWrite = { result, variables: options.variables, - evictCount: cacheEvictCounts.get(this.cache), + dmCount: destructiveMethodCounts.get(this.cache), }; } else { // If result is the same as the last result we received from diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 02ef884f56e..a9adc0ac168 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -2288,6 +2288,112 @@ describe('QueryManager', () => { }); }); + itAsync("should disable feud-stopping logic after evict or modify", (resolve, reject) => { + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + info: { + merge: false, + }, + }, + }, + }, + }); + + const client = new ApolloClient({ + cache, + link: new ApolloLink(operation => new Observable((observer: Observer) => { + observer.next!({ data: { info: { c: "see" }}}); + observer.complete!(); + })), + }); + + const query = gql`query { info { c } }`; + + const obs = client.watchQuery({ + query, + returnPartialData: true, + }); + + subscribeAndCount(reject, obs, (count, result) => { + if (count === 1) { + expect(result).toEqual({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + } else if (count === 2) { + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + c: "see", + }, + }, + }); + + cache.evict({ + fieldName: "info", + }); + + } else if (count === 3) { + expect(result).toEqual({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + } else if (count === 4) { + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + c: "see", + }, + }, + }); + + cache.modify({ + fields: { + info(_, { DELETE }) { + return DELETE; + }, + }, + }); + + } else if (count === 5) { + expect(result).toEqual({ + loading: true, + networkStatus: NetworkStatus.loading, + data: {}, + partial: true, + }); + + } else if (count === 6) { + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + info: { + c: "see", + }, + }, + }); + + setTimeout(resolve, 100); + + } else { + reject(new Error(`Unexpected ${JSON.stringify({count,result})}`)); + } + }); + }); + itAsync('should not error when replacing unidentified data with a normalized ID', (resolve, reject) => { const queryWithoutId = gql` query {