xpeditis2.0/apps/backend/src/infrastructure/cache/redis-cache.adapter.ts
2025-10-20 12:30:08 +02:00

182 lines
4.9 KiB
TypeScript

/**
* 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<void> {
const host = this.configService.get<string>('REDIS_HOST', 'localhost');
const port = this.configService.get<number>('REDIS_PORT', 6379);
const password = this.configService.get<string>('REDIS_PASSWORD');
const db = this.configService.get<number>('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<void> {
await this.client.quit();
this.logger.log('Redis connection closed');
}
async get<T>(key: string): Promise<T | null> {
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<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
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<void> {
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<void> {
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<boolean> {
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<number> {
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<void> {
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;
}
}