@@ -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
0 commit comments