Files
award/src/backend/services/cache.service.js
Joerg cce520a00e chore: code cleanup - remove duplicates and add caching
- Delete duplicate getCacheStats() function in cache.service.js
- Fix date calculation bug in lotw.service.js (was Date.now()-Date.now())
- Extract duplicate helper functions (yieldToEventLoop, getQSOKey) to sync-helpers.js
- Cache award definitions in memory to avoid repeated file I/O
- Delete unused parseDCLJSONResponse() function
- Remove unused imports (getPerformanceSummary, resetPerformanceMetrics)
- Auto-discover award JSON files instead of hardcoded list

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 10:22:00 +01:00

250 lines
5.5 KiB
JavaScript

/**
* 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++;
}