import Redis from 'ioredis'; import { TimeManager } from '../utils/time.utils.js'; import dotenv from 'dotenv'; import path from 'path'; import { fileURLToPath } from 'url'; // Get directory name in ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load environment variables again (redundant but safe) const envPath = path.resolve(__dirname, '../.env'); console.log('[RedisService] Loading .env file from:', envPath); dotenv.config({ path: envPath }); export class RedisService { constructor() { this.timeManager = new TimeManager(); this.DEFAULT_TTL = 5 * 60; // 5 minutes default TTL this.isConnected = false; this._initializeRedis(); } _initializeRedis() { try { // Debug: Print all environment variables we're looking for console.log('[RedisService] Environment variables state:', { REDIS_HOST: process.env.REDIS_HOST ? '(set)' : '(not set)', REDIS_PORT: process.env.REDIS_PORT ? '(set)' : '(not set)', REDIS_USERNAME: process.env.REDIS_USERNAME ? '(set)' : '(not set)', REDIS_PASSWORD: process.env.REDIS_PASSWORD ? '(set)' : '(not set)', }); // Log Redis configuration (without password) const host = process.env.REDIS_HOST || 'localhost'; const port = parseInt(process.env.REDIS_PORT) || 6379; const username = process.env.REDIS_USERNAME || 'default'; const password = process.env.REDIS_PASSWORD; console.log('[RedisService] Initializing Redis with config:', { host, port, username, hasPassword: !!password }); const config = { host, port, username, retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, maxRetriesPerRequest: 3, enableReadyCheck: true, connectTimeout: 10000, showFriendlyErrorStack: true, retryUnfulfilled: true, maxRetryAttempts: 5 }; // Only add password if it exists if (password) { console.log('[RedisService] Adding password to config'); config.password = password; } else { console.warn('[RedisService] No Redis password found in environment variables!'); } this.client = new Redis(config); // Handle connection events this.client.on('connect', () => { console.log('[RedisService] Connected to Redis'); this.isConnected = true; }); this.client.on('ready', () => { console.log('[RedisService] Redis is ready'); this.isConnected = true; }); this.client.on('error', (err) => { console.error('[RedisService] Redis error:', err); this.isConnected = false; // Log more details about the error if (err.code === 'WRONGPASS') { console.error('[RedisService] Authentication failed. Please check your Redis password.'); } }); this.client.on('close', () => { console.log('[RedisService] Redis connection closed'); this.isConnected = false; }); this.client.on('reconnecting', (params) => { console.log('[RedisService] Reconnecting to Redis:', params); }); } catch (error) { console.error('[RedisService] Error initializing Redis:', error); this.isConnected = false; } } async get(key) { if (!this.isConnected) { return null; } try { const data = await this.client.get(key); return data ? JSON.parse(data) : null; } catch (error) { console.error('[RedisService] Error getting data:', error); return null; } } async set(key, data, ttl = this.DEFAULT_TTL) { if (!this.isConnected) { return; } try { await this.client.setex(key, ttl, JSON.stringify(data)); } catch (error) { console.error('[RedisService] Error setting data:', error); } } // Helper to generate cache keys _getCacheKey(type, params = {}) { const { timeRange, startDate, endDate, metricId, metric, daily } = params; let key = `klaviyo:${type}`; if (type === 'stats:details') { key += `:${metric}${daily ? ':daily' : ''}`; } if (timeRange) { key += `:${timeRange}${metricId ? `:${metricId}` : ''}`; } else if (startDate && endDate) { key += `:custom:${startDate}:${endDate}${metricId ? `:${metricId}` : ''}`; } return key; } // Get TTL based on time range _getTTL(timeRange) { const TTL_MAP = { 'today': 2 * 60, // 2 minutes 'yesterday': 30 * 60, // 30 minutes 'thisWeek': 5 * 60, // 5 minutes 'lastWeek': 60 * 60, // 1 hour 'thisMonth': 10 * 60, // 10 minutes 'lastMonth': 2 * 60 * 60, // 2 hours 'last7days': 5 * 60, // 5 minutes 'last30days': 15 * 60, // 15 minutes 'custom': 15 * 60 // 15 minutes }; return TTL_MAP[timeRange] || this.DEFAULT_TTL; } async getEventData(type, params) { if (!this.isConnected) { return null; } try { const baseKey = this._getCacheKey('events', params); const data = await this.get(`${baseKey}:${type}`); return data; } catch (error) { console.error('[RedisService] Error getting event data:', error); return null; } } async cacheEventData(type, params, data) { if (!this.isConnected) { return; } try { const ttl = this._getTTL(params.timeRange); const baseKey = this._getCacheKey('events', params); // Cache raw event data await this.set(`${baseKey}:${type}`, data, ttl); } catch (error) { console.error('[RedisService] Error caching event data:', error); } } async clearCache(params = {}) { if (!this.isConnected) { return; } try { const pattern = this._getCacheKey('events', params) + '*'; const keys = await this.client.keys(pattern); if (keys.length > 0) { await this.client.del(...keys); } } catch (error) { console.error('[RedisService] Error clearing cache:', error); } } }