import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common' import Redis from 'ioredis' @Injectable() export class RedisService implements OnModuleDestroy { private readonly logger = new Logger(RedisService.name) private readonly client: Redis constructor() { const host = process.env.REDIS_HOST || 'redis' const port = Number(process.env.REDIS_PORT || 6379) const password = process.env.REDIS_PASSWORD || undefined const db = Number(process.env.REDIS_DB || 0) this.client = new Redis({ host, port, password, db, lazyConnect: true, maxRetriesPerRequest: 3, }) this.client.on('error', (error) => { this.logger.error(`Redis error: ${error.message}`) }) } async getClient(): Promise { if (this.client.status !== 'ready') { await this.client.connect() } return this.client } async get(key: string): Promise { const client = await this.getClient() return client.get(key) } async getJson(key: string): Promise { const value = await this.get(key) if (!value) { return null } try { return JSON.parse(value) as T } catch { this.logger.warn(`Invalid JSON for key "${key}"`) return null } } async set( key: string, value: string, ttlSeconds?: number, ): Promise<'OK' | null> { const client = await this.getClient() if (ttlSeconds && ttlSeconds > 0) { return client.set(key, value, 'EX', ttlSeconds) } return client.set(key, value) } async setJson( key: string, value: unknown, ttlSeconds?: number, ): Promise<'OK' | null> { return this.set(key, JSON.stringify(value), ttlSeconds) } async delete(key: string): Promise { const client = await this.getClient() return client.del(key) } async exists(key: string): Promise { const client = await this.getClient() const result = await client.exists(key) return result === 1 } async expire(key: string, ttlSeconds: number): Promise { const client = await this.getClient() const result = await client.expire(key, ttlSeconds) return result === 1 } async deleteByPattern(pattern: string): Promise { const client = await this.getClient() let deletedCount = 0 let cursor = '0' do { const [nextCursor, keys] = await client.scan( cursor, 'MATCH', pattern, 'COUNT', 100, ) cursor = nextCursor if (keys.length > 0) { const pipeline = client.pipeline() keys.forEach(k => pipeline.del(k)) const results = await pipeline.exec() if (results) { deletedCount += results.reduce((sum, [, value]) => { return sum + (typeof value === 'number' ? value : 0) }, 0) } } } while (cursor !== '0') return deletedCount } async deleteByPatterns(patterns: string[]): Promise { const deletedCounts = await Promise.all( patterns.map(pattern => this.deleteByPattern(pattern)), ) return deletedCounts.reduce((sum, count) => sum + count, 0) } async onModuleDestroy() { await this.client.quit() } }