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