/**
* Simple in-memory cache utility with bounded size
* Stores data with TTL (time to live) and LRU eviction
*/
import { CACHE_CONFIG } from './constants/index.js';
interface CacheEntry {
data: unknown;
expiresAt: number;
lastAccessed: number;
}
const cache = new Map<string, CacheEntry>();
let maxSize: number = CACHE_CONFIG.MAX_ENTRIES;
/**
* Configure maximum cache size.
* Intended for testing and optional runtime configuration (e.g. overriding default from CACHE_CONFIG).
* @param size - Must be a positive integer (>= 1)
* @throws Error if size is not a positive number
*/
export const setCacheMaxSize = (size: number): void => {
if (typeof size !== 'number' || !Number.isFinite(size) || size < 1) {
throw new Error(
`setCacheMaxSize: size must be a positive number, got ${size}`
);
}
maxSize = Math.floor(size);
};
/**
* Evict expired entries; optionally evict by LRU if over capacity (only when adding a new key).
* @param makeRoomForNewEntry - When true, run capacity eviction to make room for one new entry
*/
const evictIfNeeded = (makeRoomForNewEntry: boolean): void => {
const now = Date.now();
// First pass: remove expired entries
for (const [key, entry] of cache) {
if (now > entry.expiresAt) {
cache.delete(key);
}
}
// Second pass: only when adding a new entry — evict least-recently-accessed if at capacity
if (makeRoomForNewEntry && cache.size >= maxSize && maxSize >= 1) {
const entries = [...cache.entries()]
.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
const toRemove = Math.min(
cache.size - maxSize + 1, // +1 to make room for the new entry
entries.length
);
for (let i = 0; i < toRemove; i++) {
cache.delete(entries[i][0]);
}
}
};
/**
* Get value from cache
* @param key - Cache key
* @returns Cached value or null if not found or expired
*/
export const getCache = <T>(key: string): T | null => {
const entry = cache.get(key);
if (!entry) {
return null;
}
// Check if expired
if (Date.now() > entry.expiresAt) {
cache.delete(key);
return null;
}
entry.lastAccessed = Date.now();
return entry.data as T;
};
/**
* Set value in cache
* @param key - Cache key
* @param value - Value to cache
* @param ttlMs - Time to live in milliseconds
*/
export const setCache = <T>(key: string, value: T, ttlMs: number): void => {
const isNewKey = !cache.has(key);
evictIfNeeded(isNewKey);
const now = Date.now();
cache.set(key, {
data: value,
expiresAt: now + ttlMs,
lastAccessed: now,
});
};
/**
* Delete value from cache
* @param key - Cache key
*/
export const deleteCache = (key: string): void => {
cache.delete(key);
};
/**
* Clear all cache
*/
export const clearCache = (): void => {
cache.clear();
};
/**
* Get current cache size (for testing)
*/
export const getCacheSize = (): number => {
return cache.size;
};