import NodeCache from "node-cache"; import logger from "@server/logger"; // Create local cache with maxKeys limit to prevent memory leaks // With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient export const localCache = new NodeCache({ stdTTL: 3600, checkperiod: 120, maxKeys: 10000 }); // Log cache statistics periodically for monitoring setInterval(() => { const stats = localCache.getStats(); logger.debug( `Local cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%` ); }, 300000); // Every 5 minutes /** * Adaptive cache that uses Redis when available in multi-node environments, * otherwise falls back to local memory cache for single-node deployments. */ class AdaptiveCache { /** * Set a value in the cache * @param key - Cache key * @param value - Value to cache (will be JSON stringified for Redis) * @param ttl - Time to live in seconds (0 = no expiration) * @returns boolean indicating success */ async set(key: string, value: any, ttl?: number): Promise { const effectiveTtl = ttl === 0 ? undefined : ttl; // Use local cache as fallback or primary const success = localCache.set(key, value, effectiveTtl || 0); if (success) { logger.debug(`Set key in local cache: ${key}`); } return success; } /** * Get a value from the cache * @param key - Cache key * @returns The cached value or undefined if not found */ async get(key: string): Promise { // Use local cache as fallback or primary const value = localCache.get(key); if (value !== undefined) { logger.debug(`Cache hit in local cache: ${key}`); } else { logger.debug(`Cache miss in local cache: ${key}`); } return value; } /** * Delete a value from the cache * @param key - Cache key or array of keys * @returns Number of deleted entries */ async del(key: string | string[]): Promise { const keys = Array.isArray(key) ? key : [key]; let deletedCount = 0; // Use local cache as fallback or primary for (const k of keys) { const success = localCache.del(k); if (success > 0) { deletedCount++; logger.debug(`Deleted key from local cache: ${k}`); } } return deletedCount; } /** * Check if a key exists in the cache * @param key - Cache key * @returns boolean indicating if key exists */ async has(key: string): Promise { // Use local cache as fallback or primary return localCache.has(key); } /** * Get multiple values from the cache * @param keys - Array of cache keys * @returns Array of values (undefined for missing keys) */ async mget(keys: string[]): Promise<(T | undefined)[]> { // Use local cache as fallback or primary return keys.map((key) => localCache.get(key)); } /** * Flush all keys from the cache */ async flushAll(): Promise { localCache.flushAll(); logger.debug("Flushed local cache"); } /** * Get cache statistics * Note: Only returns local cache stats, Redis stats are not included */ getStats() { return localCache.getStats(); } /** * Get the current cache backend being used * @returns "redis" if Redis is available and healthy, "local" otherwise */ getCurrentBackend(): "redis" | "local" { return "local"; } /** * Take a key from the cache and delete it * @param key - Cache key * @returns The value or undefined if not found */ async take(key: string): Promise { const value = await this.get(key); if (value !== undefined) { await this.del(key); } return value; } /** * Get TTL (time to live) for a key * @param key - Cache key * @returns TTL in seconds, 0 if no expiration, -1 if key doesn't exist */ getTtl(key: string): number { const ttl = localCache.getTtl(key); if (ttl === undefined) { return -1; } return Math.max(0, Math.floor((ttl - Date.now()) / 1000)); } /** * Get all keys from the cache * Note: Only returns local cache keys, Redis keys are not included */ keys(): string[] { return localCache.keys(); } } // Export singleton instance export const cache = new AdaptiveCache(); export default cache;