/** * Redis Cache Implementation * Provides distributed caching with TTL support */ import Redis from 'ioredis'; export interface RedisCacheOptions { defaultTTL: number; // in milliseconds keyPrefix: string; retryStrategy?: (times: number) => number | null; } export class RedisCache { private redis: Redis; private defaultTTL: number; private keyPrefix: string; constructor(redisUrl: string, options?: Partial) { this.defaultTTL = options?.defaultTTL || 60000; // 1 minute default this.keyPrefix = options?.keyPrefix || 'lazy-render:'; this.redis = new Redis(redisUrl, { retryStrategy: options?.retryStrategy || ((times: number) => { if (times > 3) { return null; // Stop retrying after 3 attempts } return Math.min(times * 200, 3000); // Exponential backoff }) }); this.redis.on('error', (err) => { console.error('Redis error:', err); }); } /** * Get value from cache */ async get(key: string): Promise { const fullKey = this.keyPrefix + key; const data = await this.redis.get(fullKey); return data ? JSON.parse(data) : null; } /** * Set value in cache with TTL */ async set(key: string, value: any, ttl?: number): Promise { const fullKey = this.keyPrefix + key; const timeout = ttl || this.defaultTTL; const serialized = JSON.stringify(value); await this.redis.setex(fullKey, Math.floor(timeout / 1000), serialized); } /** * Delete value from cache */ async delete(key: string): Promise { const fullKey = this.keyPrefix + key; await this.redis.del(fullKey); } /** * Check if key exists */ async exists(key: string): Promise { const fullKey = this.keyPrefix + key; const result = await this.redis.exists(fullKey); return result === 1; } /** * Get TTL for key */ async getTTL(key: string): Promise { const fullKey = this.keyPrefix + key; const ttl = await this.redis.ttl(fullKey); return ttl > 0 ? ttl * 1000 : 0; // Convert to milliseconds } /** * Extend TTL for key */ async touch(key: string, ttl?: number): Promise { const fullKey = this.keyPrefix + key; const timeout = ttl || this.defaultTTL; await this.redis.expire(fullKey, Math.floor(timeout / 1000)); } /** * Clear all keys with prefix */ async clear(): Promise { const keys = await this.redis.keys(this.keyPrefix + '*'); if (keys.length > 0) { await this.redis.del(...keys); } } /** * Get cache statistics */ async getStats(): Promise<{ totalKeys: number; memoryUsage: number; connected: boolean; }> { const [keys, memory, info] = await Promise.all([ this.redis.keys(this.keyPrefix + '*'), this.redis.info('memory'), this.redis.info('server') ]); return { totalKeys: keys.length, memoryUsage: this.parseMemoryInfo(memory), connected: info.includes('connected') }; } /** * Close Redis connection */ async disconnect(): Promise { await this.redis.quit(); } /** * Parse Redis memory info */ private parseMemoryInfo(memoryInfo: string): number { const lines = memoryInfo.split('\r\n'); const usedMemoryLine = lines.find(line => line.startsWith('used_memory:')); return usedMemoryLine ? parseInt(usedMemoryLine.split(':')[1]) : 0; } /** * Get Redis client (for advanced operations) */ getClient(): Redis { return this.redis; } } export default RedisCache;