UNPKG

@nasriya/cachify

Version:

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

248 lines (247 loc) 10.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const EvictConfig_1 = __importDefault(require("../configs/strategies/evict/EvictConfig")); const constants = __importStar(require("../consts/consts")); const kvs_record_1 = __importDefault(require("./kvs/kvs.record")); const files_record_1 = __importDefault(require("./files/files.record")); const FilesEventsManager_1 = require("../events/managers/files/FilesEventsManager"); const KVsEventsManager_1 = require("../events/managers/kvs/KVsEventsManager"); 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 kvs_record_1.default || record instanceof files_record_1.default)) { 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_1.default)) { 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_1.FilesEventsManager || eventsManager instanceof KVsEventsManager_1.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(); exports.default = helpers;