From cce520a00e44a0afd074e309f9802e4759ee2594 Mon Sep 17 00:00:00 2001 From: Joerg Date: Thu, 22 Jan 2026 10:22:00 +0100 Subject: [PATCH] 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 --- src/backend/services/awards.service.js | 29 +++++++++++++++----------- src/backend/services/cache.service.js | 26 ----------------------- src/backend/services/dcl.service.js | 27 +----------------------- src/backend/services/lotw.service.js | 21 ++++--------------- src/backend/utils/sync-helpers.js | 23 ++++++++++++++++++++ 5 files changed, 45 insertions(+), 81 deletions(-) create mode 100644 src/backend/utils/sync-helpers.js diff --git a/src/backend/services/awards.service.js b/src/backend/services/awards.service.js index 0168922..baa36fe 100644 --- a/src/backend/services/awards.service.js +++ b/src/backend/services/awards.service.js @@ -1,7 +1,7 @@ import { db, logger } from '../config.js'; import { qsos } from '../db/schema/index.js'; import { eq, and, or, desc, sql } from 'drizzle-orm'; -import { readFileSync } from 'fs'; +import { readFileSync, readdirSync } from 'fs'; import { join } from 'path'; import { getCachedAwardProgress, setCachedAwardProgress } from './cache.service.js'; @@ -13,23 +13,25 @@ import { getCachedAwardProgress, setCachedAwardProgress } from './cache.service. // Load award definitions from files const AWARD_DEFINITIONS_DIR = join(process.cwd(), 'award-definitions'); +// In-memory cache for award definitions (static, never changes at runtime) +let cachedAwardDefinitions = null; + /** - * Load all award definitions + * Load all award definitions (cached in memory) */ function loadAwardDefinitions() { + // Return cached definitions if available + if (cachedAwardDefinitions) { + return cachedAwardDefinitions; + } + const definitions = []; try { - const files = [ - 'dxcc.json', - 'dxcc-sat.json', - 'was.json', - 'vucc-sat.json', - 'sat-rs44.json', - 'special-stations.json', - 'dld.json', - '73-on-73.json', - ]; + // Auto-discover all JSON files in the award-definitions directory + const files = readdirSync(AWARD_DEFINITIONS_DIR) + .filter(f => f.endsWith('.json')) + .sort(); for (const file of files) { try { @@ -45,6 +47,9 @@ function loadAwardDefinitions() { logger.error('Error loading award definitions', { error: error.message }); } + // Cache the definitions for future calls + cachedAwardDefinitions = definitions; + return definitions; } diff --git a/src/backend/services/cache.service.js b/src/backend/services/cache.service.js index 5844181..d65960a 100644 --- a/src/backend/services/cache.service.js +++ b/src/backend/services/cache.service.js @@ -86,32 +86,6 @@ export function clearAllCache() { return size; } -/** - * Get cache statistics (for monitoring/debugging) - * @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++; - } - } - - return { - total: awardCache.size, - valid, - expired, - ttl: CACHE_TTL - }; -} - /** * Clean up expired cache entries (maintenance function) * Can be called periodically to free memory diff --git a/src/backend/services/dcl.service.js b/src/backend/services/dcl.service.js index dd9916c..320baba 100644 --- a/src/backend/services/dcl.service.js +++ b/src/backend/services/dcl.service.js @@ -4,6 +4,7 @@ import { max, sql, eq, and, desc } from 'drizzle-orm'; import { updateJobProgress } from './job-queue.service.js'; import { parseDCLResponse, normalizeBand, normalizeMode } from '../utils/adif-parser.js'; import { invalidateUserCache, invalidateStatsCache } from './cache.service.js'; +import { yieldToEventLoop, getQSOKey } from '../utils/sync-helpers.js'; /** * DCL (DARC Community Logbook) Service @@ -122,17 +123,6 @@ export async function fetchQSOsFromDCL(dclApiKey, sinceDate = null) { } } -/** - * Parse DCL API response from JSON - * Can be used for testing with example payloads - * - * @param {Object} jsonResponse - JSON response in DCL format - * @returns {Array} Array of parsed QSO records - */ -export function parseDCLJSONResponse(jsonResponse) { - return parseDCLResponse(jsonResponse); -} - /** * Convert DCL ADIF QSO to database format * @param {Object} adifQSO - Parsed ADIF QSO record @@ -169,21 +159,6 @@ function convertQSODatabaseFormat(adifQSO, userId) { }; } -/** - * Yield to event loop to allow other requests to be processed - * This prevents blocking the server during long-running sync operations - */ -function yieldToEventLoop() { - return new Promise(resolve => setImmediate(resolve)); -} - -/** - * Get QSO key for duplicate detection - */ -function getQSOKey(qso) { - return `${qso.callsign}|${qso.qsoDate}|${qso.timeOn}|${qso.band}|${qso.mode}`; -} - /** * Sync QSOs from DCL to database (optimized with batch operations) * Updates existing QSOs with DCL confirmation data diff --git a/src/backend/services/lotw.service.js b/src/backend/services/lotw.service.js index 35eadd3..56a5f5e 100644 --- a/src/backend/services/lotw.service.js +++ b/src/backend/services/lotw.service.js @@ -4,7 +4,8 @@ import { max, sql, eq, and, or, desc, like } from 'drizzle-orm'; import { updateJobProgress } from './job-queue.service.js'; import { parseADIF, normalizeBand, normalizeMode } from '../utils/adif-parser.js'; import { invalidateUserCache, getCachedStats, setCachedStats, invalidateStatsCache } from './cache.service.js'; -import { trackQueryPerformance, getPerformanceSummary, resetPerformanceMetrics } from './performance.service.js'; +import { trackQueryPerformance } from './performance.service.js'; +import { yieldToEventLoop, getQSOKey } from '../utils/sync-helpers.js'; /** * LoTW (Logbook of the World) Service @@ -81,6 +82,7 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); * Fetch QSOs from LoTW with retry support */ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) { + const startTime = Date.now(); const url = 'https://lotw.arrl.org/lotwuser/lotwreport.adi'; const params = new URLSearchParams({ @@ -176,7 +178,7 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) { } } - const totalTime = Math.round((Date.now() - Date.now()) / 1000); + const totalTime = Math.round((Date.now() - startTime) / 1000); return { error: `LoTW sync failed: Report not ready after ${MAX_RETRIES} attempts (${totalTime}s). LoTW may be experiencing high load. Please try again later.` }; @@ -210,21 +212,6 @@ function convertQSODatabaseFormat(adifQSO, userId) { }; } -/** - * Yield to event loop to allow other requests to be processed - * This prevents blocking the server during long-running sync operations - */ -function yieldToEventLoop() { - return new Promise(resolve => setImmediate(resolve)); -} - -/** - * Get QSO key for duplicate detection - */ -function getQSOKey(qso) { - return `${qso.callsign}|${qso.qsoDate}|${qso.timeOn}|${qso.band}|${qso.mode}`; -} - /** * Sync QSOs from LoTW to database (optimized with batch operations) * @param {number} userId - User ID diff --git a/src/backend/utils/sync-helpers.js b/src/backend/utils/sync-helpers.js new file mode 100644 index 0000000..1fb5df0 --- /dev/null +++ b/src/backend/utils/sync-helpers.js @@ -0,0 +1,23 @@ +/** + * Sync Helper Utilities + * + * Shared utilities for LoTW and DCL sync operations + */ + +/** + * Yield to event loop to allow other requests to be processed + * This prevents blocking the server during long-running sync operations + * @returns {Promise} + */ +export function yieldToEventLoop() { + return new Promise(resolve => setImmediate(resolve)); +} + +/** + * Get QSO key for duplicate detection + * @param {object} qso - QSO object + * @returns {string} Unique key for the QSO + */ +export function getQSOKey(qso) { + return `${qso.callsign}|${qso.qsoDate}|${qso.timeOn}|${qso.band}|${qso.mode}`; +}