import { LoggerService } from '@/modules/logger/pino/logger.service'; import { Cache } from '@nestjs/cache-manager'; import { createHash } from 'crypto'; import { CacheConfiguration } from '../domain/interfaces/cache-configuration.interface'; /** * Base service with caching capabilities. */ export class CachedService { protected readonly serviceName = this.constructor.name.toLowerCase().replace('service', ''); /** * Creates an instance of CachedBaseService. * * @param {LoggerService} logger - The logger service. * @param {Cache} cacheManager - The NestJS cache manager. * @param {CacheConfiguration} cacheConfig - The cache configuration. */ constructor( protected readonly logger: LoggerService, protected readonly cacheManager: Cache, protected readonly cacheConfig: CacheConfiguration, ) { } /** * Generates a cache key based on the service name, method, and parameters. * * @param {string} method - The method name. * @param {any} params - The parameters to include in the key. * @returns {string} The generated cache key. */ protected generateCacheKey(method: string, params?: any): string { const paramsHash = params ? createHash('md5').update(JSON.stringify(params)).digest('hex') : 'all'; return `${this.serviceName}:${method}:${paramsHash}`; } /** * Gets data from cache or executes the provided function and caches the result. * * @param {string} key - The cache key. * @param {() => Promise} fn - The function to execute if cache miss. * @returns {Promise} The cached or freshly fetched data. */ protected async getOrSetCache(key: string, fn: () => Promise): Promise { try { // Try to get from cache const cached = await this.cacheManager.get(key); if (cached) { this.logger.debug(`Cache hit for key: ${key}`); return JSON.parse(cached) as T; } this.logger.debug(`Cache miss for key: ${key}`); // Cache miss - fetch fresh data const result = await fn(); // Store in cache await this.cacheManager.set(key, JSON.stringify(result)); return result; } catch (error) { this.logger.error(`An error occurred while fetching cache for key: ${key}`); this.logger.error(error); // On failure, fallback to freshly fetched data return await fn(); } } /** * Invalidates cache. * * @returns {Promise} */ protected async invalidateCache(): Promise { try { this.logger.debug(`Invalidating cache for service: ${this.serviceName}`); const storeIterator = this.cacheManager.stores[0].iterator; const keys: Array = []; if (storeIterator) { for await (const [key, value] of storeIterator('namespace')) { if (key.split(':')[0] === this.serviceName) { keys.push(key); } } await this.cacheManager.mdel(keys); } } catch (error) { // Silently fail cache invalidation to not break the main flow this.logger.error(`An error occurred while invalidating cache for service: ${this.serviceName}`); this.logger.error(error); } } }