// Klaviyo cache wrapper. Was a self-instantiating ioredis client per service in // the standalone klaviyo-server; now accepts an injected client so the merged // dashboard-server shares one connection across all vendors (Phase 4). // // Public surface kept identical to the original so the ~3K LOC of klaviyo // service code (events/campaigns/reporting) needs no other changes: // - get(key) // - set(key, data, ttl) // - _getCacheKey(type, params) // - _getTTL(timeRange) // - getEventData(type, params) / cacheEventData(type, params, data) // - clearCache(params) // // Reads short-circuit to null when the client isn't ready; writes are no-ops. // Same "Redis hiccup → fall through to upstream" behavior as before. import { TimeManager } from '../../utils/time.utils.js'; export class RedisService { constructor(redis) { if (!redis) { throw new Error('RedisService requires an ioredis client (Phase 4: injected, no longer self-constructed)'); } this.client = redis; this.timeManager = new TimeManager(); this.DEFAULT_TTL = 5 * 60; } get isConnected() { // ioredis: 'wait' | 'reconnecting' | 'connecting' | 'connect' | 'ready' | 'close' | 'end' return this.client.status === 'ready' || this.client.status === 'connect'; } 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); } } _getCacheKey(type, params = {}) { const { timeRange, startDate, endDate, metricId, metric, daily, cacheKey, isPreviousPeriod, customFilters, } = params; let key = `klaviyo:${type}`; if (type === 'stats:details') { key += `:${metric || 'all'}`; if (daily) key += ':daily'; if (customFilters?.length) { const filterHash = customFilters.join('').replace(/[^a-zA-Z0-9]/g, ''); key += `:${filterHash}`; } } if (cacheKey) { key += `:${cacheKey}`; } else if (timeRange) { key += `:${timeRange}`; if (metricId) key += `:${metricId}`; if (isPreviousPeriod) key += ':prev'; } else if (startDate && endDate) { key += `:custom:${startDate}:${endDate}`; if (metricId) key += `:${metricId}`; if (isPreviousPeriod) key += ':prev'; } if (['pre_orders', 'local_pickup', 'on_hold'].includes(metric)) { key += `:${metric}`; } return key; } _getTTL(timeRange) { const TTL_MAP = { today: 2 * 60, yesterday: 30 * 60, thisWeek: 5 * 60, lastWeek: 60 * 60, thisMonth: 10 * 60, lastMonth: 2 * 60 * 60, last7days: 5 * 60, last30days: 15 * 60, custom: 15 * 60, }; return TTL_MAP[timeRange] || this.DEFAULT_TTL; } async getEventData(type, params) { if (!this.isConnected) return null; try { const baseKey = this._getCacheKey('events', params); return await this.get(`${baseKey}:${type}`); } 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); 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); } } }