Skip to content

Commit 688681d

Browse files
authored
Apply some refactoring to QueryInfo (#13374)
While experimenting with `QueryInfo` for another issue, I noticed that we have a code path that is never used. `lastDiff` was applied if it was complete and we didn't write the cache, but there was never a case where `lastDiff.result` and `result.data` were never equal. This led me down a small refactor that removed that unnecessary code and moved some things around for clarity including an early return. This PR is best reviewed commit-by-commit with whitespace turned off to see where this ended up.
1 parent 2551937 commit 688681d

2 files changed

Lines changed: 118 additions & 130 deletions

File tree

src/core/QueryInfo.ts

Lines changed: 112 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,10 @@ export class QueryInfo<
143143
this.queryManager = queryManager;
144144

145145
// Track how often cache.evict is called, since we want eviction to
146-
// override the feud-stopping logic in the markQueryResult method, by
147-
// causing shouldWrite to return true. Wrapping the cache.evict method
148-
// is a bit of a hack, but it saves us from having to make eviction
149-
// counting an official part of the ApolloCache API.
146+
// override the write-skipping logic in `shouldWrite`, by causing it to
147+
// return true. Wrapping the cache.evict method is a bit of a hack, but it
148+
// saves us from having to make eviction counting an official part of the
149+
// ApolloCache API.
150150
if (!destructiveMethodCounts.has(cache)) {
151151
destructiveMethodCounts.set(cache, 0);
152152
wrapDestructiveCacheMethod(cache, "evict");
@@ -157,8 +157,14 @@ export class QueryInfo<
157157

158158
/**
159159
* @internal
160-
* For feud-preventing behaviour, `lastWrite` should be shared by all `QueryInfo` instances of an `ObservableQuery`.
161-
* In the case of a standalone `QueryInfo`, we will keep a local version.
160+
* Tracks the last result written to the cache so that `shouldWrite` can skip
161+
* an identical write. Since a `QueryInfo` only ever represents a single
162+
* network request, this is shared by all `QueryInfo` instances of an
163+
* `ObservableQuery`. A standalone `QueryInfo` keeps a local version.
164+
*
165+
* A network result that was explicitly asked for always takes precedence over
166+
* what is already cached, so `ObservableQuery.refetch` and polling clear this
167+
* value before starting their request.
162168
*/
163169
public _lastWrite?: LastWrite;
164170
private get lastWrite(): LastWrite | undefined {
@@ -177,18 +183,18 @@ export class QueryInfo<
177183
variables: ApolloClient.WatchQueryOptions["variables"]
178184
) {
179185
const { lastWrite } = this;
180-
return !(
181-
lastWrite &&
186+
return (
187+
!lastWrite ||
182188
// If cache.evict has been called since the last time we wrote this
183189
// data into the cache, there's a chance writing this result into
184190
// the cache will repair what was evicted.
185-
lastWrite.dmCount === destructiveMethodCounts.get(this.cache) &&
186-
equal(variables, lastWrite.variables) &&
187-
equal(result.data, lastWrite.result.data) &&
191+
lastWrite.dmCount !== destructiveMethodCounts.get(this.cache) ||
192+
!equal(variables, lastWrite.variables) ||
193+
!equal(result.data, lastWrite.result.data) ||
188194
// We have to compare these values because its possible the final chunk
189195
// emitted in the incremental result is just `hasNext: false`. This
190196
// ensures we trigger a cache write when we get `isLastChunk: true`.
191-
result.extensions?.[streamInfoSymbol] ===
197+
result.extensions?.[streamInfoSymbol] !==
192198
lastWrite.result.extensions?.[streamInfoSymbol]
193199
);
194200
}
@@ -251,7 +257,7 @@ export class QueryInfo<
251257
this.observableQuery?.["resetNotifications"]();
252258

253259
const skipCache = cacheWriteBehavior === CacheWriteBehavior.FORBID;
254-
const lastDiff =
260+
const diff =
255261
skipCache ? undefined : (
256262
this.getDiff({
257263
...diffOptions,
@@ -263,7 +269,7 @@ export class QueryInfo<
263269
);
264270

265271
const incrementalResult = this.maybeHandleIncrementalResult(
266-
lastDiff?.result,
272+
diff?.result,
267273
incoming,
268274
query
269275
);
@@ -303,113 +309,101 @@ export class QueryInfo<
303309
return result;
304310
}
305311

306-
if (shouldWriteResult(result, errorPolicy)) {
307-
// Using a transaction here so we have a chance to read the result
308-
// back from the cache before the watch callback fires as a result
309-
// of writeQuery, so we can store the new diff quietly and ignore
310-
// it when we receive it redundantly from the watch callback.
311-
this.cache.batch({
312-
onWatchUpdated: (
313-
// all additional options on ObservableQuery.CacheWatchOptions are
314-
// optional so we can use the type here
315-
watch: ObservableQuery.CacheWatchOptions,
316-
diff
317-
) => {
318-
if (watch.watcher === this.observableQuery) {
319-
// see comment on `lastOwnDiff` for explanation
320-
watch.lastOwnDiff = diff;
321-
}
322-
},
323-
update: (cache) => {
324-
const shouldWrite = this.shouldWrite(result, variables);
325-
326-
if (shouldWrite) {
327-
cache.writeQuery({
328-
query,
329-
data: result.data as Unmasked<any>,
330-
variables,
331-
overwrite: cacheWriteBehavior === CacheWriteBehavior.OVERWRITE,
332-
extensions: result.extensions,
333-
});
334-
335-
this.lastWrite = {
336-
result,
337-
variables,
338-
dmCount: destructiveMethodCounts.get(this.cache),
339-
};
340-
} else {
341-
// If result is the same as the last result we received from
342-
// the network (and the variables match too), avoid writing
343-
// result into the cache again. The wisdom of skipping this
344-
// cache write is far from obvious, since any cache write
345-
// could be the one that puts the cache back into a desired
346-
// state, fixing corruption or missing data. However, if we
347-
// always write every network result into the cache, we enable
348-
// feuds between queries competing to update the same data in
349-
// incompatible ways, which can lead to an endless cycle of
350-
// cache broadcasts and useless network requests. As with any
351-
// feud, eventually one side must step back from the brink,
352-
// letting the other side(s) have the last word(s). There may
353-
// be other points where we could break this cycle, such as
354-
// silencing the broadcast for cache.writeQuery (not a good
355-
// idea, since it just delays the feud a bit) or somehow
356-
// avoiding the network request that just happened (also bad,
357-
// because the server could return useful new data). All
358-
// options considered, skipping this cache write seems to be
359-
// the least damaging place to break the cycle, because it
360-
// reflects the intuition that we recently wrote this exact
361-
// result into the cache, so the cache *should* already/still
362-
// contain this data. If some other query has clobbered that
363-
// data in the meantime, that's too bad, but there will be no
364-
// winners if every query blindly reverts to its own version
365-
// of the data. This approach also gives the network a chance
366-
// to return new data, which will be written into the cache as
367-
// usual, notifying only those queries that are directly
368-
// affected by the cache updates, as usual. In the future, an
369-
// even more sophisticated cache could perhaps prevent or
370-
// mitigate the clobbering somehow, but that would make this
371-
// particular cache write even less important, and thus
372-
// skipping it would be even safer than it is today.
373-
if (lastDiff && lastDiff.complete) {
374-
// Reuse data from the last good (complete) diff that we
375-
// received, when possible.
376-
result = {
377-
...result,
378-
data: lastDiff.result,
379-
dataState: "complete",
380-
};
381-
return;
382-
}
383-
// If the previous this.diff was incomplete, fall through to
384-
// re-reading the latest data with cache.diff, below.
385-
}
386-
387-
const isNetworkOnly =
388-
fetchPolicy === "network-only" &&
389-
networkStatus !== NetworkStatus.refetch;
390-
391-
const { dataState, result: diffResult } = this.getDiff(
392-
{
393-
...diffOptions,
394-
// Never deliver partial data for network-only requests
395-
returnPartialData: returnPartialData && !isNetworkOnly,
396-
},
397-
this.getIncrementalInfo(result, { isNetworkOnly })
398-
);
399-
400-
if (
401-
dataState === "complete" ||
402-
(returnPartialData && dataState === "partial" && shouldWrite) ||
403-
(this.hasNext && dataState === "streaming")
404-
) {
405-
result = { ...result, data: diffResult, dataState };
406-
}
407-
},
408-
});
409-
} else {
312+
if (!shouldWriteResult(result, errorPolicy)) {
410313
this.lastWrite = void 0;
314+
return result;
411315
}
412316

317+
// Using a transaction here so we have a chance to read the result
318+
// back from the cache before the watch callback fires as a result
319+
// of writeQuery, so we can store the new diff quietly and ignore
320+
// it when we receive it redundantly from the watch callback.
321+
this.cache.batch({
322+
onWatchUpdated: (
323+
// all additional options on ObservableQuery.CacheWatchOptions are
324+
// optional so we can use the type here
325+
watch: ObservableQuery.CacheWatchOptions,
326+
diff
327+
) => {
328+
if (watch.watcher === this.observableQuery) {
329+
// see comment on `lastOwnDiff` for explanation
330+
watch.lastOwnDiff = diff;
331+
}
332+
},
333+
update: (cache) => {
334+
const shouldWrite = this.shouldWrite(result, variables);
335+
336+
// If result is the same as the last result we received from
337+
// the network (and the variables match too), avoid writing
338+
// result into the cache again. The wisdom of skipping this
339+
// cache write is far from obvious, since any cache write
340+
// could be the one that puts the cache back into a desired
341+
// state, fixing corruption or missing data. However, if we
342+
// always write every network result into the cache, we enable
343+
// feuds between queries competing to update the same data in
344+
// incompatible ways, which can lead to an endless cycle of
345+
// cache broadcasts and useless network requests. As with any
346+
// feud, eventually one side must step back from the brink,
347+
// letting the other side(s) have the last word(s). There may
348+
// be other points where we could break this cycle, such as
349+
// silencing the broadcast for cache.writeQuery (not a good
350+
// idea, since it just delays the feud a bit) or somehow
351+
// avoiding the network request that just happened (also bad,
352+
// because the server could return useful new data). All
353+
// options considered, skipping this cache write seems to be
354+
// the least damaging place to break the cycle, because it
355+
// reflects the intuition that we recently wrote this exact
356+
// result into the cache, so the cache *should* already/still
357+
// contain this data. If some other query has clobbered that
358+
// data in the meantime, that's too bad, but there will be no
359+
// winners if every query blindly reverts to its own version
360+
// of the data. This approach also gives the network a chance
361+
// to return new data, which will be written into the cache as
362+
// usual, notifying only those queries that are directly
363+
// affected by the cache updates, as usual. In the future, an
364+
// even more sophisticated cache could perhaps prevent or
365+
// mitigate the clobbering somehow, but that would make this
366+
// particular cache write even less important, and thus
367+
// skipping it would be even safer than it is today.
368+
if (shouldWrite) {
369+
cache.writeQuery({
370+
query,
371+
data: result.data as Unmasked<any>,
372+
variables,
373+
overwrite: cacheWriteBehavior === CacheWriteBehavior.OVERWRITE,
374+
extensions: result.extensions,
375+
});
376+
377+
this.lastWrite = {
378+
result,
379+
variables,
380+
dmCount: destructiveMethodCounts.get(this.cache),
381+
};
382+
}
383+
384+
const isNetworkOnly =
385+
fetchPolicy === "network-only" &&
386+
networkStatus !== NetworkStatus.refetch;
387+
388+
const { dataState, result: diffResult } = this.getDiff(
389+
{
390+
...diffOptions,
391+
// Never deliver partial data for network-only requests
392+
returnPartialData: returnPartialData && !isNetworkOnly,
393+
},
394+
this.getIncrementalInfo(result, { isNetworkOnly })
395+
);
396+
397+
if (
398+
dataState === "complete" ||
399+
(returnPartialData && dataState === "partial" && shouldWrite) ||
400+
(this.hasNext && dataState === "streaming")
401+
) {
402+
result = { ...result, data: diffResult, dataState };
403+
}
404+
},
405+
});
406+
413407
return result;
414408
}
415409

src/core/QueryManager.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,18 +1106,12 @@ export class QueryManager {
11061106
aqr[extensionsSymbol] = result.extensions;
11071107
}
11081108

1109-
if (hasErrors) {
1110-
if (errorPolicy === "none") {
1111-
aqr.data = void 0 as TData;
1112-
aqr.dataState = "empty";
1113-
}
1114-
if (errorPolicy !== "ignore") {
1115-
aqr.error = new CombinedGraphQLErrors(
1116-
removeStreamDetailsFromExtensions(result)
1117-
);
1118-
if (aqr.networkStatus !== NetworkStatus.streaming) {
1119-
aqr.networkStatus = NetworkStatus.error;
1120-
}
1109+
if (hasErrors && errorPolicy !== "ignore") {
1110+
aqr.error = new CombinedGraphQLErrors(
1111+
removeStreamDetailsFromExtensions(result)
1112+
);
1113+
if (aqr.networkStatus !== NetworkStatus.streaming) {
1114+
aqr.networkStatus = NetworkStatus.error;
11211115
}
11221116
}
11231117

0 commit comments

Comments
 (0)