UNPKG

20.1 kBJavaScriptView Raw
1import { __assign } from "tslib";
2import { equal } from "@wry/equality";
3import { DeepMerger } from "../utilities/index.js";
4import { mergeIncrementalData } from "../utilities/index.js";
5import { reobserveCacheFirst } from "./ObservableQuery.js";
6import { isNonEmptyArray, graphQLResultHasError, canUseWeakMap, } from "../utilities/index.js";
7import { NetworkStatus, isNetworkRequestInFlight } from "./networkStatus.js";
8var destructiveMethodCounts = new (canUseWeakMap ? WeakMap : Map)();
9function wrapDestructiveCacheMethod(cache, methodName) {
10 var original = cache[methodName];
11 if (typeof original === "function") {
12 // @ts-expect-error this is just too generic to be typed correctly
13 cache[methodName] = function () {
14 destructiveMethodCounts.set(cache,
15 // The %1e15 allows the count to wrap around to 0 safely every
16 // quadrillion evictions, so there's no risk of overflow. To be
17 // clear, this is more of a pedantic principle than something
18 // that matters in any conceivable practical scenario.
19 (destructiveMethodCounts.get(cache) + 1) % 1e15);
20 // @ts-expect-error this is just too generic to be typed correctly
21 return original.apply(this, arguments);
22 };
23 }
24}
25function cancelNotifyTimeout(info) {
26 if (info["notifyTimeout"]) {
27 clearTimeout(info["notifyTimeout"]);
28 info["notifyTimeout"] = void 0;
29 }
30}
31// A QueryInfo object represents a single query managed by the
32// QueryManager, which tracks all QueryInfo objects by queryId in its
33// this.queries Map. QueryInfo objects store the latest results and errors
34// for the given query, and are responsible for reporting those results to
35// the corresponding ObservableQuery, via the QueryInfo.notify method.
36// Results are reported asynchronously whenever setDiff marks the
37// QueryInfo object as dirty, though a call to the QueryManager's
38// broadcastQueries method may trigger the notification before it happens
39// automatically. This class used to be a simple interface type without
40// any field privacy or meaningful methods, which is why it still has so
41// many public fields. The effort to lock down and simplify the QueryInfo
42// interface is ongoing, and further improvements are welcome.
43var QueryInfo = /** @class */ (function () {
44 function QueryInfo(queryManager, queryId) {
45 if (queryId === void 0) { queryId = queryManager.generateQueryId(); }
46 this.queryId = queryId;
47 this.listeners = new Set();
48 this.document = null;
49 this.lastRequestId = 1;
50 this.stopped = false;
51 this.dirty = false;
52 this.observableQuery = null;
53 var cache = (this.cache = queryManager.cache);
54 // Track how often cache.evict is called, since we want eviction to
55 // override the feud-stopping logic in the markResult method, by
56 // causing shouldWrite to return true. Wrapping the cache.evict method
57 // is a bit of a hack, but it saves us from having to make eviction
58 // counting an official part of the ApolloCache API.
59 if (!destructiveMethodCounts.has(cache)) {
60 destructiveMethodCounts.set(cache, 0);
61 wrapDestructiveCacheMethod(cache, "evict");
62 wrapDestructiveCacheMethod(cache, "modify");
63 wrapDestructiveCacheMethod(cache, "reset");
64 }
65 }
66 QueryInfo.prototype.init = function (query) {
67 var networkStatus = query.networkStatus || NetworkStatus.loading;
68 if (this.variables &&
69 this.networkStatus !== NetworkStatus.loading &&
70 !equal(this.variables, query.variables)) {
71 networkStatus = NetworkStatus.setVariables;
72 }
73 if (!equal(query.variables, this.variables)) {
74 this.lastDiff = void 0;
75 }
76 Object.assign(this, {
77 document: query.document,
78 variables: query.variables,
79 networkError: null,
80 graphQLErrors: this.graphQLErrors || [],
81 networkStatus: networkStatus,
82 });
83 if (query.observableQuery) {
84 this.setObservableQuery(query.observableQuery);
85 }
86 if (query.lastRequestId) {
87 this.lastRequestId = query.lastRequestId;
88 }
89 return this;
90 };
91 QueryInfo.prototype.reset = function () {
92 cancelNotifyTimeout(this);
93 this.dirty = false;
94 };
95 QueryInfo.prototype.resetDiff = function () {
96 this.lastDiff = void 0;
97 };
98 QueryInfo.prototype.getDiff = function () {
99 var options = this.getDiffOptions();
100 if (this.lastDiff && equal(options, this.lastDiff.options)) {
101 return this.lastDiff.diff;
102 }
103 this.updateWatch(this.variables);
104 var oq = this.observableQuery;
105 if (oq && oq.options.fetchPolicy === "no-cache") {
106 return { complete: false };
107 }
108 var diff = this.cache.diff(options);
109 this.updateLastDiff(diff, options);
110 return diff;
111 };
112 QueryInfo.prototype.updateLastDiff = function (diff, options) {
113 this.lastDiff =
114 diff ?
115 {
116 diff: diff,
117 options: options || this.getDiffOptions(),
118 }
119 : void 0;
120 };
121 QueryInfo.prototype.getDiffOptions = function (variables) {
122 var _a;
123 if (variables === void 0) { variables = this.variables; }
124 return {
125 query: this.document,
126 variables: variables,
127 returnPartialData: true,
128 optimistic: true,
129 canonizeResults: (_a = this.observableQuery) === null || _a === void 0 ? void 0 : _a.options.canonizeResults,
130 };
131 };
132 QueryInfo.prototype.setDiff = function (diff) {
133 var _this = this;
134 var _a;
135 var oldDiff = this.lastDiff && this.lastDiff.diff;
136 // If we do not tolerate partial results, skip this update to prevent it
137 // from being reported. This prevents a situtuation where a query that
138 // errors and another succeeds with overlapping data does not report the
139 // partial data result to the errored query.
140 //
141 // See https://github.com/apollographql/apollo-client/issues/11400 for more
142 // information on this issue.
143 if (diff &&
144 !diff.complete &&
145 !((_a = this.observableQuery) === null || _a === void 0 ? void 0 : _a.options.returnPartialData) &&
146 // In the case of a cache eviction, the diff will become partial so we
147 // schedule a notification to send a network request (this.oqListener) to
148 // go and fetch the missing data.
149 !(oldDiff && oldDiff.complete)) {
150 return;
151 }
152 this.updateLastDiff(diff);
153 if (!this.dirty && !equal(oldDiff && oldDiff.result, diff && diff.result)) {
154 this.dirty = true;
155 if (!this.notifyTimeout) {
156 this.notifyTimeout = setTimeout(function () { return _this.notify(); }, 0);
157 }
158 }
159 };
160 QueryInfo.prototype.setObservableQuery = function (oq) {
161 var _this = this;
162 if (oq === this.observableQuery)
163 return;
164 if (this.oqListener) {
165 this.listeners.delete(this.oqListener);
166 }
167 this.observableQuery = oq;
168 if (oq) {
169 oq["queryInfo"] = this;
170 this.listeners.add((this.oqListener = function () {
171 var diff = _this.getDiff();
172 if (diff.fromOptimisticTransaction) {
173 // If this diff came from an optimistic transaction, deliver the
174 // current cache data to the ObservableQuery, but don't perform a
175 // reobservation, since oq.reobserveCacheFirst might make a network
176 // request, and we never want to trigger network requests in the
177 // middle of optimistic updates.
178 oq["observe"]();
179 }
180 else {
181 // Otherwise, make the ObservableQuery "reobserve" the latest data
182 // using a temporary fetch policy of "cache-first", so complete cache
183 // results have a chance to be delivered without triggering additional
184 // network requests, even when options.fetchPolicy is "network-only"
185 // or "cache-and-network". All other fetch policies are preserved by
186 // this method, and are handled by calling oq.reobserve(). If this
187 // reobservation is spurious, isDifferentFromLastResult still has a
188 // chance to catch it before delivery to ObservableQuery subscribers.
189 reobserveCacheFirst(oq);
190 }
191 }));
192 }
193 else {
194 delete this.oqListener;
195 }
196 };
197 QueryInfo.prototype.notify = function () {
198 var _this = this;
199 cancelNotifyTimeout(this);
200 if (this.shouldNotify()) {
201 this.listeners.forEach(function (listener) { return listener(_this); });
202 }
203 this.dirty = false;
204 };
205 QueryInfo.prototype.shouldNotify = function () {
206 if (!this.dirty || !this.listeners.size) {
207 return false;
208 }
209 if (isNetworkRequestInFlight(this.networkStatus) && this.observableQuery) {
210 var fetchPolicy = this.observableQuery.options.fetchPolicy;
211 if (fetchPolicy !== "cache-only" && fetchPolicy !== "cache-and-network") {
212 return false;
213 }
214 }
215 return true;
216 };
217 QueryInfo.prototype.stop = function () {
218 if (!this.stopped) {
219 this.stopped = true;
220 // Cancel the pending notify timeout
221 this.reset();
222 this.cancel();
223 // Revert back to the no-op version of cancel inherited from
224 // QueryInfo.prototype.
225 this.cancel = QueryInfo.prototype.cancel;
226 var oq = this.observableQuery;
227 if (oq)
228 oq.stopPolling();
229 }
230 };
231 // This method is a no-op by default, until/unless overridden by the
232 // updateWatch method.
233 QueryInfo.prototype.cancel = function () { };
234 QueryInfo.prototype.updateWatch = function (variables) {
235 var _this = this;
236 if (variables === void 0) { variables = this.variables; }
237 var oq = this.observableQuery;
238 if (oq && oq.options.fetchPolicy === "no-cache") {
239 return;
240 }
241 var watchOptions = __assign(__assign({}, this.getDiffOptions(variables)), { watcher: this, callback: function (diff) { return _this.setDiff(diff); } });
242 if (!this.lastWatch || !equal(watchOptions, this.lastWatch)) {
243 this.cancel();
244 this.cancel = this.cache.watch((this.lastWatch = watchOptions));
245 }
246 };
247 QueryInfo.prototype.resetLastWrite = function () {
248 this.lastWrite = void 0;
249 };
250 QueryInfo.prototype.shouldWrite = function (result, variables) {
251 var lastWrite = this.lastWrite;
252 return !(lastWrite &&
253 // If cache.evict has been called since the last time we wrote this
254 // data into the cache, there's a chance writing this result into
255 // the cache will repair what was evicted.
256 lastWrite.dmCount === destructiveMethodCounts.get(this.cache) &&
257 equal(variables, lastWrite.variables) &&
258 equal(result.data, lastWrite.result.data));
259 };
260 QueryInfo.prototype.markResult = function (result, document, options, cacheWriteBehavior) {
261 var _this = this;
262 var merger = new DeepMerger();
263 var graphQLErrors = isNonEmptyArray(result.errors) ? result.errors.slice(0) : [];
264 // Cancel the pending notify timeout (if it exists) to prevent extraneous network
265 // requests. To allow future notify timeouts, diff and dirty are reset as well.
266 this.reset();
267 if ("incremental" in result && isNonEmptyArray(result.incremental)) {
268 var mergedData = mergeIncrementalData(this.getDiff().result, result);
269 result.data = mergedData;
270 // Detect the first chunk of a deferred query and merge it with existing
271 // cache data. This ensures a `cache-first` fetch policy that returns
272 // partial cache data or a `cache-and-network` fetch policy that already
273 // has full data in the cache does not complain when trying to merge the
274 // initial deferred server data with existing cache data.
275 }
276 else if ("hasNext" in result && result.hasNext) {
277 var diff = this.getDiff();
278 result.data = merger.merge(diff.result, result.data);
279 }
280 this.graphQLErrors = graphQLErrors;
281 if (options.fetchPolicy === "no-cache") {
282 this.updateLastDiff({ result: result.data, complete: true }, this.getDiffOptions(options.variables));
283 }
284 else if (cacheWriteBehavior !== 0 /* CacheWriteBehavior.FORBID */) {
285 if (shouldWriteResult(result, options.errorPolicy)) {
286 // Using a transaction here so we have a chance to read the result
287 // back from the cache before the watch callback fires as a result
288 // of writeQuery, so we can store the new diff quietly and ignore
289 // it when we receive it redundantly from the watch callback.
290 this.cache.performTransaction(function (cache) {
291 if (_this.shouldWrite(result, options.variables)) {
292 cache.writeQuery({
293 query: document,
294 data: result.data,
295 variables: options.variables,
296 overwrite: cacheWriteBehavior === 1 /* CacheWriteBehavior.OVERWRITE */,
297 });
298 _this.lastWrite = {
299 result: result,
300 variables: options.variables,
301 dmCount: destructiveMethodCounts.get(_this.cache),
302 };
303 }
304 else {
305 // If result is the same as the last result we received from
306 // the network (and the variables match too), avoid writing
307 // result into the cache again. The wisdom of skipping this
308 // cache write is far from obvious, since any cache write
309 // could be the one that puts the cache back into a desired
310 // state, fixing corruption or missing data. However, if we
311 // always write every network result into the cache, we enable
312 // feuds between queries competing to update the same data in
313 // incompatible ways, which can lead to an endless cycle of
314 // cache broadcasts and useless network requests. As with any
315 // feud, eventually one side must step back from the brink,
316 // letting the other side(s) have the last word(s). There may
317 // be other points where we could break this cycle, such as
318 // silencing the broadcast for cache.writeQuery (not a good
319 // idea, since it just delays the feud a bit) or somehow
320 // avoiding the network request that just happened (also bad,
321 // because the server could return useful new data). All
322 // options considered, skipping this cache write seems to be
323 // the least damaging place to break the cycle, because it
324 // reflects the intuition that we recently wrote this exact
325 // result into the cache, so the cache *should* already/still
326 // contain this data. If some other query has clobbered that
327 // data in the meantime, that's too bad, but there will be no
328 // winners if every query blindly reverts to its own version
329 // of the data. This approach also gives the network a chance
330 // to return new data, which will be written into the cache as
331 // usual, notifying only those queries that are directly
332 // affected by the cache updates, as usual. In the future, an
333 // even more sophisticated cache could perhaps prevent or
334 // mitigate the clobbering somehow, but that would make this
335 // particular cache write even less important, and thus
336 // skipping it would be even safer than it is today.
337 if (_this.lastDiff && _this.lastDiff.diff.complete) {
338 // Reuse data from the last good (complete) diff that we
339 // received, when possible.
340 result.data = _this.lastDiff.diff.result;
341 return;
342 }
343 // If the previous this.diff was incomplete, fall through to
344 // re-reading the latest data with cache.diff, below.
345 }
346 var diffOptions = _this.getDiffOptions(options.variables);
347 var diff = cache.diff(diffOptions);
348 // In case the QueryManager stops this QueryInfo before its
349 // results are delivered, it's important to avoid restarting the
350 // cache watch when markResult is called. We also avoid updating
351 // the watch if we are writing a result that doesn't match the current
352 // variables to avoid race conditions from broadcasting the wrong
353 // result.
354 if (!_this.stopped && equal(_this.variables, options.variables)) {
355 // Any time we're about to update this.diff, we need to make
356 // sure we've started watching the cache.
357 _this.updateWatch(options.variables);
358 }
359 // If we're allowed to write to the cache, and we can read a
360 // complete result from the cache, update result.data to be the
361 // result from the cache, rather than the raw network result.
362 // Set without setDiff to avoid triggering a notify call, since
363 // we have other ways of notifying for this result.
364 _this.updateLastDiff(diff, diffOptions);
365 if (diff.complete) {
366 result.data = diff.result;
367 }
368 });
369 }
370 else {
371 this.lastWrite = void 0;
372 }
373 }
374 };
375 QueryInfo.prototype.markReady = function () {
376 this.networkError = null;
377 return (this.networkStatus = NetworkStatus.ready);
378 };
379 QueryInfo.prototype.markError = function (error) {
380 this.networkStatus = NetworkStatus.error;
381 this.lastWrite = void 0;
382 this.reset();
383 if (error.graphQLErrors) {
384 this.graphQLErrors = error.graphQLErrors;
385 }
386 if (error.networkError) {
387 this.networkError = error.networkError;
388 }
389 return error;
390 };
391 return QueryInfo;
392}());
393export { QueryInfo };
394export function shouldWriteResult(result, errorPolicy) {
395 if (errorPolicy === void 0) { errorPolicy = "none"; }
396 var ignoreErrors = errorPolicy === "ignore" || errorPolicy === "all";
397 var writeWithErrors = !graphQLResultHasError(result);
398 if (!writeWithErrors && ignoreErrors && result.data) {
399 writeWithErrors = true;
400 }
401 return writeWithErrors;
402}
403//# sourceMappingURL=QueryInfo.js.map
\No newline at end of file