UNPKG

@nasriya/cachify

Version:

A lightweight, extensible in-memory caching library for storing anything, with built-in TTL and customizable cache types.

210 lines (209 loc) 8.7 kB
import EvictConfig from "../configs/strategies/evict/EvictConfig.js"; import * as constants from "../consts/consts.js"; import KVCacheRecord from "./kvs/kvs.record.js"; import FileCacheRecord from "./files/files.record.js"; import { FilesEventsManager } from "../events/managers/files/FilesEventsManager.js"; import { KVsEventsManager } from "../events/managers/kvs/KVsEventsManager.js"; class CacheHelpers { estimateValueSize(value, seen = new WeakSet()) { if (value === null || value === undefined) { return 0; } const type = typeof value; if (type === 'boolean') return 4; if (type === 'number') return 8; if (type === 'string') return value.length * 2; if (type === 'symbol') return 8; if (type === 'function') return 0; if (Buffer.isBuffer(value)) return value.length; if (ArrayBuffer.isView(value)) return value.byteLength; if (typeof value === 'object') { if (seen.has(value)) return 0; seen.add(value); let bytes = constants.OBJECT_OVERHEAD; if (Array.isArray(value)) { for (const item of value) { bytes += this.estimateValueSize(item, seen); } } else if (value instanceof Map) { for (const [k, v] of value.entries()) { bytes += this.estimateValueSize(k, seen); bytes += this.estimateValueSize(v, seen); } } else if (value instanceof Set) { for (const v of value.values()) { bytes += this.estimateValueSize(v, seen); } } else { for (const [key, val] of Object.entries(value)) { bytes += key.length * 2; bytes += this.estimateValueSize(val, seen); } } return bytes; } return 0; } records = { getScopeMap: (scope, map) => { if (!map.has(scope)) { map.set(scope, new Map()); } return map.get(scope); }, toArray: (map) => { const arr = new Array(); for (const [scope, scopeMap] of map) { for (const [key, record] of scopeMap) { if (!(record instanceof KVCacheRecord || record instanceof FileCacheRecord)) { console.warn(`[WARN] Non-CacheRecord found at ${scope}:${key}`); console.dir(record); continue; } arr.push(record); } } return arr; }, sortBy: { oldest: (maps) => { const arr = this.records.toArray(maps); return arr.sort((a, b) => Number(a.stats.dates.created - b.stats.dates.created)); }, leastRecentlyUsed: (maps) => { const arr = this.records.toArray(maps); return arr.sort((a, b) => { const lastAccessA = a.stats.dates.lastAccess || a.stats.dates.created; const lastAccessB = b.stats.dates.lastAccess || b.stats.dates.created; return Number(lastAccessA - lastAccessB); }); }, leastFrequentlyUsed: (maps) => { const arr = this.records.toArray(maps); return arr.sort((a, b) => { const countA = a.stats.counts.touch + a.stats.counts.read; const countB = b.stats.counts.touch + b.stats.counts.read; return Number(countA - countB); }); } }, createIterator: function* (recordsMap) { for (const [_, scopeMap] of recordsMap) { for (const [_, record] of scopeMap) { yield { map: scopeMap, record }; } } }, estimateSize: (key, value) => { const keyLength = Buffer.byteLength(key); const valueLength = this.estimateValueSize(value); return keyLength + valueLength; } }; cacheManagement = { idle: { /** * Creates a function that cleans up idle cache records. * This generated function, when executed, iterates over all cache records and checks * their last access time against the configured maximum idle time. * If a record has been idle for longer than the allowed duration, it is removed from the cache. * This function does nothing if idle timeout is not enabled. * * @returns A function that performs the idle cleanup asynchronously. */ createCleanHandler: (records, policy, eventsManager) => { return async () => { if (!policy.enabled) { return; } for (const [_, scopeMap] of records) { for (const [_, record] of scopeMap) { const lastActivity = record.stats.dates.lastAccess || record.stats.dates.created; const diff = Date.now() - lastActivity; if (diff > policy.maxIdleTime) { await eventsManager.emit.evict(record, { reason: 'idle' }); } } } }; } }, eviction: { async evictIfEnabled(configs) { const { records, policy, getSize, eventsManager } = configs; // ======= /// Validating arguments if (!(records instanceof Map)) { throw new TypeError('records must be a Map'); } if (!(policy instanceof EvictConfig)) { throw new TypeError('policy must be an EvictConfig'); } if (typeof getSize !== 'function') { throw new TypeError('getSize must be a function'); } if (typeof getSize() !== 'number') { throw new TypeError('getSize must return a number'); } if (!(eventsManager instanceof FilesEventsManager || eventsManager instanceof KVsEventsManager)) { throw new TypeError('eventsManager must be a FilesEventsManager or KVsEventsManager'); } // ======= if (!policy.enabled || records.size === 0) { return; } const mode = policy.mode; const eviction = Object.seal({ items: this.getItems(records, policy), hasNext() { return this.items.length > 0 && getSize() > policy.maxRecords; }, async next() { if (!this.hasNext()) { return; } const item = this.items.shift(); await eventsManager.emit.evict(item, { reason: mode }); } }); while (eviction.hasNext()) { // console.log(`[Evict] Evicting ${eviction.items.length} items...`); await eviction.next(); } }, getItems: (maps, policy) => { const mode = policy.mode; switch (mode) { case 'fifo': { // First-In-First-Out: evict the oldest item added. return this.records.sortBy.oldest(maps); } case 'lru': { // Least Recently Used: evict the item with the oldest lastAccess timestamp. return this.records.sortBy.leastRecentlyUsed(maps); } case 'lfu': { // Least Frequently Used: evict the item with the lowest access count. return this.records.sortBy.leastFrequentlyUsed(maps); } default: { return []; } } } } }; } const helpers = new CacheHelpers(); export default helpers;