mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
feat: add resource management types and utilities for memory optimization
- Add Disposable, BufferConfig, MemoryMetrics, CleanupRegistration types - Create src/config/buffer-limits.ts with consolidated buffer size constants - Create src/config/map-limits.ts with Map size limits to prevent unbounded growth - Implement BufferAccumulator utility with configurable trim and onTrim callback - Implement LRUMap with automatic eviction and O(1) operations - Implement CleanupManager for unified resource cleanup with isStopped guard - Add comprehensive tests for all new utilities This lays the foundation for memory leak prevention and performance improvements. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @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;
|
||||
Reference in New Issue
Block a user