fn-merge-cache
Version:
`FnMergeCache` is a caching utility that allows functions to cache their results based on input arguments, with options for cache lifetime, size limits, error handling, and parameter comparison, while supporting cache invalidation via tags and global reva
240 lines (234 loc) • 8.55 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
import _createForOfIteratorHelper from "@babel/runtime/helpers/esm/createForOfIteratorHelper";
import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck";
import _createClass from "@babel/runtime/helpers/esm/createClass";
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
import { isEqual, throttle } from "lodash-es";
import { EventEmitter } from "events";
// hack for next.js edge runtime
var _queueMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : function (fn) {
return Promise.resolve().then(fn);
};
var revalidateEE = new EventEmitter();
var revalidateAllStr = "__FN_MERGE_CACHE_INSIDE__all";
/**
* A class for merging and caching function calls
*
* @typeParam A - Tuple type of function parameters
* @typeParam R - Function return type
*/
export var FnMergeCache = /*#__PURE__*/function () {
/**
* Creates a new FnMergeCache instance
*
* @param fn - The original function to be cached
* @param options - Configuration options
* @param options.cache - Whether to enable caching
* @param options.cacheOnError - Whether to cache results when errors occur
* @param options.argComparer - Parameter comparison function, returns true if parameters are equal
* @param options.ttl - Cache lifetime in milliseconds, 0 means never expires
* @param options.maxCacheSize - Cache pool size limit, 0 means no limit
* @param options.tags - Tags for cache revalidation
*
* @throws Error when using reserved tag names
*/
function FnMergeCache(fn) {
var _this = this;
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$cache = _ref.cache,
cache = _ref$cache === void 0 ? true : _ref$cache,
_ref$cacheOnError = _ref.cacheOnError,
cacheOnError = _ref$cacheOnError === void 0 ? false : _ref$cacheOnError,
_ref$argComparer = _ref.argComparer,
argComparer = _ref$argComparer === void 0 ? isEqual : _ref$argComparer,
_ref$ttl = _ref.ttl,
ttl = _ref$ttl === void 0 ? 0 : _ref$ttl,
_ref$maxCacheSize = _ref.maxCacheSize,
maxCacheSize = _ref$maxCacheSize === void 0 ? 0 : _ref$maxCacheSize,
_ref$tags = _ref.tags,
tags = _ref$tags === void 0 ? [] : _ref$tags;
_classCallCheck(this, FnMergeCache);
_defineProperty(this, "_disposed", false);
_defineProperty(this, "_fn", void 0);
_defineProperty(this, "_cache", void 0);
_defineProperty(this, "_cacheOnError", void 0);
_defineProperty(this, "_argComparer", void 0);
_defineProperty(this, "_ttl", void 0);
_defineProperty(this, "_maxCacheSize", void 0);
_defineProperty(this, "_tags", void 0);
_defineProperty(this, "_result", new Map());
// arg: [time, result, error]
_defineProperty(this, "_callGC", throttle(function () {
var now = Date.now();
var _iterator = _createForOfIteratorHelper(_this._result),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _step$value = _slicedToArray(_step.value, 2),
k = _step$value[0],
v = _step$value[1];
if (_this._ttl && now - v[0] > _this._ttl || _this._maxCacheSize && _this._result.size > _this._maxCacheSize) {
_this._result.delete(k);
} else {
// Since it is sorted by time, you can exit once you encounter an unexpired one.
break;
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}, 1000));
/**
* Clears all cached results
*/
_defineProperty(this, "revalidate", function () {
_this._result.clear();
});
if (tags.includes(revalidateAllStr)) {
throw new Error("Tag name \"".concat(revalidateAllStr, "\" is reserved, please use another tag name"));
}
this._fn = fn;
this._cache = cache;
this._cacheOnError = cacheOnError;
this._argComparer = argComparer;
this._ttl = ttl;
this._maxCacheSize = maxCacheSize;
this._tags = tags;
revalidateEE.on(revalidateAllStr, this.revalidate);
tags.forEach(function (tag) {
return revalidateEE.on(tag, _this.revalidate);
});
}
/**
* Calls the cached function
*
* @param args - Arguments passed to the original function
* @returns Function return value, may be cached result
* @throws Error if instance is disposed or original function throws
*/
_createClass(FnMergeCache, [{
key: "call",
value: function call() {
var _this2 = this;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
if (this._disposed) {
throw new Error("FnMergeCache instance has been disposed");
}
if (this._cache && (this._ttl || this._maxCacheSize)) {
_queueMicrotask(this._callGC);
}
var resultKey;
var _iterator2 = _createForOfIteratorHelper(this._result.keys()),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var k = _step2.value;
if (this._argComparer(k, args)) {
resultKey = k;
break;
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
if (resultKey) {
var result = this._result.get(resultKey);
if (!this._ttl || Date.now() - result[0] <= this._ttl) {
if (this._cache && !this._ttl && this._maxCacheSize) {
// use lru strategy
this._result.delete(resultKey);
this._result.set(resultKey, result);
}
if (result[2]) throw result[2];
return result[1];
}
this._result.delete(resultKey);
}
var now = Date.now();
try {
var _this$_fn;
var fnResult = (_this$_fn = this._fn).call.apply(_this$_fn, [void 0].concat(args));
if (fnResult instanceof Promise) {
this._result.set(args, [now, fnResult]); // for merge promise call
fnResult.then(function () {
if (!_this2._cache) {
_this2._result.delete(args);
}
}, function () {
if (!_this2._cache || !_this2._cacheOnError) {
_this2._result.delete(args);
}
});
return fnResult;
} else {
if (this._cache) {
this._result.set(args, [now, fnResult]);
}
return fnResult;
}
} catch (e) {
if (this._cache && this._cacheOnError) {
this._result.set(args, [now, void 0, e]);
}
throw e;
}
}
}, {
key: "dispose",
value:
/**
* Destroys the instance, clears all caches and event listeners
*/
function dispose() {
var _this3 = this;
this._disposed = true;
this._result.clear();
revalidateEE.off(revalidateAllStr, this.revalidate);
this._tags.forEach(function (tag) {
return revalidateEE.off(tag, _this3.revalidate);
});
}
}]);
return FnMergeCache;
}();
export default FnMergeCache;
/**
* Creates a function with caching capability
*
* @typeParam A - Tuple type of function parameters
* @typeParam R - Function return type
* @param fn - The original function to be cached
* @param opts - FnMergeCache configuration options
* @param opts.cache - Whether to enable caching
* @param opts.cacheOnError - Whether to cache results when errors occur
* @param opts.argComparer - Parameter comparison function, returns true if parameters are equal
* @param opts.ttl - Cache lifetime in milliseconds, 0 means never expires
* @param opts.maxCacheSize - Cache pool size limit, 0 means no limit
* @param opts.tags - Tags for cache revalidation
* @returns A new function with caching capability
*/
export function createMergedCachedFn(fn, opts) {
var cache = new FnMergeCache(fn, opts);
return cache.call.bind(cache);
}
/**
* Invalidates cache for specified tags
*
* @param tag - Tag or array of tags to invalidate, defaults to invalidating all caches
*/
export function revalidateTag() {
var tag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : revalidateAllStr;
if (Array.isArray(tag)) {
tag.forEach(function (t) {
return revalidateEE.emit(t);
});
} else {
revalidateEE.emit(tag);
}
}