react-native-avo-inspector
Version:
[](https://badge.fury.io/js/react-native-avo-inspector)
166 lines (165 loc) • 5.53 kB
JavaScript
/**
* 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
};
}
}