@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
JavaScript
;
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;