Files
Codeman/src/utils/lru-map.ts
T
arkonandClaude Opus 4.7 02e2f3e8b5 refactor: remove dead code and narrow internal exports (knip sweep)
Knip-driven cleanup. All changes verified with tsc --noEmit, lint, and
build.

Removed (zero consumers):
- VERIFICATION_PROMPT constant + its barrel re-export
- createInitialOrchestratorPersistState factory
- transcriptWatcher singleton export
- createAnsiPatternFull / createAnsiPatternSimple factories
- TimerInfo interface + unused AiCheckResult/AiPlanCheckResult imports
  in respawn-controller.ts
- 35 unused Zod z.infer \`*Input\` types in schemas.ts
- Dead re-exports: SessionMode from session.ts, AuthSessionRecord from
  web/ports/index.ts, EnhancedPlanTask/CheckpointReview from
  ralph-tracker.ts, 7 unused entries in utils/index.ts
- 14 event/config interfaces that lived only as JSDoc hints (no TS type
  position usage): Session/Respawn/RalphLoop/RalphTracker/
  SessionManager/SessionAutoOps/Subagent/TaskQueue/TaskTracker/
  TranscriptWatcher/Image/OrchestratorLoop Events + RespawnPreset +
  SessionOutput

Narrowed to module scope (kept but no longer exported):
- buildPermissionArgs in session-cli-builder.ts
- 28 type/interface declarations used only within their own file:
  Ai{Idle,Plan}Check{Config,State}, BashToolParser{Events,Config},
  FileStream/CreateStream{Options,Result}, PlanSubagentEvent,
  SubagentCallback, RalphLoopConfig, RalphLoop{Events,Options},
  ActiveTimerInfo, DetectionStatus, ActionLogEntry, AutoOpsCallbacks,
  TunnelStatus, Timer/LRUMap/StaleExpirationMap Options, AuthState,
  SessionListenerDeps, SseStreamManagerDeps, and 8 more

Docs: CLAUDE.md advice for global-regex `lastIndex` now points to the
remaining `execPattern()` helper instead of the deleted factories.

Knip delta: unused files 42→0, unused exports 161→16, unused types 92→0.
The 16 remaining exports are a mobile-test helper toolkit intentionally
kept for upcoming tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:57:00 +02:00

264 lines
6.9 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.
*/
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;
/** Tracks the newest key for O(1) newest() access */
private _newestKey: K | undefined = undefined;
/**
* 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);
// Track newest key for O(1) newest() access
this._newestKey = key;
// 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);
// Track newest key for O(1) newest() access
this._newestKey = key;
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);
}
/**
* Delete a key-value pair.
* Updates _newestKey if the deleted key was the newest.
*
* @param key - Key to delete
* @returns True if the key existed and was deleted
*/
override delete(key: K): boolean {
const existed = super.delete(key);
// If we deleted the newest key, we need to find the new newest
// This is O(n) but delete is rare; set/get are the hot paths
if (existed && this._newestKey === key) {
this._newestKey = undefined;
// Find the new newest by iterating (last entry)
for (const k of super.keys()) {
this._newestKey = k;
}
}
return existed;
}
/**
* Clear all entries.
* Resets _newestKey to undefined.
*/
override clear(): void {
super.clear();
this._newestKey = undefined;
}
/**
* 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).
* O(1) operation using tracked newest key.
*
* @returns [key, value] of newest entry, or undefined if empty
*/
newest(): [K, V] | undefined {
if (this._newestKey === undefined || !super.has(this._newestKey)) {
return undefined;
}
// Use super.get to avoid refreshing the position
const value = super.get(this._newestKey)!;
return [this._newestKey, value];
}
/**
* 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;
let deletedNewest = false;
// Iterate from oldest to newest
for (const [key, value] of super.entries()) {
if (getTimestamp(value) < cutoff) {
// Track if we're deleting the newest key
if (key === this._newestKey) {
deletedNewest = true;
}
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;
}
}
// Update _newestKey if we deleted it (find new newest from remaining entries)
if (deletedNewest) {
this._newestKey = undefined;
for (const k of super.keys()) {
this._newestKey = k; // Last one becomes newest
}
}
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);
}
}