182 lines
4.9 KiB
TypeScript
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;
|
|
}
|
|
}
|