/**
* Simple LRU (Least Recently Used) Cache implementation
* Provides automatic eviction of least recently used items when cache is full
*/
export interface LRUCacheOptions {
maxSize: number;
maxAge?: number; // Optional TTL in milliseconds
onEvict?: (key: string, value: any) => void;
}
interface CacheEntry<T> {
value: T;
timestamp: number;
}
export class LRUCache<T> {
private cache: Map<string, CacheEntry<T>>;
private readonly maxSize: number;
private readonly maxAge?: number;
private readonly onEvict?: (key: string, value: T) => void;
constructor(options: LRUCacheOptions) {
this.cache = new Map();
this.maxSize = options.maxSize;
this.maxAge = options.maxAge;
this.onEvict = options.onEvict;
}
/**
* Get a value from the cache
* Moves the item to the end (most recently used)
*/
get(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) {
return undefined;
}
// Check if entry has expired
if (this.maxAge && Date.now() - entry.timestamp > this.maxAge) {
this.delete(key);
return undefined;
}
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, entry);
return entry.value;
}
/**
* Set a value in the cache
* Evicts least recently used item if cache is full
*/
set(key: string, value: T): void {
// Delete existing entry if present (to update position)
if (this.cache.has(key)) {
this.cache.delete(key);
}
// Evict oldest entries if at capacity
while (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey) {
const evictedEntry = this.cache.get(firstKey);
this.cache.delete(firstKey);
if (this.onEvict && evictedEntry) {
this.onEvict(firstKey, evictedEntry.value);
}
}
}
// Add new entry
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
/**
* Check if key exists in cache
*/
has(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
// Check expiration
if (this.maxAge && Date.now() - entry.timestamp > this.maxAge) {
this.delete(key);
return false;
}
return true;
}
/**
* Delete a key from cache
*/
delete(key: string): boolean {
const entry = this.cache.get(key);
if (entry && this.onEvict) {
this.onEvict(key, entry.value);
}
return this.cache.delete(key);
}
/**
* Clear all entries
*/
clear(): void {
if (this.onEvict) {
for (const [key, entry] of this.cache.entries()) {
this.onEvict(key, entry.value);
}
}
this.cache.clear();
}
/**
* Get current cache size
*/
get size(): number {
return this.cache.size;
}
/**
* Clean up expired entries
*/
cleanExpired(): void {
if (!this.maxAge) return;
const now = Date.now();
const expiredKeys: string[] = [];
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.maxAge) {
expiredKeys.push(key);
}
}
for (const key of expiredKeys) {
this.delete(key);
}
}
/**
* Get all keys in cache (oldest to newest)
*/
keys(): string[] {
return Array.from(this.cache.keys());
}
/**
* Get cache statistics
*/
getStats(): { size: number; maxSize: number; utilization: number } {
return {
size: this.cache.size,
maxSize: this.maxSize,
utilization: this.cache.size / this.maxSize
};
}
}