import { Inject, Injectable } from "@nestjs/common"; import Redis from "ioredis"; import { Logger } from "nestjs-pino"; @Injectable() export class CacheService { constructor( @Inject("REDIS_CLIENT") private readonly redis: Redis, @Inject(Logger) private readonly logger: Logger ) {} async get(key: string): Promise { const value = await this.redis.get(key); if (!value) { return null; } try { return JSON.parse(value) as T; } catch (error) { this.logger.warn({ key, error }, "Failed to parse cached value; evicting entry"); await this.redis.del(key); return null; } } async set(key: string, value: unknown, ttlSeconds?: number): Promise { const serialized = JSON.stringify(value); if (ttlSeconds !== undefined) { const ttl = Math.max(0, Math.floor(ttlSeconds)); if (ttl > 0) { await this.redis.set(key, serialized, "EX", ttl); } else { // Allow callers to request immediate expiry without leaking stale values await this.redis.set(key, serialized, "PX", 1); } return; } await this.redis.set(key, serialized); } async del(key: string): Promise { await this.redis.del(key); } async delPattern(pattern: string): Promise { let cursor = "0"; const batch: string[] = []; const flush = async () => { if (batch.length > 0) { const pipeline = this.redis.pipeline(); for (const k of batch.splice(0, batch.length)) pipeline.del(k); await pipeline.exec(); } }; do { const [next, keys] = (await this.redis.scan( cursor, "MATCH", pattern, "COUNT", 1000 )) as unknown as [string, string[]]; cursor = next; if (keys && keys.length) { batch.push(...keys); if (batch.length >= 1000) { await flush(); } } } while (cursor !== "0"); await flush(); } async exists(key: string): Promise { return (await this.redis.exists(key)) === 1; } buildKey(prefix: string, userId: string, ...parts: string[]): string { return [prefix, userId, ...parts].join(":"); } async getOrSet(key: string, fetcher: () => Promise, ttlSeconds: number = 300): Promise { const cached = await this.get(key); if (cached !== null) { return cached; } const fresh = await fetcher(); await this.set(key, fresh, ttlSeconds); return fresh; } }