/** * Redis caching layer for The Order * Implements caching for database queries, cache invalidation, and cache monitoring */ import { createClient } from 'redis'; import type { RedisClientType } from 'redis'; import { getEnv, createLogger } from '@the-order/shared'; const logger = createLogger('cache'); export interface CacheConfig { url?: string; ttl?: number; // Default TTL in seconds keyPrefix?: string; enableCompression?: boolean; } export interface CacheStats { hits: number; misses: number; sets: number; deletes: number; errors: number; } /** * Redis Cache Client */ export class CacheClient { private client: RedisClientType | null = null; private config: Required; private stats: CacheStats = { hits: 0, misses: 0, sets: 0, deletes: 0, errors: 0, }; constructor(config: CacheConfig = {}) { const env = getEnv(); this.config = { url: config.url || env.REDIS_URL || 'redis://localhost:6379', ttl: config.ttl || 3600, // 1 hour default keyPrefix: config.keyPrefix || 'the-order:', enableCompression: config.enableCompression || false, }; } /** * Initialize Redis client */ async connect(): Promise { if (this.client) { return; } try { this.client = createClient({ url: this.config.url, }) as RedisClientType; this.client.on('error', (err) => { logger.error('Redis client error:', err); this.stats.errors++; }); this.client.on('connect', () => { logger.info('Redis client connected'); }); this.client.on('disconnect', () => { logger.warn('Redis client disconnected'); }); await this.client.connect(); } catch (error) { logger.error('Failed to connect to Redis:', error); throw error; } } /** * Disconnect Redis client */ async disconnect(): Promise { if (this.client) { await this.client.quit(); this.client = null; } } /** * Get value from cache */ async get(key: string): Promise { if (!this.client) { await this.connect(); } if (!this.client) { this.stats.errors++; return null; } try { const fullKey = this.getFullKey(key); const value = await this.client.get(fullKey); if (value === null) { this.stats.misses++; return null; } this.stats.hits++; return this.deserialize(value); } catch (error) { logger.error(`Cache get error for key ${key}:`, error); this.stats.errors++; this.stats.misses++; return null; } } /** * Set value in cache */ async set(key: string, value: unknown, ttl?: number): Promise { if (!this.client) { await this.connect(); } if (!this.client) { this.stats.errors++; return; } try { const fullKey = this.getFullKey(key); const serialized = this.serialize(value); const expiresIn = ttl || this.config.ttl; await this.client.setEx(fullKey, expiresIn, serialized); this.stats.sets++; } catch (error) { logger.error(`Cache set error for key ${key}:`, error); this.stats.errors++; } } /** * Delete value from cache */ async delete(key: string): Promise { if (!this.client) { await this.connect(); } if (!this.client) { this.stats.errors++; return; } try { const fullKey = this.getFullKey(key); await this.client.del(fullKey); this.stats.deletes++; } catch (error) { logger.error(`Cache delete error for key ${key}:`, error); this.stats.errors++; } } /** * Delete multiple keys by pattern */ async invalidate(pattern: string): Promise { if (!this.client) { await this.connect(); } if (!this.client) { this.stats.errors++; return 0; } try { const fullPattern = this.getFullKey(pattern); const keys = await this.client.keys(fullPattern); if (keys.length === 0) { return 0; } const deleted = await this.client.del(keys); this.stats.deletes += deleted; return deleted; } catch (error) { logger.error(`Cache invalidate error for pattern ${pattern}:`, error); this.stats.errors++; return 0; } } /** * Check if key exists */ async exists(key: string): Promise { if (!this.client) { await this.connect(); } if (!this.client) { this.stats.errors++; return false; } try { const fullKey = this.getFullKey(key); const result = await this.client.exists(fullKey); return result === 1; } catch (error) { logger.error(`Cache exists error for key ${key}:`, error); this.stats.errors++; return false; } } /** * Get cache statistics */ getStats(): CacheStats { return { ...this.stats }; } /** * Reset cache statistics */ resetStats(): void { this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, errors: 0, }; } /** * Get full key with prefix */ private getFullKey(key: string): string { return `${this.config.keyPrefix}${key}`; } /** * Serialize value */ private serialize(value: unknown): string { return JSON.stringify(value); } /** * Deserialize value */ private deserialize(value: string): T { return JSON.parse(value) as T; } } /** * Get default cache client */ let defaultCacheClient: CacheClient | null = null; export function getCacheClient(config?: CacheConfig): CacheClient { if (!defaultCacheClient) { defaultCacheClient = new CacheClient(config); } return defaultCacheClient; } /** * Cache decorator for functions */ export function cached Promise>( fn: T, keyGenerator?: (...args: Parameters) => string, ttl?: number ): T { const cache = getCacheClient(); return (async (...args: Parameters) => { const key = keyGenerator ? keyGenerator(...args) : `fn:${fn.name}:${JSON.stringify(args)}`; const cachedValue = await cache.get(key); if (cachedValue !== null) { return cachedValue as ReturnType; } const result = await fn(...args); await cache.set(key, result, ttl); return result; }) as T; }