UNPKG

react-native-avo-inspector

Version:

[![npm version](https://badge.fury.io/js/react-native-avo-inspector.svg)](https://badge.fury.io/js/react-native-avo-inspector)

166 lines (165 loc) 5.53 kB
/** * EventSpecCache implements a dual-condition cache with LRU eviction. * * Cache Policy: * - Entries expire after 5 minutes OR 50 cache hits, whichever comes first * - When 50 cache hits occur globally, the oldest cached entry is evicted * - Each cache entry tracks: spec, timestamp, and hit count * * Cache Key Format: ${apiKey}:${streamId}:${eventName} * * Note: Event count only increments on cache hits, not on every tracked event. * This ensures the cache evicts based on actual usage, not overall tracking volume. */ export class EventSpecCache { constructor(shouldLog = false) { /** Time-to-live in milliseconds (1 minute) */ this.TTL_MS = 60 * 1000; /** Maximum cache hit count before rotating cache (50 hits) */ this.MAX_EVENT_COUNT = 50; /** Global cache hit counter to track when to rotate cache */ this.globalEventCount = 0; this.cache = new Map(); this.shouldLog = shouldLog; } /** * Generates a cache key from the provided parameters. */ generateKey(apiKey, streamId, eventName) { return `${apiKey}:${streamId}:${eventName}`; } /** * Checks if a non-expired cache entry exists for the given key. * Returns true even for cached null specs (empty backend responses). * Use this to distinguish between "cache miss" and "cached empty response". */ contains(apiKey, streamId, eventName) { const key = this.generateKey(apiKey, streamId, eventName); const entry = this.cache.get(key); if (!entry) { return false; } if (this.shouldEvict(entry)) { this.cache.delete(key); return false; } return true; } /** * Retrieves an event spec response from the cache if it exists and is valid. * Returns null if the entry is missing, expired, has exceeded event count, * or if the cached spec itself is null (empty backend response). * * On cache hit, increments the hit count for this entry and the global counter. */ get(apiKey, streamId, eventName) { const key = this.generateKey(apiKey, streamId, eventName); const entry = this.cache.get(key); if (!entry) { return null; } // Check if entry has expired if (this.shouldEvict(entry)) { this.cache.delete(key); return null; } if (this.shouldLog) { console.log(`[Avo Inspector] Cache hit for key: ${key}`); } // Update lastAccessed for LRU (Least Recently Used) eviction entry.lastAccessed = Date.now(); // Increment hit count for this entry entry.eventCount++; this.globalEventCount++; // Check if we need to evict the oldest entry if (this.globalEventCount >= this.MAX_EVENT_COUNT) { this.evictOldest(); this.globalEventCount = 0; } return entry.spec; } /** * Stores an event spec response in the cache. * Pass null to cache an empty response (event not found on backend). */ set(apiKey, streamId, eventName, spec) { const key = this.generateKey(apiKey, streamId, eventName); const now = Date.now(); const entry = { spec, timestamp: now, lastAccessed: now, eventCount: 0 }; this.cache.set(key, entry); } /** * Determines if a cache entry should be evicted based on: * - Age (older than 5 minutes) * - Hit count (50 or more cache hits) */ shouldEvict(entry) { const age = Date.now() - entry.timestamp; const ageExpired = age > this.TTL_MS; const countExpired = entry.eventCount >= this.MAX_EVENT_COUNT; return ageExpired || countExpired; } /** * Evicts the least recently used cache entry based on lastAccessed timestamp. * This implements the LRU (Least Recently Used) eviction policy. */ evictOldest() { if (this.cache.size === 0) { return; } let lruKey = null; let oldestAccessTime = Infinity; // Find the entry with the oldest lastAccessed time (least recently used) this.cache.forEach((entry, key) => { if (entry.lastAccessed < oldestAccessTime) { oldestAccessTime = entry.lastAccessed; lruKey = key; } }); // Remove the least recently used entry if (lruKey !== null) { this.cache.delete(lruKey); } } /** * Clears all cached entries. Useful for testing. */ clear() { this.cache.clear(); this.globalEventCount = 0; if (this.shouldLog) { console.log("[Avo Inspector] Cache cleared"); } } /** * Returns the current size of the cache. */ size() { return this.cache.size; } /** * Returns cache statistics for debugging. */ getStats() { const entries = []; const now = Date.now(); this.cache.forEach((entry, key) => { entries.push({ key, age: now - entry.timestamp, lastAccessedAgo: now - entry.lastAccessed, eventCount: entry.eventCount }); }); return { size: this.cache.size, globalEventCount: this.globalEventCount, entries }; } }