UNPKG

cache-entanglement

Version:

Manage caches that are dependent on each other efficiently.

504 lines (496 loc) 15.1 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // node_modules/ms/index.js var require_ms = __commonJS({ "node_modules/ms/index.js"(exports, module) { var s = 1e3; var m = s * 60; var h = m * 60; var d = h * 24; var w = d * 7; var y = d * 365.25; module.exports = function(val, options) { options = options || {}; var type = typeof val; if (type === "string" && val.length > 0) { return parse(val); } else if (type === "number" && isFinite(val)) { return options.long ? fmtLong(val) : fmtShort(val); } throw new Error( "val is not a non-empty string or a valid number. val=" + JSON.stringify(val) ); }; function parse(str) { str = String(str); if (str.length > 100) { return; } var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( str ); if (!match) { return; } var n = parseFloat(match[1]); var type = (match[2] || "ms").toLowerCase(); switch (type) { case "years": case "year": case "yrs": case "yr": case "y": return n * y; case "weeks": case "week": case "w": return n * w; case "days": case "day": case "d": return n * d; case "hours": case "hour": case "hrs": case "hr": case "h": return n * h; case "minutes": case "minute": case "mins": case "min": case "m": return n * m; case "seconds": case "second": case "secs": case "sec": case "s": return n * s; case "milliseconds": case "millisecond": case "msecs": case "msec": case "ms": return n; default: return void 0; } } function fmtShort(ms2) { var msAbs = Math.abs(ms2); if (msAbs >= d) { return Math.round(ms2 / d) + "d"; } if (msAbs >= h) { return Math.round(ms2 / h) + "h"; } if (msAbs >= m) { return Math.round(ms2 / m) + "m"; } if (msAbs >= s) { return Math.round(ms2 / s) + "s"; } return ms2 + "ms"; } function fmtLong(ms2) { var msAbs = Math.abs(ms2); if (msAbs >= d) { return plural(ms2, msAbs, d, "day"); } if (msAbs >= h) { return plural(ms2, msAbs, h, "hour"); } if (msAbs >= m) { return plural(ms2, msAbs, m, "minute"); } if (msAbs >= s) { return plural(ms2, msAbs, s, "second"); } return ms2 + " ms"; } function plural(ms2, msAbs, n, name) { var isPlural = msAbs >= n * 1.5; return Math.round(ms2 / n) + " " + name + (isPlural ? "s" : ""); } } }); // src/CacheEntanglement.ts var import_ms = __toESM(require_ms()); // src/utils/InvertedWeakMap.ts var InvertedWeakMap = class { _map; _keepAlive; _timeouts; _registry; _lifespan; constructor(option) { const { lifespan } = option; this._lifespan = lifespan; this._map = /* @__PURE__ */ new Map(); this._keepAlive = /* @__PURE__ */ new Map(); this._timeouts = /* @__PURE__ */ new Map(); this._registry = new FinalizationRegistry((key) => { this._stopExpire(key, true); this._map.delete(key); }); } clear() { this._keepAlive.clear(); this._map.clear(); } delete(key) { const ref = this._map.get(key); if (ref) { const raw = ref.deref(); if (raw !== void 0) { this._registry.unregister(raw); } } this._stopExpire(key, true); this._keepAlive.delete(key); return this._map.delete(key); } get(key) { return this._map.get(key)?.deref(); } has(key) { return this._map.has(key) && this.get(key) !== void 0; } set(key, value) { this._map.set(key, new WeakRef(value)); this._registry.register(value, key); if (this._lifespan > 0) { this._stopExpire(key, true); this._startExpire(key, value); } return this; } extendExpire(key) { if (!(this._lifespan > 0)) { return; } if (!this._keepAlive.has(key)) { return; } this._stopExpire(key, false); this._startExpire(key, this._keepAlive.get(key)); } _startExpire(key, value) { this._keepAlive.set(key, value); this._timeouts.set(key, setTimeout(() => { this._keepAlive.delete(key); }, this._lifespan)); } _stopExpire(key, removeKeepAlive) { if (!this._timeouts.has(key)) { return; } const timeout = this._timeouts.get(key); this._timeouts.delete(key); clearTimeout(timeout); if (removeKeepAlive) { this._keepAlive.delete(key); } } get size() { return this._map.size; } keys() { return this._map.keys(); } }; // src/CacheEntanglement.ts var CacheEntanglement = class { creation; beforeUpdateHook; lifespan; dependencies; caches; parameters; assignments; dependencyProperties; updateRequirements; constructor(creation, option) { option = option ?? {}; const { dependencies, lifespan, beforeUpdateHook } = option; this.creation = creation; this.beforeUpdateHook = beforeUpdateHook ?? (() => { }); this.lifespan = this._normalizeMs(lifespan ?? 0); this.assignments = []; this.caches = new InvertedWeakMap({ lifespan: this.lifespan }); this.parameters = /* @__PURE__ */ new Map(); this.dependencies = dependencies ?? {}; this.dependencyProperties = Object.keys(this.dependencies); this.updateRequirements = /* @__PURE__ */ new Set(); for (const name in this.dependencies) { const dependency = this.dependencies[name]; if (!dependency.assignments.includes(this)) { dependency.assignments.push(this); } } } _normalizeMs(time) { if (typeof time === "string") { return (0, import_ms.default)(time); } return time; } bubbleUpdateSignal(key) { this.updateRequirements.add(key); for (let i = 0, len = this.assignments.length; i < len; i++) { const t = this.assignments[i]; const instance = t; for (const cacheKey of instance.caches.keys()) { if (cacheKey === key || cacheKey.startsWith(`${key}/`)) { instance.bubbleUpdateSignal(cacheKey); } } } } dependencyKey(key) { const i = key.lastIndexOf("/"); if (i === -1) { return key; } return key.substring(0, i); } /** * Returns all keys stored in the instance. */ keys() { return this.parameters.keys(); } /** * Deletes all cache values stored in the instance. */ clear() { for (const key of this.keys()) { this.delete(key); } } /** * Checks if there is a cache value stored in the key within the instance. * @param key The key to search. */ exists(key) { return this.parameters.has(key); } /** * Checks if there is a cache value stored in the key within the instance. * This method is an alias for `exists`. * @param key The key to search. */ has(key) { return this.exists(key); } /** * Deletes the cache value stored in the key within the instance. * @param key The key to delete. */ delete(key) { this.caches.delete(key); this.parameters.delete(key); this.updateRequirements.delete(key); for (let i = 0, len = this.assignments.length; i < len; i++) { const t = this.assignments[i]; const instance = t; for (const cacheKey of instance.keys()) { if (cacheKey === key || cacheKey.startsWith(`${key}/`)) { instance.delete(cacheKey); } } } } }; // src/CacheData.ts var CacheData = class _CacheData { static StructuredClone = globalThis.structuredClone.bind(globalThis); _value; constructor(value) { this._value = value; } /** * This is cached data. * It was generated at the time of caching, so there is a risk of modification if it's an object due to shallow copying. * Therefore, if it's not a primitive type, please avoid using this value directly and use the `clone` method to use a copied version of the data. */ get raw() { return this._value; } /** * The method returns a copied value of the cached data. * You can pass a function as a parameter to copy the value. This parameter function should return the copied value. * * If no parameter is passed, it defaults to using `structuredClone` function to copy the value. * If you prefer shallow copying instead of deep copying, * you can use the default options `array-shallow-copy`, `object-shallow-copy` and `deep-copy`, * which are replaced with functions to shallow copy arrays and objects, respectively. This is a syntactic sugar. * @param strategy The function that returns the copied value. * If you want to perform a shallow copy, simply pass the strings `array-shallow-copy` or `object-shallow-copy` for easy use. * The `array-shallow-copy` strategy performs a shallow copy of an array. * The `object-shallow-copy` strategy performs a shallow copy of an object. * The `deep-copy` strategy performs a deep copy of the value using `structuredClone`. * The default is `deep-copy`. */ clone(strategy = "deep-copy") { if (strategy && typeof strategy !== "string") { return strategy(this.raw); } switch (strategy) { case "array-shallow-copy": return [].concat(this.raw); case "object-shallow-copy": return Object.assign({}, this.raw); case "deep-copy": default: return _CacheData.StructuredClone(this.raw); } } }; // src/CacheEntanglementSync.ts var CacheEntanglementSync = class extends CacheEntanglement { constructor(creation, option) { super(creation, option); } recache(key) { if (!this.parameters.has(key)) { return; } if (!this.caches.has(key) || this.updateRequirements.has(key)) { this.resolve(key, ...this.parameters.get(key)); } return this.caches.get(key); } resolve(key, ...parameter) { const resolved = {}; const dependencyKey = this.dependencyKey(key); this.beforeUpdateHook(key, dependencyKey, ...parameter); for (let i = 0, len = this.dependencyProperties.length; i < len; i++) { const name = this.dependencyProperties[i]; const dependency = this.dependencies[name]; if (!dependency.exists(key) && !dependency.exists(dependencyKey)) { throw new Error(`The key '${key}' or '${dependencyKey}' has not been assigned yet in dependency '${name.toString()}'.`, { cause: { from: this } }); } const dependencyValue = dependency.recache(key) ?? dependency.recache(dependencyKey); resolved[name] = dependencyValue; } const value = new CacheData(this.creation(key, resolved, ...parameter)); this.updateRequirements.delete(key); this.parameters.set(key, parameter); this.caches.set(key, value); return value; } get(key) { if (!this.parameters.has(key)) { throw new Error(`Cache value not found: ${key}`); } return this.cache(key, ...this.parameters.get(key)); } cache(key, ...parameter) { if (!this.caches.has(key) || this.updateRequirements.has(key)) { this.resolve(key, ...parameter); } else { this.caches.extendExpire(key); } return this.caches.get(key); } update(key, ...parameter) { this.bubbleUpdateSignal(key); this.resolve(key, ...parameter); return this.caches.get(key); } }; // src/CacheEntanglementAsync.ts var CacheEntanglementAsync = class extends CacheEntanglement { constructor(creation, option) { super(creation, option); } async recache(key) { if (!this.parameters.has(key)) { return; } if (!this.caches.has(key) || this.updateRequirements.has(key)) { await this.resolve(key, ...this.parameters.get(key)); } return this.caches.get(key); } async resolve(key, ...parameter) { const resolved = {}; const dependencyKey = this.dependencyKey(key); await this.beforeUpdateHook(key, dependencyKey, ...parameter); for (let i = 0, len = this.dependencyProperties.length; i < len; i++) { const name = this.dependencyProperties[i]; const dependency = this.dependencies[name]; if (!dependency.exists(key) && !dependency.exists(dependencyKey)) { throw new Error(`The key '${key}' or '${dependencyKey}' has not been assigned yet in dependency '${name.toString()}'.`, { cause: { from: this } }); } const dependencyValue = await dependency.recache(key) ?? await dependency.recache(dependencyKey); resolved[name] = dependencyValue; } const value = new CacheData(await this.creation(key, resolved, ...parameter)); this.updateRequirements.delete(key); this.parameters.set(key, parameter); this.caches.set(key, value); return value; } async get(key) { if (!this.parameters.has(key)) { throw new Error(`Cache value not found: ${key}`); } return this.cache(key, ...this.parameters.get(key)); } async cache(key, ...parameter) { if (!this.caches.has(key) || this.updateRequirements.has(key)) { await this.update(key, ...parameter); } else { this.caches.extendExpire(key); } return this.caches.get(key); } async update(key, ...parameter) { this.bubbleUpdateSignal(key); await this.resolve(key, ...parameter); return this.caches.get(key); } }; export { CacheEntanglementAsync, CacheEntanglementSync };