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:
arkon
2026-01-28 04:35:20 +01:00
co-authored by Claude Opus 4.5
parent 0a928144da
commit 83e5b0b78e
10 changed files with 1802 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* @fileoverview Centralized buffer size limits for memory management.
*
* These constants define the maximum sizes for various buffers throughout
* Claudeman. Consolidating them here ensures consistent limits and makes
* it easy to tune memory usage.
*
* Memory Budget Rationale (for 20 concurrent sessions):
* - Terminal buffer: 2MB max × 20 = 40MB worst case
* - Text output: 1MB max × 20 = 20MB worst case
* - Messages: ~1KB each × 1000 × 20 = 20MB worst case
* - Total buffer overhead: ~80MB (acceptable for long-running server)
*
* @module config/buffer-limits
*/
// ============================================================================
// Terminal Buffer Limits
// ============================================================================
/**
* Maximum terminal buffer size in characters.
* Contains raw terminal output with ANSI escape sequences.
* Reduced from 5MB to 2MB for better render performance.
*/
export const MAX_TERMINAL_BUFFER_SIZE = 2 * 1024 * 1024; // 2MB
/**
* Size to trim terminal buffer to when max is exceeded.
* Keeps the most recent portion to preserve context.
*/
export const TRIM_TERMINAL_TO = 1.5 * 1024 * 1024; // 1.5MB
// ============================================================================
// Text Output Buffer Limits
// ============================================================================
/**
* Maximum text output buffer size in characters.
* Contains ANSI-stripped text for search and analysis.
*/
export const MAX_TEXT_OUTPUT_SIZE = 1 * 1024 * 1024; // 1MB
/**
* Size to trim text output buffer to when max is exceeded.
*/
export const TRIM_TEXT_TO = 768 * 1024; // 768KB
// ============================================================================
// Message Buffer Limits
// ============================================================================
/**
* Maximum number of Claude JSON messages to keep in memory per session.
* Older messages are discarded when limit is exceeded.
*/
export const MAX_MESSAGES = 1000;
/**
* Number of messages to keep when trimming (80% of max).
*/
export const TRIM_MESSAGES_TO = 800;
// ============================================================================
// Line Buffer Limits
// ============================================================================
/**
* Maximum line buffer size in characters.
* Prevents unbounded growth for extremely long lines without newlines.
*/
export const MAX_LINE_BUFFER_SIZE = 64 * 1024; // 64KB
// ============================================================================
// Respawn Controller Buffer Limits
// ============================================================================
/**
* Maximum respawn controller buffer size.
* Smaller than session buffer since it's only used for idle detection.
*/
export const MAX_RESPAWN_BUFFER_SIZE = 1 * 1024 * 1024; // 1MB
/**
* Size to trim respawn buffer to when max is exceeded.
*/
export const TRIM_RESPAWN_BUFFER_TO = 512 * 1024; // 512KB
// ============================================================================
// Run Summary Limits
// ============================================================================
/**
* Maximum number of events to keep in run summary.
*/
export const MAX_RUN_SUMMARY_EVENTS = 1000;
/**
* Number of events to keep when trimming run summary.
*/
export const TRIM_RUN_SUMMARY_TO = 800;
// ============================================================================
// Spawn Message Limits
// ============================================================================
/**
* Maximum messages per spawn communication channel.
*/
export const MAX_MESSAGES_PER_CHANNEL = 100;
+137
View File
@@ -0,0 +1,137 @@
/**
* @fileoverview Centralized Map size limits for memory management.
*
* These constants define maximum sizes for Maps that track ephemeral data.
* Without limits, long-running sessions can accumulate unbounded entries
* leading to memory leaks.
*
* Memory Budget Rationale:
* - Assuming average entry size of ~1KB
* - MAX_TRACKED_AGENTS=500 × 1KB = ~500KB for agent tracking
* - Activity/results per agent × agents = bounded by these limits
* - Total Map overhead: <50MB even under heavy load
*
* @module config/map-limits
*/
// ============================================================================
// Agent Tracking Limits
// ============================================================================
/**
* Maximum number of agents to track across all sessions.
* Oldest agents are evicted when limit is exceeded (LRU policy).
*/
export const MAX_TRACKED_AGENTS = 500;
/**
* Maximum activity entries to keep per agent.
* Includes tool calls, status updates, progress reports.
*/
export const MAX_SUBAGENT_ACTIVITY_PER_AGENT = 100;
/**
* Maximum tool results to keep per agent.
* Prevents memory growth from long-running agents with many tool calls.
*/
export const MAX_TOOL_RESULTS_PER_AGENT = 200;
// ============================================================================
// Hook Event Limits
// ============================================================================
/**
* Maximum pending hook events to queue.
* Prevents unbounded growth if hook processing is slow.
*/
export const MAX_PENDING_HOOKS = 50;
// ============================================================================
// Session Tracking Limits
// ============================================================================
/**
* Maximum concurrent sessions allowed.
* Each session consumes significant resources (PTY, buffers, watchers).
*/
export const MAX_CONCURRENT_SESSIONS = 50;
/**
* Maximum session history entries to keep (for analytics).
*/
export const MAX_SESSION_HISTORY = 100;
// ============================================================================
// SSE Client Limits
// ============================================================================
/**
* Maximum SSE clients per session.
* Prevents resource exhaustion from many browser tabs.
*/
export const MAX_SSE_CLIENTS_PER_SESSION = 10;
/**
* Maximum total SSE clients across all sessions.
*/
export const MAX_TOTAL_SSE_CLIENTS = 100;
// ============================================================================
// File Watcher Limits
// ============================================================================
/**
* Maximum file watchers (FSWatcher) to allow.
* Linux default max_user_watches is 8192-65536.
* We warn at 80% capacity and evict idle watchers.
*/
export const MAX_FILE_WATCHERS = 500;
/**
* Warning threshold as percentage of max watchers.
*/
export const FILE_WATCHER_WARNING_THRESHOLD = 0.8;
// ============================================================================
// Task Tracking Limits
// ============================================================================
/**
* Maximum tasks to keep in the task queue.
*/
export const MAX_QUEUED_TASKS = 100;
/**
* Maximum completed tasks to keep for history.
*/
export const MAX_COMPLETED_TASKS_HISTORY = 50;
// ============================================================================
// Todo Item Limits (Ralph Tracker)
// ============================================================================
/**
* Maximum todo items to track per session.
*/
export const MAX_TODOS_PER_SESSION = 500;
/**
* TTL for completed todo items before cleanup (1 hour).
*/
export const COMPLETED_TODO_TTL_MS = 60 * 60 * 1000;
// ============================================================================
// Pending Tool Calls Limits
// ============================================================================
/**
* Maximum pending tool calls to track per subagent.
* Entries should be cleaned up on tool_result, but this prevents leaks.
*/
export const MAX_PENDING_TOOL_CALLS = 100;
/**
* TTL for orphaned pending tool calls (5 minutes).
* If no tool_result received, entry is cleaned up.
*/
export const PENDING_TOOL_CALL_TTL_MS = 5 * 60 * 1000;
+71
View File
@@ -11,6 +11,77 @@
* - Inner loop tracking (Ralph Wiggum detection)
*/
// ========== Resource Management Types ==========
/**
* Interface for objects that hold resources requiring explicit cleanup.
* Implementing classes should release timers, watchers, and other resources in dispose().
*/
export interface Disposable {
/** Release all held resources. Safe to call multiple times. */
dispose(): void;
/** Whether this object has been disposed */
readonly isDisposed: boolean;
}
/**
* Configuration for buffer accumulator instances.
* Used for terminal buffers, text output, and other size-limited string storage.
*/
export interface BufferConfig {
/** Maximum buffer size in bytes before trimming */
maxSize: number;
/** Size to trim to when maxSize is exceeded */
trimSize: number;
/** Optional callback invoked when buffer is trimmed */
onTrim?: (trimmedBytes: number) => void;
}
/**
* Memory metrics for monitoring and debugging.
* Extends Node.js process.memoryUsage() with application-specific tracking.
*/
export interface MemoryMetrics {
/** Heap memory used by V8 (bytes) */
heapUsed: number;
/** Total heap size allocated by V8 (bytes) */
heapTotal: number;
/** Memory used by C++ objects bound to JavaScript (bytes) */
external: number;
/** Memory used by ArrayBuffers and SharedArrayBuffers (bytes) */
arrayBuffers: number;
/** Sizes of tracked Maps by name */
mapSizes: Record<string, number>;
/** Number of active timers (setTimeout/setInterval) */
timerCount: number;
/** Number of active file system watchers */
watcherCount: number;
/** Timestamp when metrics were collected */
timestamp: number;
}
/**
* Resource types that can be registered for cleanup.
*/
export type CleanupResourceType = 'timer' | 'interval' | 'watcher' | 'listener' | 'stream';
/**
* Registration entry for a cleanup resource.
* Used by CleanupManager to track and dispose resources.
*/
export interface CleanupRegistration {
/** Unique identifier for this registration */
id: string;
/** Type of resource */
type: CleanupResourceType;
/** Human-readable description for debugging */
description: string;
/** Cleanup function to call on dispose */
cleanup: () => void;
/** Timestamp when registered */
registeredAt: number;
}
// ========== Core Status Types ==========
/** Status of a Claude session */
+190
View File
@@ -0,0 +1,190 @@
/**
* @fileoverview High-performance buffer accumulator for string data.
*
* This utility reduces GC pressure by avoiding repeated string concatenation (`+=`).
* Instead, chunks are pushed to an array and joined only when needed.
* Automatically trims when size limits are exceeded.
*
* @module utils/buffer-accumulator
*/
import type { BufferConfig } from '../types.js';
/**
* High-performance buffer accumulator using array-based collection.
*
* Features:
* - Efficient append via array push (avoids O(n) string concat)
* - Lazy join on read (consolidates only when accessed)
* - Automatic trimming when max size exceeded
* - Optional callback on trim for logging/metrics
*
* @example
* ```typescript
* const buffer = new BufferAccumulator({
* maxSize: 2 * 1024 * 1024, // 2MB
* trimSize: 1.5 * 1024 * 1024, // Trim to 1.5MB
* onTrim: (bytes) => console.log(`Trimmed ${bytes} bytes`)
* });
*
* buffer.append('chunk1');
* buffer.append('chunk2');
* console.log(buffer.value); // Joins and returns full content
* ```
*/
export class BufferAccumulator {
private chunks: string[] = [];
private totalLength: number = 0;
private readonly maxSize: number;
private readonly trimSize: number;
private readonly onTrim?: (trimmedBytes: number) => void;
/**
* Creates a new BufferAccumulator.
*
* @param config - Buffer configuration with max/trim sizes
*/
constructor(config: BufferConfig);
/**
* Creates a new BufferAccumulator with simple size parameters.
*
* @param maxSize - Maximum buffer size before trimming
* @param trimSize - Size to trim to when max is exceeded
*/
constructor(maxSize: number, trimSize: number);
constructor(configOrMaxSize: BufferConfig | number, trimSize?: number) {
if (typeof configOrMaxSize === 'number') {
this.maxSize = configOrMaxSize;
this.trimSize = trimSize!;
this.onTrim = undefined;
} else {
this.maxSize = configOrMaxSize.maxSize;
this.trimSize = configOrMaxSize.trimSize;
this.onTrim = configOrMaxSize.onTrim;
}
}
/**
* Append data to the buffer.
* Automatically trims if maxSize is exceeded.
*
* @param data - String data to append
*/
append(data: string): void {
if (!data) return;
this.chunks.push(data);
this.totalLength += data.length;
// Trim if exceeded max size
if (this.totalLength > this.maxSize) {
this.trim();
}
}
/**
* Get the full buffer content.
* Consolidates chunks on access for efficient subsequent reads.
*/
get value(): string {
if (this.chunks.length === 0) return '';
if (this.chunks.length === 1) return this.chunks[0];
// Consolidate chunks on access
const result = this.chunks.join('');
this.chunks = [result];
return result;
}
/**
* Get current buffer length without joining chunks.
*/
get length(): number {
return this.totalLength;
}
/**
* Check if buffer is empty.
*/
get isEmpty(): boolean {
return this.totalLength === 0;
}
/**
* Clear the buffer completely.
*/
clear(): void {
this.chunks = [];
this.totalLength = 0;
}
/**
* Set buffer to a specific value, replacing all content.
*
* @param value - New buffer content
*/
set(value: string): void {
this.chunks = value ? [value] : [];
this.totalLength = value?.length || 0;
}
/**
* Get the last N characters from the buffer.
* Useful for checking recent content without reading the entire buffer.
*
* @param n - Number of characters to get from the end
* @returns The last N characters (or entire buffer if smaller)
*/
tail(n: number): string {
if (n >= this.totalLength) {
return this.value;
}
const full = this.value;
return full.slice(-n);
}
/**
* Check if buffer ends with a specific string.
*
* @param suffix - String to check for
* @returns True if buffer ends with the suffix
*/
endsWith(suffix: string): boolean {
if (!suffix) return true; // All strings end with empty string
if (suffix.length > this.totalLength) return false;
return this.tail(suffix.length) === suffix;
}
/**
* Search for a pattern in the buffer (from the end).
*
* @param pattern - String or RegExp to search for
* @param fromEnd - Number of characters from end to search within (default: entire buffer)
* @returns True if pattern is found
*/
contains(pattern: string | RegExp, fromEnd?: number): boolean {
const searchIn = fromEnd ? this.tail(fromEnd) : this.value;
if (typeof pattern === 'string') {
return searchIn.includes(pattern);
}
return pattern.test(searchIn);
}
/**
* Trim buffer to keep only the most recent data.
* Called automatically when maxSize is exceeded.
*/
private trim(): void {
const full = this.chunks.join('');
const trimmedBytes = full.length - this.trimSize;
const trimmed = full.slice(-this.trimSize);
this.chunks = [trimmed];
this.totalLength = trimmed.length;
// Notify callback if configured
if (this.onTrim && trimmedBytes > 0) {
this.onTrim(trimmedBytes);
}
}
}
export default BufferAccumulator;
+330
View File
@@ -0,0 +1,330 @@
/**
* @fileoverview Centralized resource cleanup manager.
*
* Tracks timers, intervals, watchers, and other resources that need explicit
* cleanup. Provides a unified dispose() method that safely cleans up all
* registered resources, even on partial failures.
*
* @module utils/cleanup-manager
*/
import { v4 as uuidv4 } from 'uuid';
import type { Disposable, CleanupRegistration, CleanupResourceType } from '../types.js';
/**
* Options for setTimeout/setInterval with automatic cleanup.
*/
export interface TimerOptions {
/** Human-readable description for debugging */
description?: string;
}
/**
* Centralized manager for tracking and disposing resources.
*
* Implements the Disposable interface for hierarchical cleanup.
* All registered resources are cleaned up when dispose() is called.
*
* Features:
* - Tracks timers, intervals, watchers, and custom cleanup functions
* - isStopped guard to prevent callbacks firing after disposal
* - Safe disposal that continues even if individual cleanups fail
* - Debug logging for resource tracking
*
* @example
* ```typescript
* class MyService implements Disposable {
* private cleanup = new CleanupManager();
*
* start() {
* // Timer automatically cleared on dispose
* this.cleanup.setTimeout(() => {
* if (this.cleanup.isStopped) return; // Guard
* this.doWork();
* }, 5000, { description: 'work timer' });
* }
*
* dispose() {
* this.cleanup.dispose();
* }
*
* get isDisposed() { return this.cleanup.isDisposed; }
* }
* ```
*/
export class CleanupManager implements Disposable {
private registrations = new Map<string, CleanupRegistration>();
private _isDisposed = false;
private readonly debugMode: boolean;
/**
* Creates a new CleanupManager.
*
* @param debug - Enable debug logging for resource tracking
*/
constructor(debug = false) {
this.debugMode = debug;
}
/**
* Whether this manager has been disposed.
* Check this before executing callbacks to prevent zombie operations.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Alias for isDisposed - check before executing async callbacks.
*/
get isStopped(): boolean {
return this._isDisposed;
}
/**
* Number of currently registered resources.
*/
get resourceCount(): number {
return this.registrations.size;
}
/**
* Get counts by resource type for metrics/debugging.
*/
get resourceCounts(): Record<CleanupResourceType, number> {
const counts: Record<CleanupResourceType, number> = {
timer: 0,
interval: 0,
watcher: 0,
listener: 0,
stream: 0,
};
for (const reg of this.registrations.values()) {
counts[reg.type]++;
}
return counts;
}
/**
* Schedule a timeout that will be automatically cleared on dispose.
*
* @param callback - Function to call when timeout fires
* @param delay - Delay in milliseconds
* @param options - Optional configuration
* @returns Timer ID for manual clearing if needed
*/
setTimeout(
callback: () => void,
delay: number,
options?: TimerOptions
): string {
const id = uuidv4();
const timeoutId = setTimeout(() => {
// Remove registration when timer fires naturally
this.registrations.delete(id);
// Don't execute if already stopped
if (this._isDisposed) return;
callback();
}, delay);
this.register({
id,
type: 'timer',
description: options?.description || `setTimeout(${delay}ms)`,
cleanup: () => clearTimeout(timeoutId),
registeredAt: Date.now(),
});
return id;
}
/**
* Schedule an interval that will be automatically cleared on dispose.
*
* @param callback - Function to call on each interval
* @param delay - Interval in milliseconds
* @param options - Optional configuration
* @returns Interval ID for manual clearing if needed
*/
setInterval(
callback: () => void,
delay: number,
options?: TimerOptions
): string {
const id = uuidv4();
const intervalId = setInterval(() => {
// Don't execute if stopped
if (this._isDisposed) return;
callback();
}, delay);
this.register({
id,
type: 'interval',
description: options?.description || `setInterval(${delay}ms)`,
cleanup: () => clearInterval(intervalId),
registeredAt: Date.now(),
});
return id;
}
/**
* Register a custom cleanup function.
*
* @param type - Type of resource for categorization
* @param cleanup - Function to call on dispose
* @param description - Human-readable description
* @returns Registration ID for manual removal if needed
*/
registerCleanup(
type: CleanupResourceType,
cleanup: () => void,
description: string
): string {
const id = uuidv4();
this.register({
id,
type,
description,
cleanup,
registeredAt: Date.now(),
});
return id;
}
/**
* Register a file system watcher for cleanup.
*
* @param watcher - Object with close() method (FSWatcher, chokidar, etc.)
* @param description - Human-readable description
* @returns Registration ID
*/
registerWatcher(
watcher: { close: () => void },
description: string
): string {
return this.registerCleanup('watcher', () => watcher.close(), description);
}
/**
* Register an event listener for cleanup.
*
* @param emitter - Object with removeListener/off method
* @param event - Event name
* @param listener - Listener function
* @param description - Human-readable description
* @returns Registration ID
*/
registerListener<T extends { removeListener?: (event: string, listener: () => void) => void; off?: (event: string, listener: () => void) => void }>(
emitter: T,
event: string,
listener: () => void,
description: string
): string {
return this.registerCleanup('listener', () => {
if (emitter.removeListener) {
emitter.removeListener(event, listener);
} else if (emitter.off) {
emitter.off(event, listener);
}
}, description);
}
/**
* Register a stream for cleanup.
*
* @param stream - Object with destroy() or close() method
* @param description - Human-readable description
* @returns Registration ID
*/
registerStream(
stream: { destroy?: () => void; close?: () => void },
description: string
): string {
return this.registerCleanup('stream', () => {
if (stream.destroy) {
stream.destroy();
} else if (stream.close) {
stream.close();
}
}, description);
}
/**
* Manually remove a registered resource.
*
* @param id - Registration ID returned from register methods
* @returns True if resource was found and removed
*/
unregister(id: string): boolean {
const reg = this.registrations.get(id);
if (!reg) return false;
try {
reg.cleanup();
} catch (err) {
this.debug(`Error cleaning up ${reg.type} "${reg.description}": ${err}`);
}
this.registrations.delete(id);
return true;
}
/**
* Dispose all registered resources.
* Safe to call multiple times (idempotent).
* Continues cleanup even if individual resources fail.
*/
dispose(): void {
if (this._isDisposed) return;
this._isDisposed = true;
const errors: Array<{ description: string; error: unknown }> = [];
for (const reg of this.registrations.values()) {
try {
reg.cleanup();
this.debug(`Cleaned up ${reg.type}: ${reg.description}`);
} catch (err) {
errors.push({ description: reg.description, error: err });
this.debug(`Error cleaning up ${reg.type} "${reg.description}": ${err}`);
}
}
this.registrations.clear();
if (errors.length > 0) {
console.error(`[CleanupManager] ${errors.length} errors during disposal:`,
errors.map(e => e.description).join(', '));
}
}
/**
* Get all current registrations for debugging.
*/
getRegistrations(): CleanupRegistration[] {
return Array.from(this.registrations.values());
}
/**
* Internal: Register a cleanup entry.
*/
private register(reg: CleanupRegistration): void {
this.registrations.set(reg.id, reg);
this.debug(`Registered ${reg.type}: ${reg.description}`);
}
/**
* Internal: Debug logging.
*/
private debug(message: string): void {
if (this.debugMode) {
console.log(`[CleanupManager] ${message}`);
}
}
}
export default CleanupManager;
+11
View File
@@ -0,0 +1,11 @@
/**
* @fileoverview Utility module exports.
*
* This module re-exports all utility classes and functions for easy import.
*
* @module utils
*/
export { BufferAccumulator } from './buffer-accumulator.js';
export { LRUMap, type LRUMapOptions } from './lru-map.js';
export { CleanupManager, type TimerOptions } from './cleanup-manager.js';
+214
View File
@@ -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;