- 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>
250 lines
5.5 KiB
JavaScript
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++;
|
|
}
|