filetree-pro
Version:
A powerful file tree generator for VS Code and Cursor. Generate beautiful file trees in multiple formats with smart exclusions and custom configurations.
455 lines (405 loc) • 10.7 kB
text/typescript
/**
* LRU (Least Recently Used) cache manager with TTL (Time To Live) support.
* Implements efficient cache eviction to prevent memory leaks.
*
* Data Structure: Doubly-linked list + HashMap for O(1) operations
*
* @module cacheManager
* @author FileTree Pro Team
* @since 0.2.0
*/
/**
* Node in the doubly-linked list for LRU cache
* Each node stores a key-value pair with timestamp
*/
class CacheNode<K, V> {
constructor(
public key: K,
public value: V,
public timestamp: number,
public prev: CacheNode<K, V> | null = null,
public next: CacheNode<K, V> | null = null
) {}
}
/**
* Cache entry metadata for telemetry and debugging
*/
export interface CacheEntry<V> {
/** Cached value */
readonly value: V;
/** Timestamp when entry was created/updated */
readonly timestamp: number;
/** Number of times this entry was accessed */
readonly accessCount: number;
}
/**
* Cache statistics for monitoring and debugging
*/
export interface CacheStatistics {
/** Current number of entries in cache */
readonly size: number;
/** Maximum capacity of cache */
readonly capacity: number;
/** Total cache hits */
readonly hits: number;
/** Total cache misses */
readonly misses: number;
/** Cache hit rate (0-1) */
readonly hitRate: number;
/** Number of evictions performed */
readonly evictions: number;
/** Number of expirations performed */
readonly expirations: number;
}
/**
* LRU Cache with TTL support for memory-efficient caching.
*
* Features:
* - O(1) get, set, delete operations using HashMap + Doubly-linked list
* - Automatic eviction of least recently used entries when capacity is reached
* - Time-based expiration (TTL) for stale data removal
* - Cache statistics for monitoring and optimization
*
* Time Complexity:
* - get: O(1)
* - set: O(1)
* - delete: O(1)
* - clear: O(n)
* - cleanup: O(n) - should be called periodically, not on every operation
*
* Space Complexity: O(n) where n is the number of cached entries
*
* @template K - Key type (must be hashable)
* @template V - Value type
*
* @example
* ```typescript
* const cache = new CacheManager<string, FileNode>(100, 5 * 60 * 1000); // 100 entries, 5min TTL
*
* cache.set('key', value);
* const result = cache.get('key');
* if (result) {
* console.log('Cache hit:', result);
* }
*
* // Periodic cleanup (e.g., every 5 minutes)
* setInterval(() => cache.cleanup(), 5 * 60 * 1000);
* ```
*/
export class CacheManager<K, V> {
private cache: Map<K, CacheNode<K, V>> = new Map();
private head: CacheNode<K, V> | null = null; // Most recently used
private tail: CacheNode<K, V> | null = null; // Least recently used
private hits: number = 0;
private misses: number = 0;
private evictions: number = 0;
private expirations: number = 0;
private accessCounts: Map<K, number> = new Map();
/**
* Creates a new LRU cache with specified capacity and TTL.
*
* @param capacity - Maximum number of entries (default: 100)
* @param ttl - Time to live in milliseconds (default: 5 minutes)
*/
constructor(
private readonly capacity: number = 100,
private readonly ttl: number = 5 * 60 * 1000 // 5 minutes default
) {
if (capacity <= 0) {
throw new Error('Cache capacity must be positive');
}
if (ttl <= 0) {
throw new Error('Cache TTL must be positive');
}
}
/**
* Gets a value from the cache.
* Moves the accessed node to the head (most recently used).
* Returns undefined if key doesn't exist or entry is expired.
*
* Time Complexity: O(1)
*
* @param key - The cache key
* @returns The cached value or undefined if not found/expired
*/
public get(key: K): V | undefined {
const node = this.cache.get(key);
if (!node) {
this.misses++;
return undefined;
}
// Check if entry has expired
const now = Date.now();
if (now - node.timestamp > this.ttl) {
this.delete(key);
this.expirations++;
this.misses++;
return undefined;
}
// Move to head (most recently used)
this.moveToHead(node);
// Update access count
const currentCount = this.accessCounts.get(key) || 0;
this.accessCounts.set(key, currentCount + 1);
this.hits++;
return node.value;
}
/**
* Sets a value in the cache.
* If key exists, updates value and moves to head.
* If cache is full, evicts the least recently used entry.
*
* Time Complexity: O(1)
*
* @param key - The cache key
* @param value - The value to cache
*/
public set(key: K, value: V): void {
const existingNode = this.cache.get(key);
if (existingNode) {
// Update existing entry
existingNode.value = value;
existingNode.timestamp = Date.now();
this.moveToHead(existingNode);
} else {
// Create new entry
const newNode = new CacheNode(key, value, Date.now());
this.cache.set(key, newNode);
this.addToHead(newNode);
this.accessCounts.set(key, 0);
// Evict least recently used if over capacity
if (this.cache.size > this.capacity) {
this.evictTail();
}
}
}
/**
* Deletes a value from the cache.
*
* Time Complexity: O(1)
*
* @param key - The cache key to delete
* @returns true if key existed and was deleted, false otherwise
*/
public delete(key: K): boolean {
const node = this.cache.get(key);
if (!node) {
return false;
}
this.removeNode(node);
this.cache.delete(key);
this.accessCounts.delete(key);
return true;
}
/**
* Checks if a key exists in the cache and is not expired.
* Does NOT update LRU order (unlike get).
*
* Time Complexity: O(1)
*
* @param key - The cache key to check
* @returns true if key exists and is not expired
*/
public has(key: K): boolean {
const node = this.cache.get(key);
if (!node) {
return false;
}
// Check if expired
const now = Date.now();
if (now - node.timestamp > this.ttl) {
this.delete(key);
this.expirations++;
return false;
}
return true;
}
/**
* Clears all entries from the cache.
*
* Time Complexity: O(n)
*
* @returns Number of entries cleared
*/
public clear(): number {
const size = this.cache.size;
this.cache.clear();
this.accessCounts.clear();
this.head = null;
this.tail = null;
return size;
}
/**
* Removes all expired entries from the cache.
* Should be called periodically to prevent memory leaks.
*
* Time Complexity: O(n)
*
* @returns Number of expired entries removed
*/
public cleanup(): number {
const now = Date.now();
let expiredCount = 0;
const keysToDelete: K[] = [];
// Collect expired keys
for (const [key, node] of this.cache.entries()) {
if (now - node.timestamp > this.ttl) {
keysToDelete.push(key);
expiredCount++;
}
}
// Delete expired entries
for (const key of keysToDelete) {
this.delete(key);
}
this.expirations += expiredCount;
return expiredCount;
}
/**
* Gets the current size of the cache.
*
* Time Complexity: O(1)
*
* @returns Number of entries in cache
*/
public size(): number {
return this.cache.size;
}
/**
* Gets cache statistics for monitoring and debugging.
*
* Time Complexity: O(1)
*
* @returns Cache statistics including hit rate, evictions, etc.
*/
public getStatistics(): CacheStatistics {
const totalAccesses = this.hits + this.misses;
const hitRate = totalAccesses > 0 ? this.hits / totalAccesses : 0;
return {
size: this.cache.size,
capacity: this.capacity,
hits: this.hits,
misses: this.misses,
hitRate,
evictions: this.evictions,
expirations: this.expirations,
};
}
/**
* Gets detailed information about a cache entry.
* Does NOT update LRU order.
*
* Time Complexity: O(1)
*
* @param key - The cache key
* @returns Cache entry with metadata or undefined if not found
*/
public getEntry(key: K): CacheEntry<V> | undefined {
const node = this.cache.get(key);
if (!node) {
return undefined;
}
// Check if expired
const now = Date.now();
if (now - node.timestamp > this.ttl) {
this.delete(key);
this.expirations++;
return undefined;
}
return {
value: node.value,
timestamp: node.timestamp,
accessCount: this.accessCounts.get(key) || 0,
};
}
/**
* Resets cache statistics.
* Useful for testing or periodic statistics reset.
*
* Time Complexity: O(1)
*/
public resetStatistics(): void {
this.hits = 0;
this.misses = 0;
this.evictions = 0;
this.expirations = 0;
}
// ==================== Private Helper Methods ====================
/**
* Adds a node to the head (most recently used position).
*
* Time Complexity: O(1)
*
* @param node - The node to add
*/
private addToHead(node: CacheNode<K, V>): void {
node.next = this.head;
node.prev = null;
if (this.head) {
this.head.prev = node;
}
this.head = node;
if (!this.tail) {
this.tail = node;
}
}
/**
* Removes a node from the doubly-linked list.
*
* Time Complexity: O(1)
*
* @param node - The node to remove
*/
private removeNode(node: CacheNode<K, V>): void {
if (node.prev) {
node.prev.next = node.next;
} else {
this.head = node.next;
}
if (node.next) {
node.next.prev = node.prev;
} else {
this.tail = node.prev;
}
}
/**
* Moves a node to the head (marks as most recently used).
*
* Time Complexity: O(1)
*
* @param node - The node to move
*/
private moveToHead(node: CacheNode<K, V>): void {
this.removeNode(node);
this.addToHead(node);
}
/**
* Evicts the tail node (least recently used).
*
* Time Complexity: O(1)
*/
private evictTail(): void {
if (!this.tail) {
return;
}
const key = this.tail.key;
this.removeNode(this.tail);
this.cache.delete(key);
this.accessCounts.delete(key);
this.evictions++;
}
}
/**
* Creates a cache manager with default settings.
* Convenience function for common use cases.
*
* @param capacity - Maximum number of entries (default: 100)
* @param ttlMinutes - Time to live in minutes (default: 5)
* @returns New CacheManager instance
*/
export function createCache<K, V>(
capacity: number = 100,
ttlMinutes: number = 5
): CacheManager<K, V> {
return new CacheManager<K, V>(capacity, ttlMinutes * 60 * 1000);
}