import Redis from 'ioredis'; // Redis/Valkey connection instance let redisClient: Redis | null = null; /** * Get or create Redis connection * Uses REDIS_URL from environment or defaults to localhost */ export function getRedisClient(): Redis { if (!redisClient) { const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; redisClient = new Redis(redisUrl, { maxRetriesPerRequest: 3, retryStrategy(times) { const delay = Math.min(times * 50, 2000); return delay; }, reconnectOnError(err) { const targetErrors = ['READONLY', 'ECONNRESET', 'ETIMEDOUT']; return targetErrors.some(targetError => err.message.includes(targetError)); } }); redisClient.on('error', (err) => { console.error('Redis connection error:', { message: err.message }); }); redisClient.on('connect', () => { console.log('Redis connected successfully'); }); } return redisClient; } /** * Cache helper functions */ interface CacheOptions { ttl?: number; // Time to live in seconds (default: 300 = 5 minutes) } /** * Get cached data by key */ export async function getCached(key: string): Promise { try { const redis = getRedisClient(); const data = await redis.get(key); if (!data) { return null; } return JSON.parse(data) as T; } catch (err) { console.error('Cache get error:', { key, message: (err as Error).message }); return null; } } /** * Set cached data with optional TTL */ export async function setCached(key: string, data: any, options: CacheOptions = {}): Promise { try { const redis = getRedisClient(); const ttl = options.ttl || 300; // Default 5 minutes const serialized = JSON.stringify(data); await redis.setex(key, ttl, serialized); return true; } catch (err) { console.error('Cache set error:', { key, message: (err as Error).message }); return false; } } /** * Delete cached data by key or pattern */ export async function deleteCached(keyOrPattern: string): Promise { try { const redis = getRedisClient(); // If it contains wildcards, use scan + delete if (keyOrPattern.includes('*')) { let cursor = '0'; let deletedCount = 0; do { const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', keyOrPattern, 'COUNT', 100); cursor = nextCursor; if (keys.length > 0) { const deleted = await redis.del(...keys); deletedCount += deleted; } } while (cursor !== '0'); return deletedCount; } else { // Direct key deletion return await redis.del(keyOrPattern); } } catch (err) { console.error('Cache delete error:', { keyOrPattern, message: (err as Error).message }); return 0; } } /** * Search functionality using Redis */ export interface SearchOptions { fields?: string[]; // Fields to search in limit?: number; // Max results fuzzy?: boolean; // Enable fuzzy matching } /** * Cache search results */ export async function cacheSearchResults(query: string, results: any[], options: CacheOptions = {}): Promise { const cacheKey = `search:${query.toLowerCase().trim()}`; return setCached(cacheKey, results, options); } /** * Get cached search results */ export async function getCachedSearchResults(query: string): Promise { const cacheKey = `search:${query.toLowerCase().trim()}`; return getCached(cacheKey); } /** * Invalidate all search caches */ export async function invalidateSearchCache(): Promise { return deleteCached('search:*'); } /** * Close Redis connection (for cleanup) */ export async function closeRedisConnection(): Promise { if (redisClient) { await redisClient.quit(); redisClient = null; } }