/** * Simple in-memory cache with TTL support * * Usage: * ```typescript * const cache = new Cache(5 * 60 * 1000); // 5 minutes TTL * * const users = await cache.getOrSet('all-users', async () => { * return await db.getAllUsers(); * }); * ``` */ import log from "../logger"; export interface CacheEntry { value: T; expiresAt: number; createdAt: number; } export interface CacheStats { hits: number; misses: number; size: number; hitRate: number; } /** * Simple in-memory cache with TTL (Time To Live) */ export class Cache { private store: Map> = new Map(); private hits = 0; private misses = 0; private defaultTtl: number; /** * Create a new cache instance * * @param defaultTtl - Default time-to-live in milliseconds (default: 5 minutes) */ constructor(defaultTtl: number = 5 * 60 * 1000) { this.defaultTtl = defaultTtl; } /** * Get a value from cache * * @param key - Cache key * @returns Cached value or null if expired/not found */ get(key: string): T | null { const entry = this.store.get(key); if (!entry) { this.misses++; return null; } // Check if expired if (Date.now() > entry.expiresAt) { this.store.delete(key); this.misses++; log.debug("Cache expired", { key }); return null; } this.hits++; log.debug("Cache hit", { key }); return entry.value; } /** * Set a value in cache * * @param key - Cache key * @param value - Value to cache * @param ttl - Time-to-live in milliseconds (optional, uses default if not provided) */ set(key: string, value: T, ttl?: number): void { const expiresAt = Date.now() + (ttl ?? this.defaultTtl); this.store.set(key, { value, expiresAt, createdAt: Date.now(), }); log.debug("Cache set", { key, ttl: ttl ?? this.defaultTtl }); } /** * Get a value from cache, or compute and cache it if not found * * @param key - Cache key * @param fn - Function to compute value if not in cache * @param ttl - Time-to-live in milliseconds (optional) * @returns Cached or computed value */ async getOrSet(key: string, fn: () => Promise, ttl?: number): Promise { const cached = this.get(key); if (cached !== null) { return cached; } log.debug("Cache miss, computing value", { key }); const value = await fn(); this.set(key, value, ttl); return value; } /** * Delete a value from cache * * @param key - Cache key * @returns True if deleted, false if not found */ delete(key: string): boolean { const deleted = this.store.delete(key); log.debug("Cache delete", { key, deleted }); return deleted; } /** * Clear all entries from cache */ clear(): void { const size = this.store.size; this.store.clear(); this.hits = 0; this.misses = 0; log.info("Cache cleared", { entriesCleared: size }); } /** * Get cache statistics */ getStats(): CacheStats { const total = this.hits + this.misses; return { hits: this.hits, misses: this.misses, size: this.store.size, hitRate: total > 0 ? this.hits / total : 0, }; } /** * Remove expired entries from cache */ cleanup(): number { const now = Date.now(); let removed = 0; for (const [key, entry] of Array.from(this.store.entries())) { if (now > entry.expiresAt) { this.store.delete(key); removed++; } } if (removed > 0) { log.info("Cache cleanup completed", { entriesRemoved: removed }); } return removed; } /** * Check if a key exists in cache (and is not expired) */ has(key: string): boolean { const entry = this.store.get(key); if (!entry) return false; if (Date.now() > entry.expiresAt) { this.store.delete(key); return false; } return true; } /** * Get all keys in cache */ keys(): string[] { return Array.from(this.store.keys()); } /** * Get cache size (number of entries) */ size(): number { return this.store.size; } } /** * Global cache instances for common use cases */ // User data cache (5 minutes) export const userCache = new Cache(5 * 60 * 1000); // Fitness goals cache (2 minutes) export const fitnessGoalsCache = new Cache(2 * 60 * 1000); // Recommendations cache (3 minutes) export const recommendationsCache = new Cache(3 * 60 * 1000); // Dashboard stats cache (1 minute - frequently changing data) export const dashboardStatsCache = new Cache(1 * 60 * 1000); /** * Setup automatic cache cleanup (run every 5 minutes) */ export function setupCacheCleanup(): NodeJS.Timeout { const interval = setInterval( () => { log.debug("Running scheduled cache cleanup"); userCache.cleanup(); fitnessGoalsCache.cleanup(); recommendationsCache.cleanup(); dashboardStatsCache.cleanup(); }, 5 * 60 * 1000, ); // Every 5 minutes return interval; } /** * Clear all caches */ export function clearAllCaches(): void { userCache.clear(); fitnessGoalsCache.clear(); recommendationsCache.clear(); dashboardStatsCache.clear(); log.info("All caches cleared"); } /** * Get stats for all caches */ export function getAllCacheStats(): Record { return { users: userCache.getStats(), fitnessGoals: fitnessGoalsCache.getStats(), recommendations: recommendationsCache.getStats(), dashboardStats: dashboardStatsCache.getStats(), }; }