/** * Cache Service for Award Progress * * Provides in-memory caching for award progress calculations to avoid * expensive database aggregations on every request. * * Cache TTL: 5 minutes (balances freshness with performance) * * Usage: * - Check cache before calculating award progress * - Invalidate cache when QSOs are synced/updated * - Automatic expiry after TTL */ const awardCache = new Map(); const statsCache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes /** * Get cached award progress if available and not expired * @param {number} userId - User ID * @param {string} awardId - Award ID * @returns {object|null} Cached progress data or null if not found/expired */ export function getCachedAwardProgress(userId, awardId) { const key = `${userId}:${awardId}`; const cached = awardCache.get(key); if (!cached) { recordAwardCacheMiss(); return null; } // Check if cache has expired const age = Date.now() - cached.timestamp; if (age > CACHE_TTL) { awardCache.delete(key); recordAwardCacheMiss(); return null; } recordAwardCacheHit(); return cached.data; } /** * Set award progress in cache * @param {number} userId - User ID * @param {string} awardId - Award ID * @param {object} data - Award progress data to cache */ export function setCachedAwardProgress(userId, awardId, data) { const key = `${userId}:${awardId}`; awardCache.set(key, { data, timestamp: Date.now() }); } /** * Invalidate all cached awards for a specific user * Call this after syncing or updating QSOs * @param {number} userId - User ID */ export function invalidateUserCache(userId) { const prefix = `${userId}:`; let deleted = 0; for (const [key] of awardCache) { if (key.startsWith(prefix)) { awardCache.delete(key); deleted++; } } return deleted; } /** * Clear all cached awards (use sparingly) *主要用于测试或紧急情况 */ export function clearAllCache() { const size = awardCache.size; awardCache.clear(); return size; } /** * Clean up expired cache entries (maintenance function) * Can be called periodically to free memory * @returns {number} Number of entries cleaned up */ export function cleanupExpiredCache() { const now = Date.now(); let cleaned = 0; for (const [key, value] of awardCache) { const age = now - value.timestamp; if (age > CACHE_TTL) { awardCache.delete(key); cleaned++; } } for (const [key, value] of statsCache) { const age = now - value.timestamp; if (age > CACHE_TTL) { statsCache.delete(key); cleaned++; } } return cleaned; } /** * Get cached QSO statistics if available and not expired * @param {number} userId - User ID * @returns {object|null} Cached stats data or null if not found/expired */ export function getCachedStats(userId) { const key = `stats_${userId}`; const cached = statsCache.get(key); if (!cached) { recordStatsCacheMiss(); return null; } // Check if cache has expired const age = Date.now() - cached.timestamp; if (age > CACHE_TTL) { statsCache.delete(key); recordStatsCacheMiss(); return null; } recordStatsCacheHit(); return cached.data; } /** * Set QSO statistics in cache * @param {number} userId - User ID * @param {object} data - Statistics data to cache */ export function setCachedStats(userId, data) { const key = `stats_${userId}`; statsCache.set(key, { data, timestamp: Date.now() }); } /** * Invalidate cached QSO statistics for a specific user * Call this after syncing or updating QSOs * @param {number} userId - User ID * @returns {boolean} True if cache was invalidated */ export function invalidateStatsCache(userId) { const key = `stats_${userId}`; const deleted = statsCache.delete(key); return deleted; } /** * Get cache statistics including both award and stats caches * @returns {object} Cache stats */ export function getCacheStats() { const now = Date.now(); let expired = 0; let valid = 0; for (const [, value] of awardCache) { const age = now - value.timestamp; if (age > CACHE_TTL) { expired++; } else { valid++; } } for (const [, value] of statsCache) { const age = now - value.timestamp; if (age > CACHE_TTL) { expired++; } else { valid++; } } const totalRequests = awardCacheStats.hits + awardCacheStats.misses + statsCacheStats.hits + statsCacheStats.misses; const hitRate = totalRequests > 0 ? ((awardCacheStats.hits + statsCacheStats.hits) / totalRequests * 100).toFixed(2) + '%' : '0%'; return { total: awardCache.size + statsCache.size, valid, expired, ttl: CACHE_TTL, hitRate, awardCache: { size: awardCache.size, hits: awardCacheStats.hits, misses: awardCacheStats.misses }, statsCache: { size: statsCache.size, hits: statsCacheStats.hits, misses: statsCacheStats.misses } }; } /** * Cache statistics tracking */ const awardCacheStats = { hits: 0, misses: 0 }; const statsCacheStats = { hits: 0, misses: 0 }; /** * Record a cache hit for awards */ export function recordAwardCacheHit() { awardCacheStats.hits++; } /** * Record a cache miss for awards */ export function recordAwardCacheMiss() { awardCacheStats.misses++; } /** * Record a cache hit for stats */ export function recordStatsCacheHit() { statsCacheStats.hits++; } /** * Record a cache miss for stats */ export function recordStatsCacheMiss() { statsCacheStats.misses++; }