mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
214 lines
5.3 KiB
TypeScript
214 lines
5.3 KiB
TypeScript
/**
|
|||
|
|
* @fileoverview LRU (Least Recently Used) Map implementation.
|
||
|
|
*
|
||
|
|
* Extends the built-in Map with automatic eviction when a maximum size is
|
||
|
|
* exceeded. Uses Map's insertion-order iteration for O(1) LRU eviction.
|
||
|
|
*
|
||
|
|
* @module utils/lru-map
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Configuration options for LRUMap.
|
||
|
|
*/
|
||
|
|
export interface LRUMapOptions<K, V> {
|
||
|
|
/** Maximum number of entries before eviction */
|
||
|
|
maxSize: number;
|
||
|
|
/** Optional callback when an entry is evicted */
|
||
|
|
onEvict?: (key: K, value: V) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A Map with automatic LRU (Least Recently Used) eviction.
|
||
|
|
*
|
||
|
|
* When the map exceeds maxSize, the oldest entries are evicted.
|
||
|
|
* Access via get() refreshes an entry's position (moves to most recent).
|
||
|
|
*
|
||
|
|
* Uses JavaScript Map's insertion-order guarantee for efficient LRU behavior.
|
||
|
|
* All operations are O(1) amortized.
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* const cache = new LRUMap<string, number>({
|
||
|
|
* maxSize: 100,
|
||
|
|
* onEvict: (key, value) => console.log(`Evicted ${key}`)
|
||
|
|
* });
|
||
|
|
*
|
||
|
|
* cache.set('a', 1);
|
||
|
|
* cache.set('b', 2);
|
||
|
|
* cache.get('a'); // Refreshes 'a', making 'b' the oldest
|
||
|
|
* // When full, 'b' would be evicted first
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export class LRUMap<K, V> extends Map<K, V> {
|
||
|
|
private readonly maxSize: number;
|
||
|
|
private readonly onEvict?: (key: K, value: V) => void;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Creates a new LRUMap.
|
||
|
|
*
|
||
|
|
* @param options - Configuration options
|
||
|
|
*/
|
||
|
|
constructor(options: LRUMapOptions<K, V>) {
|
||
|
|
super();
|
||
|
|
this.maxSize = options.maxSize;
|
||
|
|
this.onEvict = options.onEvict;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set a key-value pair.
|
||
|
|
* If key exists, updates value and refreshes position.
|
||
|
|
* If adding new entry would exceed maxSize, evicts oldest entries.
|
||
|
|
*
|
||
|
|
* @param key - Key to set
|
||
|
|
* @param value - Value to associate
|
||
|
|
* @returns this (for chaining)
|
||
|
|
*/
|
||
|
|
override set(key: K, value: V): this {
|
||
|
|
// If key exists, delete first to refresh position
|
||
|
|
if (super.has(key)) {
|
||
|
|
super.delete(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add the entry (will be at end = most recent)
|
||
|
|
super.set(key, value);
|
||
|
|
|
||
|
|
// Evict oldest entries if over capacity
|
||
|
|
while (super.size > this.maxSize) {
|
||
|
|
const oldestKey = super.keys().next().value;
|
||
|
|
if (oldestKey !== undefined) {
|
||
|
|
const oldestValue = super.get(oldestKey)!;
|
||
|
|
super.delete(oldestKey);
|
||
|
|
this.onEvict?.(oldestKey, oldestValue);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a value and refresh its position (mark as most recently used).
|
||
|
|
*
|
||
|
|
* @param key - Key to look up
|
||
|
|
* @returns Value if found, undefined otherwise
|
||
|
|
*/
|
||
|
|
override get(key: K): V | undefined {
|
||
|
|
if (!super.has(key)) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete and re-insert to move to end (most recent)
|
||
|
|
const value = super.get(key)!;
|
||
|
|
super.delete(key);
|
||
|
|
super.set(key, value);
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a key exists WITHOUT refreshing its position.
|
||
|
|
* Use this when you want to check existence without affecting LRU order.
|
||
|
|
*
|
||
|
|
* @param key - Key to check
|
||
|
|
* @returns True if key exists
|
||
|
|
*/
|
||
|
|
override has(key: K): boolean {
|
||
|
|
return super.has(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Peek at a value WITHOUT refreshing its position.
|
||
|
|
* Use this when you want to read without affecting LRU order.
|
||
|
|
*
|
||
|
|
* @param key - Key to peek
|
||
|
|
* @returns Value if found, undefined otherwise
|
||
|
|
*/
|
||
|
|
peek(key: K): V | undefined {
|
||
|
|
return super.get(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the oldest entry (next to be evicted) without removing it.
|
||
|
|
*
|
||
|
|
* @returns [key, value] of oldest entry, or undefined if empty
|
||
|
|
*/
|
||
|
|
oldest(): [K, V] | undefined {
|
||
|
|
const first = super.entries().next();
|
||
|
|
if (first.done) return undefined;
|
||
|
|
return first.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the newest entry (most recently accessed).
|
||
|
|
*
|
||
|
|
* @returns [key, value] of newest entry, or undefined if empty
|
||
|
|
*/
|
||
|
|
newest(): [K, V] | undefined {
|
||
|
|
let last: [K, V] | undefined;
|
||
|
|
for (const entry of super.entries()) {
|
||
|
|
last = entry;
|
||
|
|
}
|
||
|
|
return last;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Evict entries older than a specific timestamp.
|
||
|
|
* Assumes values have a timestamp property or are numbers representing time.
|
||
|
|
*
|
||
|
|
* @param maxAge - Maximum age in milliseconds
|
||
|
|
* @param getTimestamp - Function to extract timestamp from value
|
||
|
|
* @returns Number of entries evicted
|
||
|
|
*/
|
||
|
|
expireOlderThan(maxAge: number, getTimestamp: (value: V) => number): number {
|
||
|
|
const now = Date.now();
|
||
|
|
const cutoff = now - maxAge;
|
||
|
|
let evicted = 0;
|
||
|
|
|
||
|
|
// Iterate from oldest to newest
|
||
|
|
for (const [key, value] of super.entries()) {
|
||
|
|
if (getTimestamp(value) < cutoff) {
|
||
|
|
super.delete(key);
|
||
|
|
this.onEvict?.(key, value);
|
||
|
|
evicted++;
|
||
|
|
} else {
|
||
|
|
// Since entries are ordered, once we find a non-expired one,
|
||
|
|
// all subsequent entries are also non-expired
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return evicted;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all keys in order from oldest to newest.
|
||
|
|
*
|
||
|
|
* @returns Array of keys
|
||
|
|
*/
|
||
|
|
keysInOrder(): K[] {
|
||
|
|
return Array.from(super.keys());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all values in order from oldest to newest.
|
||
|
|
*
|
||
|
|
* @returns Array of values
|
||
|
|
*/
|
||
|
|
valuesInOrder(): V[] {
|
||
|
|
return Array.from(super.values());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the maximum size limit.
|
||
|
|
*/
|
||
|
|
get maxEntries(): number {
|
||
|
|
return this.maxSize;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the number of free slots before eviction would occur.
|
||
|
|
*/
|
||
|
|
get freeSlots(): number {
|
||
|
|
return Math.max(0, this.maxSize - super.size);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default LRUMap;
|