gqty
Version:
The No-GraphQL Client for TypeScript
314 lines (307 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
const frailMap = require('frail-map');
const set = require('just-safe-set');
const multidict = require('multidict');
const skeleton = require('../Accessor/skeleton.js');
const deepCopy = require('../Helpers/deepCopy.js');
require('graphql');
require('../Utils/hash.js');
const select = require('../Helpers/select.js');
require('just-safe-get');
const crawl = require('./crawl.js');
const normalization = require('./normalization.js');
const persistence = require('./persistence.js');
const utils = require('./utils.js');
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
const set__default = /*#__PURE__*/_interopDefault(set);
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: normalization$1
} = {}) {
__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.FrailMap());
/** Look up table for normalized objects. */
__privateAdd(this, _normalizedObjects, new frailMap.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.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.select(value, parts, (node2) => {
var _a2;
if ((getId == null ? void 0 : getId(node2)) && utils.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.crawl(value, (node) => {
if (getId(node) && utils.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.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$1) {
__privateSet(this, _normalizationOptions, normalization$1 === true ? normalization.defaultNormalizationHandler : Object.freeze({ ...normalization$1 }));
}
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 = persistence.importCacheSnapshot(data, this.normalizationOptions)) != null ? _a : {};
__privateSet(this, _normalizedObjects, normalizedObjects != null ? normalizedObjects : new frailMap.FrailMap());
__privateSet(this, _data, new frailMap.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.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 = normalization.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.crawl(
[...__privateGet(this, _data)].reduce((prev, [key, { data }]) => {
set__default["default"](prev, key, data);
return prev;
}, {}),
(it, key, obj) => {
if (skeleton.isSkeleton(it)) {
Reflect.deleteProperty(obj, key);
}
}
)
);
if (this.normalizationOptions) {
return persistence.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.select(
(_b = this.get(`${type}.${field}`, { includeExpired: true })) == null ? void 0 : _b.data,
parts,
(node) => {
const id = getId(node);
if (id && store.has(id) && utils.isCacheObject(node)) {
nsubs.set(node, fn);
}
return node;
}
);
}
};
_notifySubscribers = new WeakMap();
exports.Cache = Cache;