1 | import { __assign } from "tslib";
|
2 | import { equal } from "@wry/equality";
|
3 | import { DeepMerger } from "../utilities/index.js";
|
4 | import { mergeIncrementalData } from "../utilities/index.js";
|
5 | import { reobserveCacheFirst } from "./ObservableQuery.js";
|
6 | import { isNonEmptyArray, graphQLResultHasError, canUseWeakMap, } from "../utilities/index.js";
|
7 | import { NetworkStatus, isNetworkRequestInFlight } from "./networkStatus.js";
|
8 | var destructiveMethodCounts = new (canUseWeakMap ? WeakMap : Map)();
|
9 | function 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 | }
|
25 | function 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.
|
43 | var 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 are trying to deliver an incomplete cache result, we avoid
|
137 | // reporting it if the query has errored, otherwise we let the broadcast try
|
138 | // and repair the partial result by refetching the query. This check avoids
|
139 | // a situation where a query that errors and another succeeds with
|
140 | // overlapping data does not report the partial data result to the errored
|
141 | // query.
|
142 | //
|
143 | // See https://github.com/apollographql/apollo-client/issues/11400 for more
|
144 | // information on this issue.
|
145 | if (diff && !diff.complete && ((_a = this.observableQuery) === null || _a === void 0 ? void 0 : _a.getLastError())) {
|
146 | return;
|
147 | }
|
148 | this.updateLastDiff(diff);
|
149 | if (!this.dirty && !equal(oldDiff && oldDiff.result, diff && diff.result)) {
|
150 | this.dirty = true;
|
151 | if (!this.notifyTimeout) {
|
152 | this.notifyTimeout = setTimeout(function () { return _this.notify(); }, 0);
|
153 | }
|
154 | }
|
155 | };
|
156 | QueryInfo.prototype.setObservableQuery = function (oq) {
|
157 | var _this = this;
|
158 | if (oq === this.observableQuery)
|
159 | return;
|
160 | if (this.oqListener) {
|
161 | this.listeners.delete(this.oqListener);
|
162 | }
|
163 | this.observableQuery = oq;
|
164 | if (oq) {
|
165 | oq["queryInfo"] = this;
|
166 | this.listeners.add((this.oqListener = function () {
|
167 | var diff = _this.getDiff();
|
168 | if (diff.fromOptimisticTransaction) {
|
169 | // If this diff came from an optimistic transaction, deliver the
|
170 | // current cache data to the ObservableQuery, but don't perform a
|
171 | // reobservation, since oq.reobserveCacheFirst might make a network
|
172 | // request, and we never want to trigger network requests in the
|
173 | // middle of optimistic updates.
|
174 | oq["observe"]();
|
175 | }
|
176 | else {
|
177 | // Otherwise, make the ObservableQuery "reobserve" the latest data
|
178 | // using a temporary fetch policy of "cache-first", so complete cache
|
179 | // results have a chance to be delivered without triggering additional
|
180 | // network requests, even when options.fetchPolicy is "network-only"
|
181 | // or "cache-and-network". All other fetch policies are preserved by
|
182 | // this method, and are handled by calling oq.reobserve(). If this
|
183 | // reobservation is spurious, isDifferentFromLastResult still has a
|
184 | // chance to catch it before delivery to ObservableQuery subscribers.
|
185 | reobserveCacheFirst(oq);
|
186 | }
|
187 | }));
|
188 | }
|
189 | else {
|
190 | delete this.oqListener;
|
191 | }
|
192 | };
|
193 | QueryInfo.prototype.notify = function () {
|
194 | var _this = this;
|
195 | cancelNotifyTimeout(this);
|
196 | if (this.shouldNotify()) {
|
197 | this.listeners.forEach(function (listener) { return listener(_this); });
|
198 | }
|
199 | this.dirty = false;
|
200 | };
|
201 | QueryInfo.prototype.shouldNotify = function () {
|
202 | if (!this.dirty || !this.listeners.size) {
|
203 | return false;
|
204 | }
|
205 | if (isNetworkRequestInFlight(this.networkStatus) && this.observableQuery) {
|
206 | var fetchPolicy = this.observableQuery.options.fetchPolicy;
|
207 | if (fetchPolicy !== "cache-only" && fetchPolicy !== "cache-and-network") {
|
208 | return false;
|
209 | }
|
210 | }
|
211 | return true;
|
212 | };
|
213 | QueryInfo.prototype.stop = function () {
|
214 | if (!this.stopped) {
|
215 | this.stopped = true;
|
216 | // Cancel the pending notify timeout
|
217 | this.reset();
|
218 | this.cancel();
|
219 | // Revert back to the no-op version of cancel inherited from
|
220 | // QueryInfo.prototype.
|
221 | this.cancel = QueryInfo.prototype.cancel;
|
222 | var oq = this.observableQuery;
|
223 | if (oq)
|
224 | oq.stopPolling();
|
225 | }
|
226 | };
|
227 | // This method is a no-op by default, until/unless overridden by the
|
228 | // updateWatch method.
|
229 | QueryInfo.prototype.cancel = function () { };
|
230 | QueryInfo.prototype.updateWatch = function (variables) {
|
231 | var _this = this;
|
232 | if (variables === void 0) { variables = this.variables; }
|
233 | var oq = this.observableQuery;
|
234 | if (oq && oq.options.fetchPolicy === "no-cache") {
|
235 | return;
|
236 | }
|
237 | var watchOptions = __assign(__assign({}, this.getDiffOptions(variables)), { watcher: this, callback: function (diff) { return _this.setDiff(diff); } });
|
238 | if (!this.lastWatch || !equal(watchOptions, this.lastWatch)) {
|
239 | this.cancel();
|
240 | this.cancel = this.cache.watch((this.lastWatch = watchOptions));
|
241 | }
|
242 | };
|
243 | QueryInfo.prototype.resetLastWrite = function () {
|
244 | this.lastWrite = void 0;
|
245 | };
|
246 | QueryInfo.prototype.shouldWrite = function (result, variables) {
|
247 | var lastWrite = this.lastWrite;
|
248 | return !(lastWrite &&
|
249 | // If cache.evict has been called since the last time we wrote this
|
250 | // data into the cache, there's a chance writing this result into
|
251 | // the cache will repair what was evicted.
|
252 | lastWrite.dmCount === destructiveMethodCounts.get(this.cache) &&
|
253 | equal(variables, lastWrite.variables) &&
|
254 | equal(result.data, lastWrite.result.data));
|
255 | };
|
256 | QueryInfo.prototype.markResult = function (result, document, options, cacheWriteBehavior) {
|
257 | var _this = this;
|
258 | var merger = new DeepMerger();
|
259 | var graphQLErrors = isNonEmptyArray(result.errors) ? result.errors.slice(0) : [];
|
260 | // Cancel the pending notify timeout (if it exists) to prevent extraneous network
|
261 | // requests. To allow future notify timeouts, diff and dirty are reset as well.
|
262 | this.reset();
|
263 | if ("incremental" in result && isNonEmptyArray(result.incremental)) {
|
264 | var mergedData = mergeIncrementalData(this.getDiff().result, result);
|
265 | result.data = mergedData;
|
266 | // Detect the first chunk of a deferred query and merge it with existing
|
267 | // cache data. This ensures a `cache-first` fetch policy that returns
|
268 | // partial cache data or a `cache-and-network` fetch policy that already
|
269 | // has full data in the cache does not complain when trying to merge the
|
270 | // initial deferred server data with existing cache data.
|
271 | }
|
272 | else if ("hasNext" in result && result.hasNext) {
|
273 | var diff = this.getDiff();
|
274 | result.data = merger.merge(diff.result, result.data);
|
275 | }
|
276 | this.graphQLErrors = graphQLErrors;
|
277 | if (options.fetchPolicy === "no-cache") {
|
278 | this.updateLastDiff({ result: result.data, complete: true }, this.getDiffOptions(options.variables));
|
279 | }
|
280 | else if (cacheWriteBehavior !== 0 /* CacheWriteBehavior.FORBID */) {
|
281 | if (shouldWriteResult(result, options.errorPolicy)) {
|
282 | // Using a transaction here so we have a chance to read the result
|
283 | // back from the cache before the watch callback fires as a result
|
284 | // of writeQuery, so we can store the new diff quietly and ignore
|
285 | // it when we receive it redundantly from the watch callback.
|
286 | this.cache.performTransaction(function (cache) {
|
287 | if (_this.shouldWrite(result, options.variables)) {
|
288 | cache.writeQuery({
|
289 | query: document,
|
290 | data: result.data,
|
291 | variables: options.variables,
|
292 | overwrite: cacheWriteBehavior === 1 /* CacheWriteBehavior.OVERWRITE */,
|
293 | });
|
294 | _this.lastWrite = {
|
295 | result: result,
|
296 | variables: options.variables,
|
297 | dmCount: destructiveMethodCounts.get(_this.cache),
|
298 | };
|
299 | }
|
300 | else {
|
301 | // If result is the same as the last result we received from
|
302 | // the network (and the variables match too), avoid writing
|
303 | // result into the cache again. The wisdom of skipping this
|
304 | // cache write is far from obvious, since any cache write
|
305 | // could be the one that puts the cache back into a desired
|
306 | // state, fixing corruption or missing data. However, if we
|
307 | // always write every network result into the cache, we enable
|
308 | // feuds between queries competing to update the same data in
|
309 | // incompatible ways, which can lead to an endless cycle of
|
310 | // cache broadcasts and useless network requests. As with any
|
311 | // feud, eventually one side must step back from the brink,
|
312 | // letting the other side(s) have the last word(s). There may
|
313 | // be other points where we could break this cycle, such as
|
314 | // silencing the broadcast for cache.writeQuery (not a good
|
315 | // idea, since it just delays the feud a bit) or somehow
|
316 | // avoiding the network request that just happened (also bad,
|
317 | // because the server could return useful new data). All
|
318 | // options considered, skipping this cache write seems to be
|
319 | // the least damaging place to break the cycle, because it
|
320 | // reflects the intuition that we recently wrote this exact
|
321 | // result into the cache, so the cache *should* already/still
|
322 | // contain this data. If some other query has clobbered that
|
323 | // data in the meantime, that's too bad, but there will be no
|
324 | // winners if every query blindly reverts to its own version
|
325 | // of the data. This approach also gives the network a chance
|
326 | // to return new data, which will be written into the cache as
|
327 | // usual, notifying only those queries that are directly
|
328 | // affected by the cache updates, as usual. In the future, an
|
329 | // even more sophisticated cache could perhaps prevent or
|
330 | // mitigate the clobbering somehow, but that would make this
|
331 | // particular cache write even less important, and thus
|
332 | // skipping it would be even safer than it is today.
|
333 | if (_this.lastDiff && _this.lastDiff.diff.complete) {
|
334 | // Reuse data from the last good (complete) diff that we
|
335 | // received, when possible.
|
336 | result.data = _this.lastDiff.diff.result;
|
337 | return;
|
338 | }
|
339 | // If the previous this.diff was incomplete, fall through to
|
340 | // re-reading the latest data with cache.diff, below.
|
341 | }
|
342 | var diffOptions = _this.getDiffOptions(options.variables);
|
343 | var diff = cache.diff(diffOptions);
|
344 | // In case the QueryManager stops this QueryInfo before its
|
345 | // results are delivered, it's important to avoid restarting the
|
346 | // cache watch when markResult is called. We also avoid updating
|
347 | // the watch if we are writing a result that doesn't match the current
|
348 | // variables to avoid race conditions from broadcasting the wrong
|
349 | // result.
|
350 | if (!_this.stopped && equal(_this.variables, options.variables)) {
|
351 | // Any time we're about to update this.diff, we need to make
|
352 | // sure we've started watching the cache.
|
353 | _this.updateWatch(options.variables);
|
354 | }
|
355 | // If we're allowed to write to the cache, and we can read a
|
356 | // complete result from the cache, update result.data to be the
|
357 | // result from the cache, rather than the raw network result.
|
358 | // Set without setDiff to avoid triggering a notify call, since
|
359 | // we have other ways of notifying for this result.
|
360 | _this.updateLastDiff(diff, diffOptions);
|
361 | if (diff.complete) {
|
362 | result.data = diff.result;
|
363 | }
|
364 | });
|
365 | }
|
366 | else {
|
367 | this.lastWrite = void 0;
|
368 | }
|
369 | }
|
370 | };
|
371 | QueryInfo.prototype.markReady = function () {
|
372 | this.networkError = null;
|
373 | return (this.networkStatus = NetworkStatus.ready);
|
374 | };
|
375 | QueryInfo.prototype.markError = function (error) {
|
376 | this.networkStatus = NetworkStatus.error;
|
377 | this.lastWrite = void 0;
|
378 | this.reset();
|
379 | if (error.graphQLErrors) {
|
380 | this.graphQLErrors = error.graphQLErrors;
|
381 | }
|
382 | if (error.networkError) {
|
383 | this.networkError = error.networkError;
|
384 | }
|
385 | return error;
|
386 | };
|
387 | return QueryInfo;
|
388 | }());
|
389 | export { QueryInfo };
|
390 | export function shouldWriteResult(result, errorPolicy) {
|
391 | if (errorPolicy === void 0) { errorPolicy = "none"; }
|
392 | var ignoreErrors = errorPolicy === "ignore" || errorPolicy === "all";
|
393 | var writeWithErrors = !graphQLResultHasError(result);
|
394 | if (!writeWithErrors && ignoreErrors && result.data) {
|
395 | writeWithErrors = true;
|
396 | }
|
397 | return writeWithErrors;
|
398 | }
|
399 | //# sourceMappingURL=QueryInfo.js.map |
\ | No newline at end of file |