UNPKG

@avanio/expire-cache

Version:

Typescript/Javascript cache with expiration

535 lines (532 loc) 18.7 kB
var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __objRest = (source, exclude) => { var target = {}; for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop]; if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) { if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop]; } return target; }; // src/ExpireCache.mts import { EventEmitter } from "events"; import { LogLevel, MapLogger } from "@avanio/logger-like"; var defaultLogMap = { cleanExpired: LogLevel.None, clear: LogLevel.None, constructor: LogLevel.None, delete: LogLevel.None, expires: LogLevel.None, get: LogLevel.None, has: LogLevel.None, onExpire: LogLevel.None, set: LogLevel.None, size: LogLevel.None }; var ExpireCache = class extends EventEmitter { /** * Creates a new instance of the ExpireCache class * @param {ILoggerLike} logger - The logger to use (optional) * @param {Partial<ExpireCacheLogMapType>} logMapping - The log mapping to use (optional). Default is all logging disabled * @param {number} defaultExpireMs - The default expiration time in milliseconds (optional) */ constructor(logger, logMapping, defaultExpireMs) { super(); this.cache = /* @__PURE__ */ new Map(); this.cacheTtl = /* @__PURE__ */ new Map(); this.logger = new MapLogger(logger, Object.assign({}, defaultLogMap, logMapping)); this.logger.logKey("constructor", `ExpireCache created, defaultExpireMs: ${String(defaultExpireMs)}`); this.defaultExpireMs = defaultExpireMs; } set(key, data, expires) { var _a; const expireTs = (_a = this.getExpireDate(expires)) == null ? void 0 : _a.getTime(); this.logger.logKey("set", `ExpireCache set key: ${String(key)}, expireTs: ${String(expireTs)}`); this.emit("set", key, data, this.getExpireDate(expires)); this.cache.set(key, data); this.cacheTtl.set(key, expireTs); } get(key) { this.logger.logKey("get", `ExpireCache get key: ${String(key)}`); this.emit("get", key); this.cleanExpired(); return this.cache.get(key); } has(key) { this.logger.logKey("has", `ExpireCache has key: ${String(key)}`); this.cleanExpired(); return this.cache.has(key); } expires(key) { this.logger.logKey("expires", `ExpireCache get expire for key: ${String(key)}`); const expires = this.cacheTtl.get(key); this.cleanExpired(); return expires ? new Date(expires) : void 0; } delete(key) { this.logger.logKey("delete", `ExpireCache delete key: ${String(key)}`); const entry = this.cache.get(key); if (entry) { this.notifyExpires(/* @__PURE__ */ new Map([[key, entry]])); this.emit("delete", key); } this.cacheTtl.delete(key); return this.cache.delete(key); } clear() { this.logger.logKey("clear", `ExpireCache clear`); const copy = new Map(this.cache); this.notifyExpires(copy); this.emit("clear", copy); this.cache.clear(); this.cacheTtl.clear(); } size() { this.logger.logKey("size", `ExpireCache size: ${this.cache.size.toString()}`); return this.cache.size; } entries() { this.cleanExpired(); return new Map(this.cache).entries(); } keys() { this.cleanExpired(); return new Map(this.cache).keys(); } values() { this.cleanExpired(); return new Map(this.cache).values(); } /** * Sets the default expiration time in milliseconds * @param {number} expireMs - The default expiration time in milliseconds */ setExpireMs(expireMs) { this.defaultExpireMs = expireMs; } /** * Cleans expired cache entries */ cleanExpired() { const now = (/* @__PURE__ */ new Date()).getTime(); const deleteEntries = /* @__PURE__ */ new Map(); for (const [key, expire] of this.cacheTtl.entries()) { if (expire !== void 0 && expire < now) { const value = this.cache.get(key); if (value) { deleteEntries.set(key, value); this.cache.delete(key); } this.cacheTtl.delete(key); } } if (deleteEntries.size > 0) { this.notifyExpires(deleteEntries); this.logger.logKey("cleanExpired", `ExpireCache expired count: ${deleteEntries.size.toString()}`); } } notifyExpires(entries) { for (const [key, value] of entries.entries()) { this.emit("expires", key, value); } } getExpireDate(expires) { const defaultExpireDate = this.defaultExpireMs ? new Date(Date.now() + this.defaultExpireMs) : void 0; return expires != null ? expires : defaultExpireDate; } }; // src/ExpireTimeoutCache.mts import { EventEmitter as EventEmitter2 } from "events"; import { LogLevel as LogLevel2, MapLogger as MapLogger2 } from "@avanio/logger-like"; var defaultLogMap2 = { cleanExpired: LogLevel2.None, clear: LogLevel2.None, constructor: LogLevel2.None, delete: LogLevel2.None, expires: LogLevel2.None, get: LogLevel2.None, has: LogLevel2.None, onExpire: LogLevel2.None, set: LogLevel2.None, size: LogLevel2.None }; var ExpireTimeoutCache = class extends EventEmitter2 { /** * Creates a new instance of the ExpireTimeoutCache class * @param {ILoggerLike} logger - The logger to use (optional) * @param {Partial<ExpireTimeoutCacheLogMapType>} logMapping - The log mapping to use (optional). Default is all logging disabled * @param {number} defaultExpireMs - The default expiration time in milliseconds (optional) */ constructor(logger, logMapping, defaultExpireMs) { super(); this.cache = /* @__PURE__ */ new Map(); this.cacheTimeout = /* @__PURE__ */ new Map(); this.logger = new MapLogger2(logger, Object.assign({}, defaultLogMap2, logMapping)); this.logger.logKey("constructor", `ExpireTimeoutCache created, defaultExpireMs: ${String(defaultExpireMs)}`); this.defaultExpireMs = defaultExpireMs; } set(key, data, expires) { this.clearTimeout(key); const _a = this.handleTimeoutSetup(key, this.getExpireDate(expires)), { expiresInMs } = _a, options = __objRest(_a, ["expiresInMs"]); const expireString = expiresInMs ? `${expiresInMs.toString()} ms` : "undefined"; this.logger.logKey("set", `ExpireTimeoutCache set key: ${String(key)}, expireTs: ${expireString}`); this.cache.set(key, data); this.cacheTimeout.set(key, options); } get(key) { this.logger.logKey("get", `ExpireTimeoutCache get key: ${String(key)}`); this.emit("get", key); return this.cache.get(key); } has(key) { this.logger.logKey("has", `ExpireTimeoutCache has key: ${String(key)}`); return this.cache.has(key); } expires(key) { var _a; this.logger.logKey("expires", `ExpireTimeoutCache get expire for key: ${String(key)}`); return (_a = this.cacheTimeout.get(key)) == null ? void 0 : _a.expires; } delete(key) { this.logger.logKey("delete", `ExpireTimeoutCache delete key: ${String(key)}`); this.clearTimeout(key); const entry = this.cache.get(key); if (entry) { this.emit("delete", key); this.notifyExpires(/* @__PURE__ */ new Map([[key, entry]])); } return this.cache.delete(key); } clear() { this.logger.logKey("clear", `ExpireTimeoutCache clear`); this.cacheTimeout.forEach((_value, key) => this.clearTimeout(key)); const copy = new Map(this.cache); this.notifyExpires(copy); this.emit("clear", copy); this.cache.clear(); this.cacheTimeout.clear(); } size() { this.logger.logKey("size", `ExpireTimeoutCache size: ${this.cache.size.toString()}`); return this.cache.size; } entries() { return new Map(this.cache).entries(); } keys() { return new Map(this.cache).keys(); } values() { return new Map(this.cache).values(); } /** * Set the default expiration time in milliseconds * @param {number} expireMs - The default expiration time in milliseconds */ setExpireMs(expireMs) { this.defaultExpireMs = expireMs; } clearTimeout(key) { const entry = this.cacheTimeout.get(key); if (entry == null ? void 0 : entry.timeout) { clearTimeout(entry.timeout); entry.timeout = void 0; } } notifyExpires(entries) { for (const [key, value] of entries.entries()) { this.emit("expires", key, value); } } getExpireDate(expires) { const defaultExpireDate = this.defaultExpireMs ? new Date(Date.now() + this.defaultExpireMs) : void 0; return expires != null ? expires : defaultExpireDate; } handleExpiredCallback(key) { this.logger.logKey("onExpire", `ExpireTimeoutCache onExpire key: ${String(key)}`); this.delete(key); } handleTimeoutSetup(key, expiresDate) { const expiresInMs = expiresDate && expiresDate.getTime() - Date.now(); const timeout = expiresInMs !== void 0 ? setTimeout(() => this.handleExpiredCallback(key), expiresInMs) : void 0; return { expiresInMs, timeout, expires: expiresDate }; } }; // src/TieredCache.mts import { EventEmitter as EventEmitter3 } from "events"; import { LogLevel as LogLevel3, MapLogger as MapLogger3 } from "@avanio/logger-like"; var defaultLogMap3 = { clear: LogLevel3.None, clearTimeoutKey: LogLevel3.None, constructor: LogLevel3.None, delete: LogLevel3.None, get: LogLevel3.None, has: LogLevel3.None, runTimeout: LogLevel3.None, set: LogLevel3.None, setTimeout: LogLevel3.None, size: LogLevel3.None }; var TieredCache = class extends EventEmitter3 { constructor(logger, logMapping) { super(); this.cache = /* @__PURE__ */ new Map(); this.cacheTimeout = /* @__PURE__ */ new Map(); this.logger = new MapLogger3(logger, Object.assign({}, defaultLogMap3, logMapping)); this.logCacheName(); this.handleCacheEntry = this.handleCacheEntry.bind(this); this.statusData = { size: 0, tiers: __spreadValues({}, this.getInitialStatusData()) }; } /** * Get cache entry from cache * @param {Key} key - cache key * @param {T['tier']} tier - cache tier * @param {TimeoutEnum} [timeout] - optional update to new timeout for cache entry (if not provided, default timeout for tier will be used) * @returns - promise that resolves when cache entry is set */ async get(key, tier, timeout) { this.logger.logKey("get", `MultiTierCache ${this.cacheName} get: '${String(key)}' tier: ${tier}`); const entry = this.cache.get(key); const value = await this.handleCacheEntry(key, tier, entry); if (value) { this.setTimeout(key, timeout != null ? timeout : this.handleTierDefaultTimeout(tier)); } return value; } /** * Set cache entry * @param {Key} key - cache key * @param {T['tier']} tier - cache tier * @param {T['data']} data - cache data * @param {TimeoutEnum} [timeout] - optional timeout for cache entry. Else timeout will be checked from handleTimeoutValue or default timeout for tier. */ async set(key, tier, data, timeout) { this.logger.logKey("set", `MultiTierCache ${this.cacheName} set: '${String(key)}' tier: ${tier}`); await this.handleSetValue(key, tier, data, timeout); this.emit("set", [key]); this.emit("update", this.buildStatus(true)); } /** * Set multiple cache entries * @param {T['tier']} tier - cache tier * @param {Iterable<[Key, T['data']]>} entries - iterable of key, data pairs * @param {TimeoutEnum} [timeout] - optional timeout for cache entry. Else timeout will be checked from handleTimeoutValue or default timeout for tier. */ async setEntries(tier, entries, timeout) { const entriesArray = Array.from(entries); this.logger.logKey("set", `MultiTierCache ${this.cacheName} setEntries (count: ${entriesArray.length.toString()}) tier: ${tier}`); for (const [key, data] of entriesArray) { await this.handleSetValue(key, tier, data, timeout); } this.emit( "set", entriesArray.map(([key]) => key) ); this.emit("update", this.buildStatus(true)); } /** * Get tier type for current key * @param {Key} key - cache key * @returns {Tiers[number]['tier'] | undefined} The tier type or undefined if not found */ getTier(key) { const entry = this.cache.get(key); return entry == null ? void 0 : entry.tier; } /** * Iterate this.cache values and use handleCacheEntry to get data * @param {Tiers[number]['tier']} tier - cache tier to get values for * @returns {AsyncIterable<Tiers[number]['data']>} Async iterable of cache values */ tierValues(tier) { const iterator = this.cache.entries(); const currentTierResolve = this.handleCacheEntry.bind(this); return { [Symbol.asyncIterator]: () => { return { async next() { const { value, done } = iterator.next(); if (done) { return { value: void 0, done }; } return { value: await currentTierResolve(value[0], tier, value[1]), done }; } }; } }; } /** * Iterate this.cache entries and use handleCacheEntry to get data * @param {Tiers[number]['tier']} tier - cache tier to get entries for * @returns {AsyncIterable<[Key, Tiers[number]['data']]>} Async iterable of key-value pairs */ tierEntries(tier) { const iterator = this.cache.entries(); const currentTierResolve = this.handleCacheEntry.bind(this); return { [Symbol.asyncIterator]: () => { return { async next() { const { value, done } = iterator.next(); if (done) { return { value, done: true }; } return { value: [value[0], await currentTierResolve(value[0], tier, value[1])], done: false }; } }; } }; } keys() { return this.cache.keys(); } has(key) { this.logger.logKey("has", `MultiTierCache ${this.cacheName} has: '${String(key)}'`); return this.cache.has(key); } size() { this.logger.logKey("size", `MultiTierCache ${this.cacheName} size: ${this.cache.size.toString()}`); return this.cache.size; } clear() { const keys = new Set(this.cache.keys()); this.clearAllTimeouts(); this.cache.clear(); this.logger.logKey("clear", `MultiTierCache ${this.cacheName} clear`); this.emit("delete", keys); this.emit("clear"); this.emit("update", this.buildStatus(true)); } delete(key) { const isDeleted = this.handleDeleteValue(key); if (isDeleted) { this.logger.logKey("delete", `MultiTierCache ${this.cacheName} delete: '${String(key)}'`); this.emit("delete", [key]); this.emit("update", this.buildStatus(true)); } return isDeleted; } deleteKeys(keys) { const deleteKeys = []; for (const key of keys) { this.clearTimeoutKey(key); if (this.cache.delete(key)) { deleteKeys.push(key); } } this.logger.logKey("delete", `MultiTierCache ${this.cacheName} deleteKeys (count: ${deleteKeys.length.toString()})`); this.emit("delete", deleteKeys); this.emit("update", this.buildStatus(true)); return deleteKeys.length; } status() { return this.buildStatus(false); } buildStatus(rebuild) { if (!rebuild) { return this.statusData; } this.statusData = Object.freeze({ size: this.cache.size, tiers: Array.from(this.cache.values()).reduce( (acc, { tier: type }) => { acc[type]++; return acc; }, __spreadValues({}, this.getInitialStatusData()) ) }); return this.statusData; } /** * Internal helper to set cache entry and set timeout * @param {Key} key - cache entry key * @param {T['tier']} tier - cache entry tier * @param {T['data']} data - cache entry data * @param {TimeoutEnum} [timeout] - timeout value, optional */ async handleSetValue(key, tier, data, timeout) { this.cache.set(key, { tier, data }); this.setTimeout(key, timeout != null ? timeout : await this.handleTimeoutValue(key, tier, data)); } /** * Internal helper to delete cache entry and cancel its timeout * @param {Key} key - cache entry key * @returns true if entry was deleted, false if not found */ handleDeleteValue(key) { this.clearTimeoutKey(key); return this.cache.delete(key); } logCacheName() { this.logger.logKey("constructor", `MultiTierCache ${this.cacheName} created`); } setTimeout(key, timeout) { const oldTimeout = this.cacheTimeout.get(key); if (oldTimeout) { clearTimeout(oldTimeout); } if (timeout !== void 0) { this.cacheTimeout.set( key, setTimeout(() => void this.runTimeout(key), timeout) ); this.logger.logKey("setTimeout", `MultiTierCache ${this.cacheName} setTimeout: '${String(key)}' = timeouts: ${timeout.toString()}`); } } clearTimeoutKey(key) { const oldTimeout = this.cacheTimeout.get(key); if (oldTimeout) { this.logger.logKey("clearTimeoutKey", `MultiTierCache ${this.cacheName} clearTimeoutKey: '${String(key)}'`); clearTimeout(oldTimeout); } this.cacheTimeout.delete(key); } clearAllTimeouts() { for (const timeout of this.cacheTimeout.values()) { if (timeout) { clearTimeout(timeout); } } this.cacheTimeout.clear(); } async runTimeout(key) { try { const timeoutValue = await this.handleTierTimeout(key); if (timeoutValue === void 0) { this.logger.logKey("runTimeout", `MultiTierCache ${this.cacheName} runTimeout: '${String(key)}' cleared`); this.clearTimeoutKey(key); } else { this.logger.logKey("runTimeout", `MultiTierCache ${this.cacheName} runTimeout: '${String(key)}' cleared, new timeout: ${timeoutValue.toString()}`); this.setTimeout(key, timeoutValue); } this.emit("update", this.buildStatus(true)); } catch (error) { this.logger.error(error); } } }; export { ExpireCache, ExpireTimeoutCache, TieredCache }; //# sourceMappingURL=index.mjs.map