/** * Redis Cache Adapter * * Implements CachePort interface using Redis (ioredis) */ import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Redis from 'ioredis'; import { CachePort } from '../../domain/ports/out/cache.port'; @Injectable() export class RedisCacheAdapter implements CachePort, OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(RedisCacheAdapter.name); private client: Redis; private stats = { hits: 0, misses: 0, }; constructor(private readonly configService: ConfigService) {} async onModuleInit(): Promise { const host = this.configService.get('REDIS_HOST', 'localhost'); const port = this.configService.get('REDIS_PORT', 6379); const password = this.configService.get('REDIS_PASSWORD'); const db = this.configService.get('REDIS_DB', 0); this.client = new Redis({ host, port, password, db, retryStrategy: times => { const delay = Math.min(times * 50, 2000); return delay; }, maxRetriesPerRequest: 3, }); this.client.on('connect', () => { this.logger.log(`Connected to Redis at ${host}:${port}`); }); this.client.on('error', err => { this.logger.error(`Redis connection error: ${err.message}`); }); this.client.on('ready', () => { this.logger.log('Redis client ready'); }); } async onModuleDestroy(): Promise { await this.client.quit(); this.logger.log('Redis connection closed'); } async get(key: string): Promise { try { const value = await this.client.get(key); if (value === null) { this.stats.misses++; return null; } this.stats.hits++; return JSON.parse(value) as T; } catch (error: any) { this.logger.error(`Error getting key ${key}: ${error?.message || 'Unknown error'}`); return null; } } async set(key: string, value: T, ttlSeconds?: number): Promise { try { const serialized = JSON.stringify(value); if (ttlSeconds) { await this.client.setex(key, ttlSeconds, serialized); } else { await this.client.set(key, serialized); } } catch (error: any) { this.logger.error(`Error setting key ${key}: ${error?.message || 'Unknown error'}`); throw error; } } async delete(key: string): Promise { try { await this.client.del(key); } catch (error: any) { this.logger.error(`Error deleting key ${key}: ${error?.message || 'Unknown error'}`); throw error; } } async deleteMany(keys: string[]): Promise { if (keys.length === 0) return; try { await this.client.del(...keys); } catch (error: any) { this.logger.error(`Error deleting keys: ${error?.message || 'Unknown error'}`); throw error; } } async exists(key: string): Promise { try { const result = await this.client.exists(key); return result === 1; } catch (error: any) { this.logger.error( `Error checking key existence ${key}: ${error?.message || 'Unknown error'}` ); return false; } } async ttl(key: string): Promise { try { return await this.client.ttl(key); } catch (error: any) { this.logger.error(`Error getting TTL for key ${key}: ${error?.message || 'Unknown error'}`); return -2; } } async clear(): Promise { try { await this.client.flushdb(); this.logger.warn('Redis database cleared'); } catch (error: any) { this.logger.error(`Error clearing cache: ${error?.message || 'Unknown error'}`); throw error; } } async getStats(): Promise<{ hits: number; misses: number; hitRate: number; keyCount: number; }> { try { const keyCount = await this.client.dbsize(); const total = this.stats.hits + this.stats.misses; const hitRate = total > 0 ? this.stats.hits / total : 0; return { hits: this.stats.hits, misses: this.stats.misses, hitRate: Math.round(hitRate * 10000) / 100, // Percentage with 2 decimals keyCount, }; } catch (error: any) { this.logger.error(`Error getting stats: ${error?.message || 'Unknown error'}`); return { hits: this.stats.hits, misses: this.stats.misses, hitRate: 0, keyCount: 0, }; } } /** * Reset statistics (useful for testing) */ resetStats(): void { this.stats.hits = 0; this.stats.misses = 0; } /** * Get Redis client (for advanced usage) */ getClient(): Redis { return this.client; } }