UNPKG

gqty

Version:

The No-GraphQL Client for TypeScript

306 lines (303 loc) • 11.6 kB
import { FrailMap } from 'frail-map'; import set from 'just-safe-set'; import { MultiDict } from 'multidict'; import { isSkeleton } from '../Accessor/skeleton.mjs'; import { deepCopy } from '../Helpers/deepCopy.mjs'; import 'graphql'; import '../Utils/hash.mjs'; import { select } from '../Helpers/select.mjs'; import 'just-safe-get'; import { crawl } from './crawl.mjs'; import { defaultNormalizationHandler, deepNormalizeObject } from './normalization.mjs'; import { importCacheSnapshot, exportCacheSnapshot } from './persistence.mjs'; import { isCacheObject } from './utils.mjs'; var __typeError = (msg) => { throw TypeError(msg); }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var _maxAge, _staleWhileRevalidate, _normalizationOptions, _data, _normalizedObjects, _dataRefs, _subscriptions, _normalizedSubscriptions, _Cache_instances, subscribeNormalized_fn, _notifySubscribers; const MINIMUM_CACHE_AGE = 100; class Cache { constructor(data, { maxAge = Infinity, staleWhileRevalidate = 5 * 30 * 1e3, normalization } = {}) { __privateAdd(this, _Cache_instances); __privateAdd(this, _maxAge, Infinity); __privateAdd(this, _staleWhileRevalidate, 0); __privateAdd(this, _normalizationOptions); /** * The actual data cache. Cache keys are formatted as the first 2 layer of * the selection path, e.g. `['query', 'user']` for `query.user`. * * This enables a cache expiry/eviction strategy based on top-level queries. */ __privateAdd(this, _data, new FrailMap()); /** Look up table for normalized objects. */ __privateAdd(this, _normalizedObjects, new FrailMap()); /** Temporary strong references for the WeakRefs in FrailMap. */ __privateAdd(this, _dataRefs, /* @__PURE__ */ new Set()); /** Subscription paths and it's listener function. */ __privateAdd(this, _subscriptions, /* @__PURE__ */ new Map()); /** Subscription paths that reached a normalized object. */ __privateAdd(this, _normalizedSubscriptions, new MultiDict()); // [ ] Optimization // This is pretty inefficient, but maintaining an indexed tree is too much // effort right now. Accepting PRs. __privateAdd(this, _notifySubscribers, (value) => { var _a, _b; const listeners = /* @__PURE__ */ new Set(); const subs = __privateGet(this, _subscriptions); const nsubs = __privateGet(this, _normalizedSubscriptions); const getId = (_a = this.normalizationOptions) == null ? void 0 : _a.identity; for (const [paths, notify] of subs) { for (const path of paths) { const parts = path.split("."); const node = select(value, parts, (node2) => { var _a2; if ((getId == null ? void 0 : getId(node2)) && isCacheObject(node2)) { (_a2 = nsubs.get(node2)) == null ? void 0 : _a2.forEach((notify2) => { listeners.add(notify2); }); nsubs.set(node2, notify); } return node2; }); if (Array.isArray(node) ? node.flat(Infinity).some((item) => item !== void 0) : node !== void 0) { listeners.add(notify); break; } } } if (getId) { const norbjs = /* @__PURE__ */ new Set(); crawl(value, (node) => { if (getId(node) && isCacheObject(node)) { norbjs.add(node); } }); const resubscribingListeners = /* @__PURE__ */ new Set(); for (const norbj of norbjs) { for (const listener of (_b = nsubs.get(norbj)) != null ? _b : []) { listeners.add(listener); resubscribingListeners.add(listener); } } for (const listener of resubscribingListeners) { for (const [paths, _listener] of subs) { if (listener === _listener) { __privateMethod(this, _Cache_instances, subscribeNormalized_fn).call(this, paths, listener); } } } } if (listeners.size > 0) { const valueSnapshot = deepCopy(value); for (const notify of listeners) { notify(valueSnapshot); } } }); __privateSet(this, _maxAge, Math.max(maxAge, MINIMUM_CACHE_AGE)); __privateSet(this, _staleWhileRevalidate, Math.max(staleWhileRevalidate, 0)); if (normalization) { __privateSet(this, _normalizationOptions, normalization === true ? defaultNormalizationHandler : Object.freeze({ ...normalization })); } if (data) { this.restore(data); } } /** * Maximum age of cache data in milliseconds, expired data nodes are subjected * to garbage collection. */ get maxAge() { return __privateGet(this, _maxAge); } /** * Maximum time in milliseconds to keep stale data in cache, while allowing * stale-while-revalidate background fetches. */ get staleWhileRevalidate() { return __privateGet(this, _staleWhileRevalidate); } get normalizationOptions() { return __privateGet(this, _normalizationOptions); } restore(data) { var _a; const { query, mutation, subscription, normalizedObjects } = (_a = importCacheSnapshot(data, this.normalizationOptions)) != null ? _a : {}; __privateSet(this, _normalizedObjects, normalizedObjects != null ? normalizedObjects : new FrailMap()); __privateSet(this, _data, new FrailMap()); this.set({ query, mutation, subscription }, { skipNotify: true }); } /** Subscribe to cache changes. */ subscribe(paths, fn) { const pathsSnapshot = Object.freeze([...paths]); __privateGet(this, _subscriptions).set(pathsSnapshot, fn); __privateMethod(this, _Cache_instances, subscribeNormalized_fn).call(this, pathsSnapshot, fn); return () => { __privateGet(this, _subscriptions).delete(pathsSnapshot); __privateGet(this, _normalizedSubscriptions).delete(fn); }; } /** * Retrieve cache values by first 2 path segments, e.g. `query.todos` or * `mutation.createTodo`. */ get(path, options) { var _a; const [, type, key, subpath] = (_a = path.match(/^([a-z]+(?:\w*))\.(?:__)?([a-z]+(?:\w*))(.*[^.])?$/i)) != null ? _a : []; if (!type || !key) { throw new ReferenceError( "Cache path must starts with `${type}.`: " + path ); } const cacheKey = `${type}.${key}`; const dataContainer = __privateGet(this, _data).get(cacheKey); if (dataContainer === void 0) return; const { expiresAt, swrBefore } = dataContainer; let { data } = dataContainer; if (expiresAt < Date.now() && !(options == null ? void 0 : options.includeExpired)) { data = void 0; } else if (subpath) { data = select(data, subpath.slice(1).split(".")); } return { data, get expiresAt() { return expiresAt; }, get swrBefore() { return swrBefore; } }; } /** * Merge objects into the current cache, recursively normalize incoming values * if normalization is enabled. Notifies cache listeners afterwards. * * Example value: `{ query: { foo: "bar" } }` */ set(values, { skipNotify = false } = {}) { var _a; const age = this.maxAge; const swr = this.staleWhileRevalidate; const now = Date.now(); if (this.normalizationOptions) { values = deepNormalizeObject(values, { ...this.normalizationOptions, store: __privateGet(this, _normalizedObjects) }); } for (const [type, cacheObjects = {}] of Object.entries(values)) { for (const [field, data] of Object.entries(cacheObjects)) { const cacheKey = `${type}.${field}`; let unrefTimer; const unref = () => { clearTimeout(unrefTimer); __privateGet(this, _dataRefs).delete(dataContainer); }; const dataContainer = ( // Mutation and subscription results should be returned right away for // immediate use. Their responses are only meaningful to a cache with // normalization enabled, where it already updates listeners. // // We force a short expiration here to let it survive the next render. type === "mutation" || type === "subscription" ? { data, expiresAt: now + 100, swrBefore: now, unref } : { data, expiresAt: age + now, swrBefore: age + swr + now, unref } ); const existing = __privateGet(this, _data).get(cacheKey); if (existing) { (_a = existing.unref) == null ? void 0 : _a.call(existing); Object.assign(existing, dataContainer); } else { __privateGet(this, _data).set(cacheKey, dataContainer, { strong: !isFinite(age) }); } if (isFinite(age + swr)) { unrefTimer = setTimeout(unref, age + swr); if (typeof unrefTimer === "object") { unrefTimer.unref(); } __privateGet(this, _dataRefs).add(dataContainer); } } } if (!skipNotify) { __privateGet(this, _notifySubscribers).call(this, values); } } clear() { __privateGet(this, _data).clear(); __privateGet(this, _normalizedObjects).clear(); __privateGet(this, _dataRefs).clear(); } toJSON() { const snapshot = ( // Remove skeletons crawl( [...__privateGet(this, _data)].reduce((prev, [key, { data }]) => { set(prev, key, data); return prev; }, {}), (it, key, obj) => { if (isSkeleton(it)) { Reflect.deleteProperty(obj, key); } } ) ); if (this.normalizationOptions) { return exportCacheSnapshot(snapshot, this.normalizationOptions); } else { return snapshot; } } } _maxAge = new WeakMap(); _staleWhileRevalidate = new WeakMap(); _normalizationOptions = new WeakMap(); _data = new WeakMap(); _normalizedObjects = new WeakMap(); _dataRefs = new WeakMap(); _subscriptions = new WeakMap(); _normalizedSubscriptions = new WeakMap(); _Cache_instances = new WeakSet(); subscribeNormalized_fn = function(paths, fn) { var _a, _b; const getId = (_a = this.normalizationOptions) == null ? void 0 : _a.identity; if (!getId) return; const store = __privateGet(this, _normalizedObjects); const nsubs = __privateGet(this, _normalizedSubscriptions); nsubs.delete(fn); for (const path of paths) { const [type, field, ...parts] = path.split("."); select( (_b = this.get(`${type}.${field}`, { includeExpired: true })) == null ? void 0 : _b.data, parts, (node) => { const id = getId(node); if (id && store.has(id) && isCacheObject(node)) { nsubs.set(node, fn); } return node; } ); } }; _notifySubscribers = new WeakMap(); export { Cache };