@apollo/client 4.3.0-alpha.6 → 4.3.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/__cjs/cache/core/cache.cjs +1 -1
  3. package/__cjs/cache/inmemory/entityStore.cjs +3 -3
  4. package/__cjs/cache/inmemory/key-extractor.cjs +1 -1
  5. package/__cjs/cache/inmemory/policies.cjs +5 -5
  6. package/__cjs/cache/inmemory/readFromStore.cjs +3 -3
  7. package/__cjs/cache/inmemory/writeToStore.cjs +4 -4
  8. package/__cjs/core/ObservableQuery.cjs +166 -54
  9. package/__cjs/core/ObservableQuery.cjs.map +1 -1
  10. package/__cjs/core/ObservableQuery.d.cts +3 -6
  11. package/__cjs/core/QueryInfo.cjs +11 -125
  12. package/__cjs/core/QueryInfo.cjs.map +1 -1
  13. package/__cjs/core/QueryInfo.d.cts +0 -24
  14. package/__cjs/core/QueryManager.cjs +12 -14
  15. package/__cjs/core/QueryManager.cjs.map +1 -1
  16. package/__cjs/core/RefetchEventManager.cjs +3 -3
  17. package/__cjs/invariantErrorCodes.cjs +52 -32
  18. package/__cjs/version.cjs +1 -1
  19. package/cache/core/cache.js +1 -1
  20. package/cache/inmemory/entityStore.js +3 -3
  21. package/cache/inmemory/key-extractor.js +1 -1
  22. package/cache/inmemory/policies.js +5 -5
  23. package/cache/inmemory/readFromStore.js +3 -3
  24. package/cache/inmemory/writeToStore.js +4 -4
  25. package/core/ObservableQuery.d.ts +3 -6
  26. package/core/ObservableQuery.js +162 -54
  27. package/core/ObservableQuery.js.map +1 -1
  28. package/core/QueryInfo.d.ts +1 -25
  29. package/core/QueryInfo.js +11 -125
  30. package/core/QueryInfo.js.map +1 -1
  31. package/core/QueryManager.js +12 -14
  32. package/core/QueryManager.js.map +1 -1
  33. package/core/RefetchEventManager.js +3 -3
  34. package/invariantErrorCodes.js +52 -32
  35. package/package.json +1 -1
  36. package/version.js +1 -1
@@ -21,16 +21,29 @@ const empty = {
21
21
  dataState: "empty",
22
22
  partial: true,
23
23
  };
