agmission/Development/server/helpers/redis_cache.js

286 lines
8.6 KiB
JavaScript

'use strict';
/**
* Redis Cache Helper for Partner Authentication
* Provides a distributed cache that can be shared across different processes
*/
const Redis = require('ioredis');
const env = require('./env');
const logger = require('./logger');
const pino = logger.child('redis_cache');
class RedisCache {
constructor() {
this.redis = null;
this.isConnected = false;
this.fallbackCache = new Map(); // In-memory fallback
this.initialize();
}
initialize() {
try {
this.redis = new Redis({
host: env.REDIS_HOST || 'localhost',
port: env.REDIS_PORT || 6379,
password: env.REDIS_PWD,
retryDelayOnFailover: 100,
enableReadyCheck: false,
lazyConnect: true,
maxRetriesPerRequest: 3
});
this.redis.on('connect', () => {
this.isConnected = true;
pino.info('Redis cache connected');
});
this.redis.on('error', (err) => {
this.isConnected = false;
pino.error({ err }, 'Redis cache error');
});
this.redis.on('close', () => {
this.isConnected = false;
pino.warn('Redis cache connection closed');
});
} catch (error) {
pino.error({ err: error }, 'Failed to initialize Redis cache');
}
}
/**
* Generate cache key for partner authentication
* @param {string} partnerCode - Partner code (e.g., 'SATLOC')
* @param {string} customerId - Customer ID
* @returns {string} Cache key
*/
getAuthCacheKey(partnerCode, customerId) {
return `partner:auth:${partnerCode}:${customerId}`;
}
/**
* Set authentication data in cache with expiration
* @param {string} partnerCode - Partner code
* @param {string} customerId - Customer ID
* @param {object} authData - Authentication data to cache
* @param {number} ttlSeconds - Time to live in seconds (default: 1 hour)
* @returns {Promise<boolean>} Success status
*/
async setAuth(partnerCode, customerId, authData, ttlSeconds = 3600) {
const key = this.getAuthCacheKey(partnerCode, customerId);
const dataToCache = {
...authData,
cachedAt: Date.now(),
expiresAt: Date.now() + (ttlSeconds * 1000)
};
if (this.isConnected) {
try {
const serializedData = JSON.stringify(dataToCache);
await this.redis.setex(key, ttlSeconds, serializedData);
pino.debug(`Cached auth data in Redis for ${partnerCode}:${customerId}`);
return true;
} catch (error) {
pino.error({ err: error }, 'Failed to cache auth data in Redis');
}
}
// Fallback to in-memory cache
this.fallbackCache.set(key, dataToCache);
pino.debug(`Cached auth data in memory for ${partnerCode}:${customerId}`);
return true;
}
/**
* Get authentication data from cache
* @param {string} partnerCode - Partner code
* @param {string} customerId - Customer ID
* @returns {Promise<object|null>} Cached auth data or null
*/
async getAuth(partnerCode, customerId) {
const key = this.getAuthCacheKey(partnerCode, customerId);
if (this.isConnected) {
try {
const cachedData = await this.redis.get(key);
if (cachedData) {
const authData = JSON.parse(cachedData);
pino.debug(`Retrieved cached auth data from Redis for ${partnerCode}:${customerId}`);
return authData;
}
} catch (error) {
pino.error({ err: error }, 'Failed to retrieve cached auth data from Redis');
}
}
// Fallback to in-memory cache
const memoryData = this.fallbackCache.get(key);
if (memoryData) {
// Check if in-memory data is expired
if (memoryData.expiresAt && memoryData.expiresAt > Date.now()) {
pino.debug(`Retrieved cached auth data from memory for ${partnerCode}:${customerId}`);
return memoryData;
} else {
// Remove expired data
this.fallbackCache.delete(key);
}
}
return null;
}
/**
* Delete authentication data from cache
* @param {string} partnerCode - Partner code
* @param {string} customerId - Customer ID (optional, if not provided clears all for partner)
* @returns {Promise<boolean>} Success status
*/
async deleteAuth(partnerCode, customerId = null) {
let deleted = false;
if (this.isConnected) {
try {
if (customerId) {
const key = this.getAuthCacheKey(partnerCode, customerId);
await this.redis.del(key);
pino.debug(`Deleted cached auth data from Redis for ${partnerCode}:${customerId}`);
} else {
// Delete all auth data for the partner
const pattern = this.getAuthCacheKey(partnerCode, '*');
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
pino.debug(`Deleted ${keys.length} cached auth entries from Redis for ${partnerCode}`);
}
}
deleted = true;
} catch (error) {
pino.error({ err: error }, 'Failed to delete cached auth data from Redis');
}
}
// Also clean from fallback cache
if (customerId) {
const key = this.getAuthCacheKey(partnerCode, customerId);
this.fallbackCache.delete(key);
} else {
// Delete all entries for the partner from memory
const keysToDelete = [];
for (const key of this.fallbackCache.keys()) {
if (key.startsWith(`partner:auth:${partnerCode}:`)) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(key => this.fallbackCache.delete(key));
pino.debug(`Deleted ${keysToDelete.length} cached auth entries from memory for ${partnerCode}`);
}
return true;
}
/**
* Check if auth data is still valid based on expiration time
* @param {object} authData - Cached auth data
* @param {number} healthCheckInterval - Health check interval in ms
* @returns {boolean} Whether auth data is still valid
*/
isAuthValid(authData, healthCheckInterval = 30000) {
if (!authData) return false;
const now = Date.now();
return authData.expiresAt > now &&
authData.lastHealthCheck &&
(now - authData.lastHealthCheck) < healthCheckInterval;
}
/**
* Generic get — retrieves and JSON-parses a value from cache.
* Falls back to in-memory cache when Redis is unavailable.
* @param {string} key
* @returns {Promise<any|null>}
*/
async get(key) {
if (this.isConnected) {
try {
const raw = await this.redis.get(key);
if (raw) return JSON.parse(raw);
} catch (err) {
pino.error({ err }, `Redis get failed for key ${key}`);
}
}
const mem = this.fallbackCache.get(key);
if (mem) {
if (!mem._expiresAt || mem._expiresAt > Date.now()) return mem._value;
this.fallbackCache.delete(key);
}
return null;
}
/**
* Generic set — JSON-serialises and stores a value in cache.
* Falls back to in-memory cache when Redis is unavailable.
* @param {string} key
* @param {any} value
* @param {number} ttlSeconds - Default 60 s
* @returns {Promise<boolean>}
*/
async set(key, value, ttlSeconds = 60) {
if (this.isConnected) {
try {
await this.redis.setex(key, ttlSeconds, JSON.stringify(value));
return true;
} catch (err) {
pino.error({ err }, `Redis set failed for key ${key}`);
}
}
this.fallbackCache.set(key, { _value: value, _expiresAt: Date.now() + ttlSeconds * 1000 });
return true;
}
/**
* Delete all cache keys whose names start with the given prefix pattern
* (supports a trailing wildcard, e.g. 'jobs:list:abc123:*').
* @param {string} pattern
* @returns {Promise<number>} number of entries removed
*/
async delByPattern(pattern) {
let count = 0;
if (this.isConnected) {
try {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
count += keys.length;
}
} catch (err) {
pino.error({ err }, `Redis delByPattern failed for pattern ${pattern}`);
}
}
// Also purge the in-memory fallback
const prefix = pattern.endsWith('*') ? pattern.slice(0, -1) : pattern;
for (const key of this.fallbackCache.keys()) {
if (key.startsWith(prefix)) {
this.fallbackCache.delete(key);
count++;
}
}
return count;
}
/**
* Close Redis connection
*/
async disconnect() {
if (this.redis) {
await this.redis.disconnect();
this.isConnected = false;
pino.info('Redis cache disconnected');
}
}
}
// Export singleton instance
module.exports = new RedisCache();