import path from 'path'; import { fileURLToPath } from 'url'; import Cache from '../libs/cache.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ClientsCacheFile = path.join(__dirname, '../data/clients.cache'); const CLIENT_HEADER_FIELDS = [ 'X-Device', 'X-Version', 'X-Data-Type', 'X-Market-Type', 'X-Group-Sequence', 'X-Platform', ]; const CLIENT_KEY_FIELDS = [ 'X-Data-Type', 'X-Market-Type', 'X-Group-Sequence', 'X-Platform', ]; const CLIENT_FIELD_NAMES = { 'X-Data-Type': 'dataType', 'X-Device': 'device', 'X-Group-Sequence': 'groupSequence', 'X-Market-Type': 'marketType', 'X-Platform': 'platform', 'X-Version': 'version', }; const CLIENTS = { Items: {}, }; let saveTimer = null; const normalizeHeaderValue = (value) => { if (Array.isArray(value)) { return value.join(','); } return value == null ? '' : String(value).trim(); } const getRequestHeader = (req, field) => { return normalizeHeaderValue(req.get?.(field)); } const getClientHeaders = (req) => { return CLIENT_HEADER_FIELDS.reduce((headers, field) => { headers[field] = getRequestHeader(req, field); return headers; }, {}); } const formatClientFields = (headers) => { return CLIENT_HEADER_FIELDS.reduce((clientFields, field) => { clientFields[CLIENT_FIELD_NAMES[field]] = headers[field]; return clientFields; }, {}); } const getClientKey = (headers) => { return CLIENT_KEY_FIELDS .map(field => `${field}:${headers[field] ?? ''}`) .join('|'); } const scheduleSaveClientsToCache = () => { if (saveTimer) { clearTimeout(saveTimer); } saveTimer = setTimeout(saveClientsToCache, 1000); } const recordRequest = (req) => { const route = req.path; const headers = getClientHeaders(req); const clientFields = formatClientFields(headers); const key = getClientKey(headers); const now = Date.now(); const current = CLIENTS.Items[key] ?? {}; CLIENTS.Items[key] = { key, ...clientFields, deviceId: current.deviceId, route, firstRequestTime: current.firstRequestTime ?? now, lastRequestTime: now, requestCount: (current.requestCount ?? 0) + 1, }; scheduleSaveClientsToCache(); return CLIENTS.Items[key]; } const getClients = () => { return Object.values(CLIENTS.Items) .sort((a, b) => (b.lastRequestTime ?? 0) - (a.lastRequestTime ?? 0)); } const updateClient = ({ key, deviceId } = {}) => { if (!key || !CLIENTS.Items[key]) { return Promise.reject(new Error('CLIENT_NOT_FOUND')); } if (deviceId === undefined || deviceId === null || deviceId === '') { delete CLIENTS.Items[key].deviceId; saveClientsToCache(); return Promise.resolve(CLIENTS.Items[key]); } const parsedDeviceId = Number(deviceId); if (!Number.isInteger(parsedDeviceId)) { return Promise.reject(new Error('DEVICE_ID_INVALID')); } CLIENTS.Items[key] = { ...CLIENTS.Items[key], deviceId: parsedDeviceId, }; saveClientsToCache(); return Promise.resolve(CLIENTS.Items[key]); } const deleteClient = (key) => { if (!key || !CLIENTS.Items[key]) { return Promise.reject(new Error('CLIENT_NOT_FOUND')); } delete CLIENTS.Items[key]; saveClientsToCache(); return Promise.resolve(); } function saveClientsToCache() { if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } Cache.setData(ClientsCacheFile, CLIENTS); } function loadClientsFromCache() { const cachedClients = Cache.getData(ClientsCacheFile, true); if (!cachedClients?.Items) { return; } CLIENTS.Items = cachedClients.Items; } loadClientsFromCache(); process.on('exit', saveClientsToCache); process.on('SIGINT', () => { process.exit(0); }); process.on('SIGTERM', () => { process.exit(0); }); process.on('SIGUSR2', () => { process.exit(0); }); const Clients = { recordRequest, getClients, updateClient, deleteClient, }; export { recordRequest, getClients, updateClient, deleteClient }; export default Clients;