UNPKG

24.2 kBJavaScriptView Raw
1import { __assign, __extends } from "tslib";
2import { invariant } from "../../utilities/globals/index.js";
3// Make builtins like Map and Set safe to use with non-extensible objects.
4import "./fixPolyfills.js";
5import { wrap } from "optimism";
6import { equal } from "@wry/equality";
7import { ApolloCache } from "../core/cache.js";
8import { MissingFieldError } from "../core/types/common.js";
9import { addTypenameToDocument, isReference, DocumentTransform, canonicalStringify, print, cacheSizes, } from "../../utilities/index.js";
10import { StoreReader } from "./readFromStore.js";
11import { StoreWriter } from "./writeToStore.js";
12import { EntityStore, supportsResultCaching } from "./entityStore.js";
13import { makeVar, forgetCache, recallCache } from "./reactiveVars.js";
14import { Policies } from "./policies.js";
15import { hasOwn, normalizeConfig, shouldCanonizeResults } from "./helpers.js";
16import { getInMemoryCacheMemoryInternals } from "../../utilities/caching/getMemoryInternals.js";
17var 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));
468export { InMemoryCache };
469if (globalThis.__DEV__ !== false) {
470 InMemoryCache.prototype.getMemoryInternals = getInMemoryCacheMemoryInternals;
471}
472//# sourceMappingURL=inMemoryCache.js.map
\No newline at end of file