24
+ const destructiveMethodCounts = new WeakMap();
25
+ function wrapDestructiveCacheMethod(cache, methodName) {
26
+ const original = cache[methodName];
27
+ if (typeof original === "function") {
28
+ // @ts-expect-error this is just too generic to be typed correctly
29
+ cache[methodName] = function () {
30
+ destructiveMethodCounts.set(cache,
31
+ // The %1e15 allows the count to wrap around to 0 safely every
32
+ // quadrillion evictions, so there's no risk of overflow. To be
33
+ // clear, this is more of a pedantic principle than something
34
+ // that matters in any conceivable practical scenario.
35
+ (destructiveMethodCounts.get(cache) + 1) % 1e15);
36
+ // @ts-expect-error this is just too generic to be typed correctly
37
+ return original.apply(this, arguments);
38
+ };
39
+ }
40
+ }
24
41
  export class ObservableQuery {
25
42
  options;
26
43
  queryName;
27
44
  variablesUnknown = false;
28
- /**
29
- * @internal will be read and written from `QueryInfo`
30
- *
31
- * @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
32
- */
33
- _lastWrite;
45
+ didWarnOnFeud = false;
46
+ lastMissing;
34
47
  // The `query` computed property will always reflect the document transformed
35
48
  // by the last run query. `this.options.query` will always reflect the raw
36
49
  // untransformed query to ensure document transforms with runtime conditionals
@@ -67,6 +80,18 @@ export class ObservableQuery {
67
80
  }
68
81
  constructor({ queryManager, options, transformedQuery = queryManager.transform(options.query), }) {
69
82
  this.queryManager = queryManager;
83
+ // Track how often destructive cache methods are called, since we want
84
+ // eviction to override the feud-stopping logic in `shouldAutoRefetch`,
85
+ // by causing it to return true. Wrapping these cache methods is a bit of a
86
+ // hack, but it saves us from having to make eviction counting an official
87
+ // part of the ApolloCache API.
88
+ const { cache } = queryManager;
89
+ if (!destructiveMethodCounts.has(cache)) {
90
+ destructiveMethodCounts.set(cache, 0);
91
+ wrapDestructiveCacheMethod(cache, "evict");
92
+ wrapDestructiveCacheMethod(cache, "modify");
93
+ wrapDestructiveCacheMethod(cache, "reset");
94
+ }
70
95
  // active state
71
96
  this.waitForNetworkResult = options.fetchPolicy === "network-only";
72
97
  this.isTornDown = false;
@@ -394,7 +419,6 @@ export class ObservableQuery {
394
419
  reobserveOptions.variables = this.options.variables =
395
420
  this.getVariablesWithDefaults({ ...this.variables, ...variables });
396
421
  }
397
- this._lastWrite = undefined;
398
422
  return this._reobserve(reobserveOptions, {
399
423
  newNetworkStatus: NetworkStatus.refetch,
400
424
  });
@@ -882,7 +906,6 @@ export class ObservableQuery {
882
906
  if (this.pollingInfo) {
883
907
  if (!isNetworkRequestInFlight(this.networkStatus) &&
884
908
  !this.options.skipPollAttempt?.()) {
885
- this._lastWrite = undefined;
886
909
  this._reobserve({
887
910
  // Most fetchPolicy options don't make sense to use in a polling context, as
888
911
  // users wouldn't want to be polling the cache directly. However, network-only and
@@ -928,7 +951,10 @@ export class ObservableQuery {
928
951
  }
929
952
  _reobserve(newOptions, internalOptions) {
930
953
  this.isTornDown = false;
931
- let { newNetworkStatus } = internalOptions || {};
954
+ let { newNetworkStatus, keepLastMissing } = internalOptions || {};
955
+ if (!keepLastMissing) {
956
+ this.lastMissing = undefined;
957
+ }
932
958
  this.queryManager.obsQueries.add(this);
933
959
  const useDisposableObservable =
934
960
  // Refetching uses a disposable Observable to allow refetches using different
@@ -1069,7 +1095,7 @@ export class ObservableQuery {
1069
1095
  this.queryManager.obsQueries.delete(this);
1070
1096
  this.isTornDown = true;
1071
1097
  this.abortActiveOperations();
1072
- this._lastWrite = undefined;
1098
+ this.lastMissing = undefined;
1073
1099
  }
1074
1100
  transformDocument(document) {
1075
1101
  return this.queryManager.transform(document);
@@ -1127,51 +1153,126 @@ export class ObservableQuery {
1127
1153
  return;
1128
1154
  }
1129
1155
  }
1130
- const { dirty } = this;
1156
+ const { dirty, lastMissing } = this;
1157
+ const { fetchPolicy } = this.options;
1131
1158
  this.resetNotifications();
1132
- if (dirty &&
1133
- (this.options.fetchPolicy === "cache-only" ||
1134
- this.options.fetchPolicy === "cache-and-network" ||
1135
- !this.activeOperations.size)) {
1136
- const diff = this.getCacheDiff();
1137
- if (
1138
- // `fromOptimisticTransaction` is not available through the `cache.diff`
1139
- // code path, so we need to check it this way
1140
- equal(diff.result, this.getCacheDiff({ optimistic: false }).result)) {
1141
- //If this diff did not come from an optimistic transaction
1142
- // make the ObservableQuery "reobserve" the latest data
1143
- // using a temporary fetch policy of "cache-first", so complete cache
1144
- // results have a chance to be delivered without triggering additional
1145
- // network requests, even when options.fetchPolicy is "network-only"
1146
- // or "cache-and-network". All other fetch policies are preserved by
1147
- // this method, and are handled by calling oq.reobserve(). If this
1148
- // reobservation is spurious, distinctUntilChanged still has a
1149
- // chance to catch it before delivery to ObservableQuery subscribers.
1150
- this.reobserveCacheFirst();
1159
+ if (!dirty ||
1160
+ (fetchPolicy !== "cache-only" &&
1161
+ fetchPolicy !== "cache-and-network" &&
1162
+ this.activeOperations.size)) {
1163
+ return;
1164
+ }
1165
+ const diff = this.getCacheDiff();
1166
+ const current = this.getCurrentResult();
1167
+ // `fromOptimisticTransaction` is not available through the `cache.diff`
1168
+ // code path, so we need to check whether the cache result is an optimistic
1169
+ // result this way.
1170
+ const isOptimistic = !equal(diff.result, this.getCacheDiff({ optimistic: false }).result);
1171
+ if (
1172
+ // When this diff came from an optimistic transaction, deliver the
1173
+ // current cache data to the ObservableQuery, but don't perform a
1174
+ // reobservation, since oq.reobserveCacheFirst might make a network
1175
+ // request, and we never want to trigger network requests in the
1176
+ // middle of optimistic updates.
1177
+ isOptimistic ||
1178
+ // If we get a cache update in the middle of streaming (possible with
1179
+ // cache-and-network fetch policy), just deliver the cache value without
1180
+ // going through the full reobserve which would otherwise trigger another
1181
+ // request (deduplication should kick in, but doing so replays any
1182
+ // previous emits from the link chain, which get rewritten into the cache
1183
+ // and might clobber this cache update)
1184
+ (!diff.complete && current.networkStatus === NetworkStatus.streaming)) {
1185
+ this.deliverCacheDiff(diff);
1186
+ return;
1187
+ }
1188
+ if (diff.complete) {
1189
+ this.lastMissing = undefined;
1190
+ }
1191
+ else if (!lastMissing ||
1192
+ // If a destructive cache method has been called since the last recorded
1193
+ // incomplete result, there's a chance fetching this data again will
1194
+ // restore what was evicted, even though the cache result looks the same
1195
+ // as before.
1196
+ lastMissing.dmCount !== destructiveMethodCounts.get(this.cache) ||
1197
+ !equal(lastMissing.variables, this.variables) ||
1198
+ !equal(lastMissing.missing, diff.missing?.missing)) {
1199
+ this.didWarnOnFeud = false;
1200
+ this.lastMissing = {
1201
+ variables: this.variables,
1202
+ missing: diff.missing?.missing,
1203
+ dmCount: destructiveMethodCounts.get(this.cache),
1204
+ };
1205
+ }
1206
+ else if (
1207
+ // reobserveCacheFirst with cache-only fetch policy only calls
1208
+ // reobserve which never fetches from the network so we are ok
1209
+ // allowing cache-only queries to fallthrough to reobserveCacheFirst.
1210
+ // This also prevents the feud warning which would be confusing for a
1211
+ // cache-only query anyways.
1212
+ fetchPolicy !== "cache-only") {
1213
+ // If we've fallen through to this case, a cache emit has returned the
1214
+ // same missing fields which means at least one value on the fields we've
1215
+ // already delivered have changed. We are ok delivering the updated
1216
+ // partial result in this case to keep the result as fresh as possible. We
1217
+ // NEVER want to downgrade this query from a complete query to a partial
1218
+ // query though, so we also make sure we only deliver if the previous
1219
+ // result was also partial.
1220
+ if (current.dataState === "partial") {
1221
+ this.deliverCacheDiff(diff);
1151
1222
  }
1152
- else {
1153
- // If this diff came from an optimistic transaction, deliver the
1154
- // current cache data to the ObservableQuery, but don't perform a
1155
- // reobservation, since oq.reobserveCacheFirst might make a network
1156
- // request, and we never want to trigger network requests in the
1157
- // middle of optimistic updates.
1158
- this.input.next({
1159
- kind: "N",
1160
- value: {
1161
- data: diff.result,
1162
- dataState: diff.dataState,
1163
- networkStatus: NetworkStatus.ready,
1164
- loading: false,
1165
- error: undefined,
1166
- partial: !diff.complete,
1167
- },
1168
- source: "cache",
1169
- query: this.query,
1170
- variables: this.variables,
1171
- meta: {},
1172
- });
1223
+ // If the (partial) result is the same as the last partial result
1224
+ // we recorded from a previous broadcast (and the variables match
1225
+ // too), avoid calling reobserveCacheFirst to refetch this query
1226
+ // again. If we allow refetching anytime this result becomes partial,
1227
+ // we risk feuds between queries competing to update the same data in
1228
+ // incompatible ways, which can lead to an endless cycle of cache
1229
+ // broadcasts and useless network requests. As with any
1230
+ // feud, eventually one side must step back from the brink,
1231
+ // letting the other side(s) have the last word(s). There may
1232
+ // be other points where we could break this cycle, such as
1233
+ // silencing the broadcast for cache.writeQuery (not a good
1234
+ // idea, since it just delays the feud a bit) or somehow
1235
+ // avoiding the network request that just happened (also bad,
1236
+ // because the server could return useful new data). All
1237
+ // options considered, returning early and stopping the
1238
+ // reobserveCacheFirst cycle seems to be the least damaging place to
1239
+ // break the cycle because it allows read functions/custom scalars to
1240
+ // be applied to the feuding query while avoiding the endless cycle of
1241
+ // requests.
1242
+ if (__DEV__ && !this.didWarnOnFeud) {
1243
+ this.didWarnOnFeud = true;
1244
+ warnOnFeud(this.query, diff);
1173
1245
  }
1246
+ return;
1174
1247
  }
1248
+ //If this diff did not come from an optimistic transaction
1249
+ // make the ObservableQuery "reobserve" the latest data
1250
+ // using a temporary fetch policy of "cache-first", so complete cache
1251
+ // results have a chance to be delivered without triggering additional
1252
+ // network requests, even when options.fetchPolicy is "network-only"
1253
+ // or "cache-and-network". All other fetch policies are preserved by
1254
+ // this method, and are handled by calling oq.reobserve(). If this
1255
+ // reobservation is spurious, distinctUntilChanged still has a
1256
+ // chance to catch it before delivery to ObservableQuery subscribers.
1257
+ this.reobserveCacheFirst();
1258
+ }
1259
+ deliverCacheDiff(diff) {
1260
+ const current = this.getCurrentResult();
1261
+ this.input.next({
1262
+ kind: "N",
1263
+ value: {
1264
+ data: diff.result,
1265
+ dataState: diff.dataState,
1266
+ networkStatus: current.networkStatus,
1267
+ loading: current.loading,
1268
+ error: undefined,
1269
+ partial: !diff.complete,
1270
+ },
1271
+ source: "cache",
1272
+ query: this.query,
1273
+ variables: this.variables,
1274
+ meta: {},
1275
+ });
1175
1276
  }
1176
1277
  activeOperations = new Set();
1177
1278
  pushOperation(networkStatus) {
@@ -1234,6 +1335,7 @@ export class ObservableQuery {
1234
1335
  // exception for cache-only queries - we reset them into a "ready" state
1235
1336
  // as we won't trigger a refetch for them
1236
1337
  const resetToEmpty = this.options.fetchPolicy === "cache-only";
1338
+ this.lastMissing = undefined;
1237
1339
  this.setResult(resetToEmpty ? empty : uninitialized, {
1238
1340
  shouldEmit: resetToEmpty ? 1 /* EmitBehavior.force */ : 2 /* EmitBehavior.never */,
1239
1341
  });
@@ -1378,7 +1480,10 @@ export class ObservableQuery {
1378
1480
  reobserveCacheFirst() {
1379
1481
  const { fetchPolicy, nextFetchPolicy } = this.options;
1380
1482
  if (fetchPolicy === "cache-and-network" || fetchPolicy === "network-only") {
1381
- this.reobserve({
1483
+ // Preserve this.lastMissing so a cache update that triggers this
1484
+ // reobserve doesn't reset feud detection. All user-initiated calls to
1485
+ // reobserve (refetch/poll/reobserve, etc) should clear it.
1486
+ this._reobserve({
1382
1487
  fetchPolicy: "cache-first",
1383
1488
  // Use a temporary nextFetchPolicy function that replaces itself with the
1384
1489
  // previous nextFetchPolicy value and returns the original fetchPolicy.
@@ -1394,10 +1499,10 @@ export class ObservableQuery {
1394
1499
  // Otherwise go back to the original this.options.fetchPolicy.
1395
1500
  return fetchPolicy;
1396
1501
  },
1397
- });
1502
+ }, { keepLastMissing: true });
1398
1503
  }
1399
1504
  else {
1400
- this.reobserve();
1505
+ this._reobserve(undefined, { keepLastMissing: true });
1401
1506
  }
1402
1507
  }
1403
1508
  getVariablesWithDefaults(variables) {
@@ -1449,4 +1554,7 @@ function getTrackingOperatorPromise(defaultValue) {
1449
1554
  });
1450
1555
  return { promise, operator };
1451
1556
  }
1557
+ function warnOnFeud(query, diff) {
1558
+ __DEV__ && invariant.warn(89, getOperationName(query, "(anonymous)"), diff.missing?.missing);
1559
+ }
1452
1560
  //# sourceMappingURL=ObservableQuery.js.map