runtime-memory-cache
Version:
A lightweight, high-performance in-memory cache for Node.js with TTL support, configurable eviction policies (FIFO/LRU), statistics tracking, and zero dependencies.
196 lines (195 loc) • 6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationUtils = exports.CacheUtils = void 0;
/**
* Utility class for cache-related operations
*/
class CacheUtils {
/**
* Check if a cache entry is expired
*/
static isExpired(entry) {
return entry.expiresAt !== undefined && entry.expiresAt < Date.now();
}
/**
* Calculate expiration timestamp
*/
static calculateExpiresAt(ttl, defaultTtl) {
if (ttl !== undefined) {
return Date.now() + ttl;
}
if (defaultTtl !== undefined) {
return Date.now() + defaultTtl;
}
return undefined;
}
/**
* Create a new cache entry
*/
static createEntry(value, expiresAt) {
const now = Date.now();
return {
value,
expiresAt,
createdAt: now,
lastAccessedAt: now
};
}
/**
* Update access time for LRU tracking
*/
static updateAccessTime(entry) {
entry.lastAccessedAt = Date.now();
}
/**
* Get the key to evict based on eviction policy
*/
static getKeyToEvict(store, evictionPolicy) {
if (store.size === 0)
return undefined;
if (evictionPolicy === 'FIFO') {
// Return the first key (oldest inserted)
return store.keys().next().value;
}
else {
// LRU: Find the least recently used entry
let lruKey = undefined;
let oldestAccessTime = Number.MAX_SAFE_INTEGER;
for (const [key, entry] of store.entries()) {
if (entry.lastAccessedAt < oldestAccessTime) {
oldestAccessTime = entry.lastAccessedAt;
lruKey = key;
}
}
return lruKey;
}
}
/**
* Get the first key from a Map (for FIFO eviction)
* @deprecated Use getKeyToEvict instead
*/
static getFirstKey(map) {
const iterator = map.keys().next();
return iterator.done ? undefined : iterator.value;
}
/**
* Remove expired entries from the cache
*/
static cleanupExpired(store) {
let removedCount = 0;
for (const [key, entry] of store.entries()) {
if (CacheUtils.isExpired(entry)) {
store.delete(key);
removedCount++;
}
}
return removedCount;
}
/**
* Estimate memory usage of a cache entry
*/
static estimateEntrySize(key, entry) {
const keySize = new Blob([key]).size;
const valueSize = CacheUtils.estimateValueSize(entry.value);
const metadataSize = 24; // Approximate size of timestamps and metadata
return keySize + valueSize + metadataSize;
}
/**
* Estimate memory size of a value
*/
static estimateValueSize(value) {
if (value === null || value === undefined)
return 0;
const type = typeof value;
switch (type) {
case 'boolean':
return 4;
case 'number':
return 8;
case 'string':
return new Blob([value]).size;
case 'object':
if (Array.isArray(value)) {
return value.reduce((acc, item) => acc + CacheUtils.estimateValueSize(item), 0);
}
return new Blob([JSON.stringify(value)]).size;
default:
return new Blob([String(value)]).size;
}
}
/**
* Calculate total memory usage of cache
*/
static calculateMemoryUsage(store) {
let totalBytes = 0;
let entryCount = 0;
for (const [key, entry] of store.entries()) {
totalBytes += CacheUtils.estimateEntrySize(String(key), entry);
entryCount++;
}
return {
estimatedBytes: totalBytes,
averageBytesPerEntry: entryCount > 0 ? Math.round(totalBytes / entryCount) : 0
};
}
}
exports.CacheUtils = CacheUtils;
/**
* Validation utilities for cache operations
*/
class ValidationUtils {
/**
* Validate cache key
*/
static validateKey(key) {
if (typeof key !== 'string') {
throw new TypeError('Cache key must be a string');
}
if (key.length === 0) {
throw new Error('Cache key cannot be empty');
}
if (key.length > 250) {
throw new Error('Cache key too long (maximum 250 characters)');
}
}
/**
* Validate TTL value
*/
static validateTTL(ttl) {
if (typeof ttl !== 'number' || Number.isNaN(ttl)) {
throw new TypeError('TTL must be a number');
}
if (!isFinite(ttl)) {
throw new Error('TTL must be a finite number');
}
if (ttl < 0) {
throw new Error('TTL cannot be negative');
}
}
/**
* Validate cache options
*/
static validateCacheOptions(options) {
if (options.ttl !== undefined) {
ValidationUtils.validateTTL(options.ttl);
}
if (options.maxSize !== undefined) {
if (typeof options.maxSize !== 'number' || Number.isNaN(options.maxSize)) {
throw new TypeError('maxSize must be a number');
}
if (options.maxSize < 1) {
throw new Error('maxSize must be at least 1');
}
if (!Number.isInteger(options.maxSize)) {
throw new Error('maxSize must be an integer');
}
}
if (options.evictionPolicy !== undefined) {
const validPolicies = ['FIFO', 'LRU'];
if (!validPolicies.includes(options.evictionPolicy)) {
throw new Error(`evictionPolicy must be one of: ${validPolicies.join(', ')}`);
}
}
}
}
exports.ValidationUtils = ValidationUtils;