135 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			135 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * MetadataCache - TTL + simple LRU cache
 | 
						|
 * 
 | 
						|
 * Features:
 | 
						|
 * - TTL-based expiration
 | 
						|
 * - Simple FIFO eviction when maxItems exceeded
 | 
						|
 * - read-through helper for async producers
 | 
						|
 * - Pseudo-LRU via Map re-insertion
 | 
						|
 */
 | 
						|
 | 
						|
export class MetadataCache {
 | 
						|
  constructor({ ttlMs = 5 * 60 * 1000, maxItems = 10_000 } = {}) {
 | 
						|
    this.ttlMs = ttlMs;
 | 
						|
    this.maxItems = maxItems;
 | 
						|
    this.store = new Map(); // key -> { value, exp }
 | 
						|
    this.stats = {
 | 
						|
      hits: 0,
 | 
						|
      misses: 0,
 | 
						|
      evictions: 0,
 | 
						|
      sets: 0
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  _now() {
 | 
						|
    return Date.now();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Evict oldest entries if cache exceeds maxItems
 | 
						|
   */
 | 
						|
  _evictIfNeeded() {
 | 
						|
    if (this.store.size <= this.maxItems) return;
 | 
						|
 | 
						|
    const toEvict = this.store.size - this.maxItems + Math.floor(this.maxItems * 0.1);
 | 
						|
    let evicted = 0;
 | 
						|
 | 
						|
    for (const k of this.store.keys()) {
 | 
						|
      this.store.delete(k);
 | 
						|
      evicted++;
 | 
						|
      if (evicted >= toEvict) break;
 | 
						|
    }
 | 
						|
 | 
						|
    this.stats.evictions += evicted;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get value from cache
 | 
						|
   * Returns { hit: boolean, value: any }
 | 
						|
   */
 | 
						|
  get(key) {
 | 
						|
    const entry = this.store.get(key);
 | 
						|
    
 | 
						|
    if (!entry) {
 | 
						|
      this.stats.misses++;
 | 
						|
      return { hit: false, value: undefined };
 | 
						|
    }
 | 
						|
 | 
						|
    // Check expiration
 | 
						|
    if (entry.exp < this._now()) {
 | 
						|
      this.store.delete(key);
 | 
						|
      this.stats.misses++;
 | 
						|
      return { hit: false, value: undefined };
 | 
						|
    }
 | 
						|
 | 
						|
    // Touch for pseudo-LRU: re-insert at end
 | 
						|
    this.store.delete(key);
 | 
						|
    this.store.set(key, entry);
 | 
						|
    this.stats.hits++;
 | 
						|
    return { hit: true, value: entry.value };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set value in cache with optional TTL override
 | 
						|
   */
 | 
						|
  set(key, value, { ttlMs = this.ttlMs } = {}) {
 | 
						|
    const exp = this._now() + ttlMs;
 | 
						|
    this.store.set(key, { value, exp });
 | 
						|
    this.stats.sets++;
 | 
						|
    this._evictIfNeeded();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Delete specific key
 | 
						|
   */
 | 
						|
  delete(key) {
 | 
						|
    this.store.delete(key);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Clear entire cache
 | 
						|
   */
 | 
						|
  clear() {
 | 
						|
    this.store.clear();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Read-through helper: get from cache or call producer
 | 
						|
   * Returns { value, hit: boolean }
 | 
						|
   */
 | 
						|
  async remember(key, producer, { ttlMs = this.ttlMs } = {}) {
 | 
						|
    const { hit, value } = this.get(key);
 | 
						|
    if (hit) return { value, hit: true };
 | 
						|
 | 
						|
    const fresh = await producer();
 | 
						|
    this.set(key, fresh, { ttlMs });
 | 
						|
    return { value: fresh, hit: false };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get cache statistics
 | 
						|
   */
 | 
						|
  getStats() {
 | 
						|
    const total = this.stats.hits + this.stats.misses;
 | 
						|
    const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
 | 
						|
 | 
						|
    return {
 | 
						|
      size: this.store.size,
 | 
						|
      maxItems: this.maxItems,
 | 
						|
      ttlMs: this.ttlMs,
 | 
						|
      hits: this.stats.hits,
 | 
						|
      misses: this.stats.misses,
 | 
						|
      hitRate: Math.round(hitRate * 10) / 10,
 | 
						|
      evictions: this.stats.evictions,
 | 
						|
      sets: this.stats.sets
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Reset statistics
 | 
						|
   */
 | 
						|
  resetStats() {
 | 
						|
    this.stats = { hits: 0, misses: 0, evictions: 0, sets: 0 };
 | 
						|
  }
 | 
						|
}
 |