1 | import { __assign, __extends } from "tslib";
|
2 | import { invariant } from "../../utilities/globals/index.js";
|
3 | // Make builtins like Map and Set safe to use with non-extensible objects.
|
4 | import "./fixPolyfills.js";
|
5 | import { wrap } from "optimism";
|
6 | import { equal } from "@wry/equality";
|
7 | import { ApolloCache } from "../core/cache.js";
|
8 | import { MissingFieldError } from "../core/types/common.js";
|
9 | import { addTypenameToDocument, isReference, DocumentTransform, canonicalStringify, print, cacheSizes, } from "../../utilities/index.js";
|
10 | import { StoreReader } from "./readFromStore.js";
|
11 | import { StoreWriter } from "./writeToStore.js";
|
12 | import { EntityStore, supportsResultCaching } from "./entityStore.js";
|
13 | import { makeVar, forgetCache, recallCache } from "./reactiveVars.js";
|
14 | import { Policies } from "./policies.js";
|
15 | import { hasOwn, normalizeConfig, shouldCanonizeResults } from "./helpers.js";
|
16 | import { getInMemoryCacheMemoryInternals } from "../../utilities/caching/getMemoryInternals.js";
|
17 | var InMemoryCache = /** @class */ (function (_super) {
|
18 | __extends(InMemoryCache, _super);
|
19 | function InMemoryCache(config) {
|
20 | if (config === void 0) { config = {}; }
|
21 | var _this = _super.call(this) || this;
|
22 | _this.watches = new Set();
|
23 | _this.addTypenameTransform = new DocumentTransform(addTypenameToDocument);
|
24 | // Override the default value, since InMemoryCache result objects are frozen
|
25 | // in development and expected to remain logically immutable in production.
|
26 | _this.assumeImmutableResults = true;
|
27 | _this.makeVar = makeVar;
|
28 | _this.txCount = 0;
|
29 | _this.config = normalizeConfig(config);
|
30 | _this.addTypename = !!_this.config.addTypename;
|
31 | _this.policies = new Policies({
|
32 | cache: _this,
|
33 | dataIdFromObject: _this.config.dataIdFromObject,
|
34 | possibleTypes: _this.config.possibleTypes,
|
35 | typePolicies: _this.config.typePolicies,
|
36 | });
|
37 | _this.init();
|
38 | return _this;
|
39 | }
|
40 | InMemoryCache.prototype.init = function () {
|
41 | // Passing { resultCaching: false } in the InMemoryCache constructor options
|
42 | // will completely disable dependency tracking, which will improve memory
|
43 | // usage but worsen the performance of repeated reads.
|
44 | var rootStore = (this.data = new EntityStore.Root({
|
45 | policies: this.policies,
|
46 | resultCaching: this.config.resultCaching,
|
47 | }));
|
48 | // When no optimistic writes are currently active, cache.optimisticData ===
|
49 | // cache.data, so there are no additional layers on top of the actual data.
|
50 | // When an optimistic update happens, this.optimisticData will become a
|
51 | // linked list of EntityStore Layer objects that terminates with the
|
52 | // original this.data cache object.
|
53 | this.optimisticData = rootStore.stump;
|
54 | this.resetResultCache();
|
55 | };
|
56 | InMemoryCache.prototype.resetResultCache = function (resetResultIdentities) {
|
57 | var _this = this;
|
58 | var previousReader = this.storeReader;
|
59 | var fragments = this.config.fragments;
|
60 | // The StoreWriter is mostly stateless and so doesn't really need to be
|
61 | // reset, but it does need to have its writer.storeReader reference updated,
|
62 | // so it's simpler to update this.storeWriter as well.
|
63 | this.storeWriter = new StoreWriter(this, (this.storeReader = new StoreReader({
|
64 | cache: this,
|
65 | addTypename: this.addTypename,
|
66 | resultCacheMaxSize: this.config.resultCacheMaxSize,
|
67 | canonizeResults: shouldCanonizeResults(this.config),
|
68 | canon: resetResultIdentities ? void 0 : (previousReader && previousReader.canon),
|
69 | fragments: fragments,
|
70 | })), fragments);
|
71 | this.maybeBroadcastWatch = wrap(function (c, options) {
|
72 | return _this.broadcastWatch(c, options);
|
73 | }, {
|
74 | max: this.config.resultCacheMaxSize ||
|
75 | cacheSizes["inMemoryCache.maybeBroadcastWatch"] ||
|
76 | 5000 /* defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"] */,
|
77 | makeCacheKey: function (c) {
|
78 | // Return a cache key (thus enabling result caching) only if we're
|
79 | // currently using a data store that can track cache dependencies.
|
80 | var store = c.optimistic ? _this.optimisticData : _this.data;
|
81 | if (supportsResultCaching(store)) {
|
82 | var optimistic = c.optimistic, id = c.id, variables = c.variables;
|
83 | return store.makeCacheKey(c.query,
|
84 | // Different watches can have the same query, optimistic
|
85 | // status, rootId, and variables, but if their callbacks are
|
86 | // different, the (identical) result needs to be delivered to
|
87 | // each distinct callback. The easiest way to achieve that
|
88 | // separation is to include c.callback in the cache key for
|
89 | // maybeBroadcastWatch calls. See issue #5733.
|
90 | c.callback, canonicalStringify({ optimistic: optimistic, id: id, variables: variables }));
|
91 | }
|
92 | },
|
93 | });
|
94 | // Since we have thrown away all the cached functions that depend on the
|
95 | // CacheGroup dependencies maintained by EntityStore, we should also reset
|
96 | // all CacheGroup dependency information.
|
97 | new Set([this.data.group, this.optimisticData.group]).forEach(function (group) {
|
98 | return group.resetCaching();
|
99 | });
|
100 | };
|
101 | InMemoryCache.prototype.restore = function (data) {
|
102 | this.init();
|
103 | // Since calling this.init() discards/replaces the entire StoreReader, along
|
104 | // with the result caches it maintains, this.data.replace(data) won't have
|
105 | // to bother deleting the old data.
|
106 | if (data)
|
107 | this.data.replace(data);
|
108 | return this;
|
109 | };
|
110 | InMemoryCache.prototype.extract = function (optimistic) {
|
111 | if (optimistic === void 0) { optimistic = false; }
|
112 | return (optimistic ? this.optimisticData : this.data).extract();
|
113 | };
|
114 | InMemoryCache.prototype.read = function (options) {
|
115 | var
|
116 | // Since read returns data or null, without any additional metadata
|
117 | // about whether/where there might have been missing fields, the
|
118 | // default behavior cannot be returnPartialData = true (like it is
|
119 | // for the diff method), since defaulting to true would violate the
|
120 | // integrity of the T in the return type. However, partial data may
|
121 | // be useful in some cases, so returnPartialData:true may be
|
122 | // specified explicitly.
|
123 | _a = options.returnPartialData,
|
124 | // Since read returns data or null, without any additional metadata
|
125 | // about whether/where there might have been missing fields, the
|
126 | // default behavior cannot be returnPartialData = true (like it is
|
127 | // for the diff method), since defaulting to true would violate the
|
128 | // integrity of the T in the return type. However, partial data may
|
129 | // be useful in some cases, so returnPartialData:true may be
|
130 | // specified explicitly.
|
131 | returnPartialData = _a === void 0 ? false : _a;
|
132 | try {
|
133 | return (this.storeReader.diffQueryAgainstStore(__assign(__assign({}, options), { store: options.optimistic ? this.optimisticData : this.data, config: this.config, returnPartialData: returnPartialData })).result || null);
|
134 | }
|
135 | catch (e) {
|
136 | if (e instanceof MissingFieldError) {
|
137 | // Swallow MissingFieldError and return null, so callers do not need to
|
138 | // worry about catching "normal" exceptions resulting from incomplete
|
139 | // cache data. Unexpected errors will be re-thrown. If you need more
|
140 | // information about which fields were missing, use cache.diff instead,
|
141 | // and examine diffResult.missing.
|
142 | return null;
|
143 | }
|
144 | throw e;
|
145 | }
|
146 | };
|
147 | InMemoryCache.prototype.write = function (options) {
|
148 | try {
|
149 | ++this.txCount;
|
150 | return this.storeWriter.writeToStore(this.data, options);
|
151 | }
|
152 | finally {
|
153 | if (!--this.txCount && options.broadcast !== false) {
|
154 | this.broadcastWatches();
|
155 | }
|
156 | }
|
157 | };
|
158 | InMemoryCache.prototype.modify = function (options) {
|
159 | if (hasOwn.call(options, "id") && !options.id) {
|
160 | // To my knowledge, TypeScript does not currently provide a way to
|
161 | // enforce that an optional property?:type must *not* be undefined
|
162 | // when present. That ability would be useful here, because we want
|
163 | // options.id to default to ROOT_QUERY only when no options.id was
|
164 | // provided. If the caller attempts to pass options.id with a
|
165 | // falsy/undefined value (perhaps because cache.identify failed), we
|
166 | // should not assume the goal was to modify the ROOT_QUERY object.
|
167 | // We could throw, but it seems natural to return false to indicate
|
168 | // that nothing was modified.
|
169 | return false;
|
170 | }
|
171 | var store = ((options.optimistic) // Defaults to false.
|
172 | ) ?
|
173 | this.optimisticData
|
174 | : this.data;
|
175 | try {
|
176 | ++this.txCount;
|
177 | return store.modify(options.id || "ROOT_QUERY", options.fields);
|
178 | }
|
179 | finally {
|
180 | if (!--this.txCount && options.broadcast !== false) {
|
181 | this.broadcastWatches();
|
182 | }
|
183 | }
|
184 | };
|
185 | InMemoryCache.prototype.diff = function (options) {
|
186 | return this.storeReader.diffQueryAgainstStore(__assign(__assign({}, options), { store: options.optimistic ? this.optimisticData : this.data, rootId: options.id || "ROOT_QUERY", config: this.config }));
|
187 | };
|
188 | InMemoryCache.prototype.watch = function (watch) {
|
189 | var _this = this;
|
190 | if (!this.watches.size) {
|
191 | // In case we previously called forgetCache(this) because
|
192 | // this.watches became empty (see below), reattach this cache to any
|
193 | // reactive variables on which it previously depended. It might seem
|
194 | // paradoxical that we're able to recall something we supposedly
|
195 | // forgot, but the point of calling forgetCache(this) is to silence
|
196 | // useless broadcasts while this.watches is empty, and to allow the
|
197 | // cache to be garbage collected. If, however, we manage to call
|
198 | // recallCache(this) here, this cache object must not have been
|
199 | // garbage collected yet, and should resume receiving updates from
|
200 | // reactive variables, now that it has a watcher to notify.
|
201 | recallCache(this);
|
202 | }
|
203 | this.watches.add(watch);
|
204 | if (watch.immediate) {
|
205 | this.maybeBroadcastWatch(watch);
|
206 | }
|
207 | return function () {
|
208 | // Once we remove the last watch from this.watches, cache.broadcastWatches
|
209 | // no longer does anything, so we preemptively tell the reactive variable
|
210 | // system to exclude this cache from future broadcasts.
|
211 | if (_this.watches.delete(watch) && !_this.watches.size) {
|
212 | forgetCache(_this);
|
213 | }
|
214 | // Remove this watch from the LRU cache managed by the
|
215 | // maybeBroadcastWatch OptimisticWrapperFunction, to prevent memory
|
216 | // leaks involving the closure of watch.callback.
|
217 | _this.maybeBroadcastWatch.forget(watch);
|
218 | };
|
219 | };
|
220 | InMemoryCache.prototype.gc = function (options) {
|
221 | var _a;
|
222 | canonicalStringify.reset();
|
223 | print.reset();
|
224 | this.addTypenameTransform.resetCache();
|
225 | (_a = this.config.fragments) === null || _a === void 0 ? void 0 : _a.resetCaches();
|
226 | var ids = this.optimisticData.gc();
|
227 | if (options && !this.txCount) {
|
228 | if (options.resetResultCache) {
|
229 | this.resetResultCache(options.resetResultIdentities);
|
230 | }
|
231 | else if (options.resetResultIdentities) {
|
232 | this.storeReader.resetCanon();
|
233 | }
|
234 | }
|
235 | return ids;
|
236 | };
|
237 | // Call this method to ensure the given root ID remains in the cache after
|
238 | // garbage collection, along with its transitive child entities. Note that
|
239 | // the cache automatically retains all directly written entities. By default,
|
240 | // the retainment persists after optimistic updates are removed. Pass true
|
241 | // for the optimistic argument if you would prefer for the retainment to be
|
242 | // discarded when the top-most optimistic layer is removed. Returns the
|
243 | // resulting (non-negative) retainment count.
|
244 | InMemoryCache.prototype.retain = function (rootId, optimistic) {
|
245 | return (optimistic ? this.optimisticData : this.data).retain(rootId);
|
246 | };
|
247 | // Call this method to undo the effect of the retain method, above. Once the
|
248 | // retainment count falls to zero, the given ID will no longer be preserved
|
249 | // during garbage collection, though it may still be preserved by other safe
|
250 | // entities that refer to it. Returns the resulting (non-negative) retainment
|
251 | // count, in case that's useful.
|
252 | InMemoryCache.prototype.release = function (rootId, optimistic) {
|
253 | return (optimistic ? this.optimisticData : this.data).release(rootId);
|
254 | };
|
255 | // Returns the canonical ID for a given StoreObject, obeying typePolicies
|
256 | // and keyFields (and dataIdFromObject, if you still use that). At minimum,
|
257 | // the object must contain a __typename and any primary key fields required
|
258 | // to identify entities of that type. If you pass a query result object, be
|
259 | // sure that none of the primary key fields have been renamed by aliasing.
|
260 | // If you pass a Reference object, its __ref ID string will be returned.
|
261 | InMemoryCache.prototype.identify = function (object) {
|
262 | if (isReference(object))
|
263 | return object.__ref;
|
264 | try {
|
265 | return this.policies.identify(object)[0];
|
266 | }
|
267 | catch (e) {
|
268 | globalThis.__DEV__ !== false && invariant.warn(e);
|
269 | }
|
270 | };
|
271 | InMemoryCache.prototype.evict = function (options) {
|
272 | if (!options.id) {
|
273 | if (hasOwn.call(options, "id")) {
|
274 | // See comment in modify method about why we return false when
|
275 | // options.id exists but is falsy/undefined.
|
276 | return false;
|
277 | }
|
278 | options = __assign(__assign({}, options), { id: "ROOT_QUERY" });
|
279 | }
|
280 | try {
|
281 | // It's unlikely that the eviction will end up invoking any other
|
282 | // cache update operations while it's running, but {in,de}crementing
|
283 | // this.txCount still seems like a good idea, for uniformity with
|
284 | // the other update methods.
|
285 | ++this.txCount;
|
286 | // Pass this.data as a limit on the depth of the eviction, so evictions
|
287 | // during optimistic updates (when this.data is temporarily set equal to
|
288 | // this.optimisticData) do not escape their optimistic Layer.
|
289 | return this.optimisticData.evict(options, this.data);
|
290 | }
|
291 | finally {
|
292 | if (!--this.txCount && options.broadcast !== false) {
|
293 | this.broadcastWatches();
|
294 | }
|
295 | }
|
296 | };
|
297 | InMemoryCache.prototype.reset = function (options) {
|
298 | var _this = this;
|
299 | this.init();
|
300 | canonicalStringify.reset();
|
301 | if (options && options.discardWatches) {
|
302 | // Similar to what happens in the unsubscribe function returned by
|
303 | // cache.watch, applied to all current watches.
|
304 | this.watches.forEach(function (watch) { return _this.maybeBroadcastWatch.forget(watch); });
|
305 | this.watches.clear();
|
306 | forgetCache(this);
|
307 | }
|
308 | else {
|
309 | // Calling this.init() above unblocks all maybeBroadcastWatch caching, so
|
310 | // this.broadcastWatches() triggers a broadcast to every current watcher
|
311 | // (letting them know their data is now missing). This default behavior is
|
312 | // convenient because it means the watches do not have to be manually
|
313 | // reestablished after resetting the cache. To prevent this broadcast and
|
314 | // cancel all watches, pass true for options.discardWatches.
|
315 | this.broadcastWatches();
|
316 | }
|
317 | return Promise.resolve();
|
318 | };
|
319 | InMemoryCache.prototype.removeOptimistic = function (idToRemove) {
|
320 | var newOptimisticData = this.optimisticData.removeLayer(idToRemove);
|
321 | if (newOptimisticData !== this.optimisticData) {
|
322 | this.optimisticData = newOptimisticData;
|
323 | this.broadcastWatches();
|
324 | }
|
325 | };
|
326 | InMemoryCache.prototype.batch = function (options) {
|
327 | var _this = this;
|
328 | var update = options.update, _a = options.optimistic, optimistic = _a === void 0 ? true : _a, removeOptimistic = options.removeOptimistic, onWatchUpdated = options.onWatchUpdated;
|
329 | var updateResult;
|
330 | var perform = function (layer) {
|
331 | var _a = _this, data = _a.data, optimisticData = _a.optimisticData;
|
332 | ++_this.txCount;
|
333 | if (layer) {
|
334 | _this.data = _this.optimisticData = layer;
|
335 | }
|
336 | try {
|
337 | return (updateResult = update(_this));
|
338 | }
|
339 | finally {
|
340 | --_this.txCount;
|
341 | _this.data = data;
|
342 | _this.optimisticData = optimisticData;
|
343 | }
|
344 | };
|
345 | var alreadyDirty = new Set();
|
346 | if (onWatchUpdated && !this.txCount) {
|
347 | // If an options.onWatchUpdated callback is provided, we want to call it
|
348 | // with only the Cache.WatchOptions objects affected by options.update,
|
349 | // but there might be dirty watchers already waiting to be broadcast that
|
350 | // have nothing to do with the update. To prevent including those watchers
|
351 | // in the post-update broadcast, we perform this initial broadcast to
|
352 | // collect the dirty watchers, so we can re-dirty them later, after the
|
353 | // post-update broadcast, allowing them to receive their pending
|
354 | // broadcasts the next time broadcastWatches is called, just as they would
|
355 | // if we never called cache.batch.
|
356 | this.broadcastWatches(__assign(__assign({}, options), { onWatchUpdated: function (watch) {
|
357 | alreadyDirty.add(watch);
|
358 | return false;
|
359 | } }));
|
360 | }
|
361 | if (typeof optimistic === "string") {
|
362 | // Note that there can be multiple layers with the same optimistic ID.
|
363 | // When removeOptimistic(id) is called for that id, all matching layers
|
364 | // will be removed, and the remaining layers will be reapplied.
|
365 | this.optimisticData = this.optimisticData.addLayer(optimistic, perform);
|
366 | }
|
367 | else if (optimistic === false) {
|
368 | // Ensure both this.data and this.optimisticData refer to the root
|
369 | // (non-optimistic) layer of the cache during the update. Note that
|
370 | // this.data could be a Layer if we are currently executing an optimistic
|
371 | // update function, but otherwise will always be an EntityStore.Root
|
372 | // instance.
|
373 | perform(this.data);
|
374 | }
|
375 | else {
|
376 | // Otherwise, leave this.data and this.optimisticData unchanged and run
|
377 | // the update with broadcast batching.
|
378 | perform();
|
379 | }
|
380 | if (typeof removeOptimistic === "string") {
|
381 | this.optimisticData = this.optimisticData.removeLayer(removeOptimistic);
|
382 | }
|
383 | // Note: if this.txCount > 0, then alreadyDirty.size === 0, so this code
|
384 | // takes the else branch and calls this.broadcastWatches(options), which
|
385 | // does nothing when this.txCount > 0.
|
386 | if (onWatchUpdated && alreadyDirty.size) {
|
387 | this.broadcastWatches(__assign(__assign({}, options), { onWatchUpdated: function (watch, diff) {
|
388 | var result = onWatchUpdated.call(this, watch, diff);
|
389 | if (result !== false) {
|
390 | // Since onWatchUpdated did not return false, this diff is
|
391 | // about to be broadcast to watch.callback, so we don't need
|
392 | // to re-dirty it with the other alreadyDirty watches below.
|
393 | alreadyDirty.delete(watch);
|
394 | }
|
395 | return result;
|
396 | } }));
|
397 | // Silently re-dirty any watches that were already dirty before the update
|
398 | // was performed, and were not broadcast just now.
|
399 | if (alreadyDirty.size) {
|
400 | alreadyDirty.forEach(function (watch) { return _this.maybeBroadcastWatch.dirty(watch); });
|
401 | }
|
402 | }
|
403 | else {
|
404 | // If alreadyDirty is empty or we don't have an onWatchUpdated
|
405 | // function, we don't need to go to the trouble of wrapping
|
406 | // options.onWatchUpdated.
|
407 | this.broadcastWatches(options);
|
408 | }
|
409 | return updateResult;
|
410 | };
|
411 | InMemoryCache.prototype.performTransaction = function (update, optimisticId) {
|
412 | return this.batch({
|
413 | update: update,
|
414 | optimistic: optimisticId || optimisticId !== null,
|
415 | });
|
416 | };
|
417 | InMemoryCache.prototype.transformDocument = function (document) {
|
418 | return this.addTypenameToDocument(this.addFragmentsToDocument(document));
|
419 | };
|
420 | InMemoryCache.prototype.broadcastWatches = function (options) {
|
421 | var _this = this;
|
422 | if (!this.txCount) {
|
423 | this.watches.forEach(function (c) { return _this.maybeBroadcastWatch(c, options); });
|
424 | }
|
425 | };
|
426 | InMemoryCache.prototype.addFragmentsToDocument = function (document) {
|
427 | var fragments = this.config.fragments;
|
428 | return fragments ? fragments.transform(document) : document;
|
429 | };
|
430 | InMemoryCache.prototype.addTypenameToDocument = function (document) {
|
431 | if (this.addTypename) {
|
432 | return this.addTypenameTransform.transformDocument(document);
|
433 | }
|
434 | return document;
|
435 | };
|
436 | // This method is wrapped by maybeBroadcastWatch, which is called by
|
437 | // broadcastWatches, so that we compute and broadcast results only when
|
438 | // the data that would be broadcast might have changed. It would be
|
439 | // simpler to check for changes after recomputing a result but before
|
440 | // broadcasting it, but this wrapping approach allows us to skip both
|
441 | // the recomputation and the broadcast, in most cases.
|
442 | InMemoryCache.prototype.broadcastWatch = function (c, options) {
|
443 | var lastDiff = c.lastDiff;
|
444 | // Both WatchOptions and DiffOptions extend ReadOptions, and DiffOptions
|
445 | // currently requires no additional properties, so we can use c (a
|
446 | // WatchOptions object) as DiffOptions, without having to allocate a new
|
447 | // object, and without having to enumerate the relevant properties (query,
|
448 | // variables, etc.) explicitly. There will be some additional properties
|
449 | // (lastDiff, callback, etc.), but cache.diff ignores them.
|
450 | var diff = this.diff(c);
|
451 | if (options) {
|
452 | if (c.optimistic && typeof options.optimistic === "string") {
|
453 | diff.fromOptimisticTransaction = true;
|
454 | }
|
455 | if (options.onWatchUpdated &&
|
456 | options.onWatchUpdated.call(this, c, diff, lastDiff) === false) {
|
457 | // Returning false from the onWatchUpdated callback will prevent
|
458 | // calling c.callback(diff) for this watcher.
|
459 | return;
|
460 | }
|
461 | }
|
462 | if (!lastDiff || !equal(lastDiff.result, diff.result)) {
|
463 | c.callback((c.lastDiff = diff), lastDiff);
|
464 | }
|
465 | };
|
466 | return InMemoryCache;
|
467 | }(ApolloCache));
|
468 | export { InMemoryCache };
|
469 | if (globalThis.__DEV__ !== false) {
|
470 | InMemoryCache.prototype.getMemoryInternals = getInMemoryCacheMemoryInternals;
|
471 | }
|
472 | //# sourceMappingURL=inMemoryCache.js.map |
\ | No newline at end of file |