UNPKG

@voilajsx/appkit

Version:

Minimal and framework agnostic Node.js toolkit designed for AI agentic backend development

318 lines 11.3 kB
/** * Redis cache strategy with automatic connection management and retry logic * @module @voilajsx/appkit/cache * @file src/cache/strategies/redis.ts * * @llm-rule WHEN: App has REDIS_URL environment variable for distributed caching * @llm-rule AVOID: Manual Redis setup - this handles connection, retry, and serialization automatically * @llm-rule NOTE: Auto-reconnects on failure, handles JSON serialization, production-ready */ /** * Redis cache strategy with enterprise-grade reliability */ export class RedisStrategy { config; client = null; connected = false; connectionPromise = null; /** * Creates Redis strategy with direct environment access (like auth pattern) * @llm-rule WHEN: Cache initialization with Redis URL detected * @llm-rule AVOID: Manual Redis configuration - environment detection handles this */ constructor(config) { this.config = config; } /** * Connects to Redis with automatic retry and connection pooling * @llm-rule WHEN: Cache initialization or reconnection after failure * @llm-rule AVOID: Manual connection management - this handles all Redis complexity */ async connect() { if (this.connected) return; // Prevent multiple connection attempts if (this.connectionPromise) { return this.connectionPromise; } this.connectionPromise = this.establishConnection(); await this.connectionPromise; this.connectionPromise = null; } /** * Establishes Redis connection with retry logic */ async establishConnection() { try { // Dynamic import for Redis client const { createClient } = await import('redis'); const redisConfig = this.config.redis; // Create Redis client with comprehensive configuration this.client = createClient({ url: redisConfig.url, password: redisConfig.password, socket: { connectTimeout: redisConfig.connectTimeout, reconnectStrategy: (retries) => { if (retries >= redisConfig.maxRetries) { console.error(`[AppKit] Redis max retries (${redisConfig.maxRetries}) exceeded`); return new Error('Redis connection failed'); } const delay = Math.min(redisConfig.retryDelay * Math.pow(2, retries), 10000); console.warn(`[AppKit] Redis reconnecting in ${delay}ms (attempt ${retries + 1})`); return delay; }, }, commandsQueueMaxLength: 1000, // Set global command timeout via client options isolationPoolOptions: { min: 1, max: 10, }, }); // Set up event handlers this.setupEventHandlers(); // Connect to Redis await this.client.connect(); this.connected = true; if (this.config.environment.isDevelopment) { console.log(`✅ [AppKit] Redis connected to ${this.maskUrl(redisConfig.url)}`); } } catch (error) { this.connected = false; this.client = null; throw new Error(`Redis connection failed: ${error.message}`); } } /** * Sets up Redis event handlers for connection management */ setupEventHandlers() { if (!this.client) return; this.client.on('error', (error) => { console.error('[AppKit] Redis error:', error.message); this.connected = false; }); this.client.on('connect', () => { if (this.config.environment.isDevelopment) { console.log('🔄 [AppKit] Redis connecting...'); } }); this.client.on('ready', () => { this.connected = true; if (this.config.environment.isDevelopment) { console.log('✅ [AppKit] Redis ready'); } }); this.client.on('reconnecting', () => { this.connected = false; if (this.config.environment.isDevelopment) { console.log('🔄 [AppKit] Redis reconnecting...'); } }); this.client.on('end', () => { this.connected = false; if (this.config.environment.isDevelopment) { console.log('👋 [AppKit] Redis connection ended'); } }); } /** * Disconnects from Redis gracefully * @llm-rule WHEN: App shutdown or cache cleanup * @llm-rule AVOID: Abrupt disconnection - graceful shutdown prevents data loss */ async disconnect() { if (!this.client || !this.connected) return; try { await this.client.quit(); this.connected = false; this.client = null; } catch (error) { console.error('[AppKit] Redis disconnect error:', error.message); // Force close if graceful quit fails if (this.client) { this.client.disconnect(); this.client = null; } } } /** * Gets value from Redis with automatic JSON deserialization * @llm-rule WHEN: Retrieving cached data from distributed Redis cache * @llm-rule AVOID: Manual Redis commands - this handles serialization automatically */ async get(key) { await this.ensureConnected(); try { const value = await this.client.get(key); if (value === null) { return null; // Key not found or expired } // Deserialize JSON value return this.deserialize(value); } catch (error) { console.error(`[AppKit] Redis get error for key "${key}":`, error.message); return null; // Graceful degradation } } /** * Sets value in Redis with TTL and automatic JSON serialization * @llm-rule WHEN: Storing data in distributed Redis cache with expiration * @llm-rule AVOID: Manual Redis commands - this handles serialization and TTL automatically */ async set(key, value, ttl) { await this.ensureConnected(); try { // Serialize value to JSON const serialized = this.serialize(value); // Set with TTL (Redis EX option expects seconds) const result = await this.client.setEx(key, ttl, serialized); return result === 'OK'; } catch (error) { console.error(`[AppKit] Redis set error for key "${key}":`, error.message); return false; } } /** * Deletes key from Redis * @llm-rule WHEN: Cache invalidation or removing specific cached data * @llm-rule AVOID: Manual key management - this handles Redis delete operations */ async delete(key) { await this.ensureConnected(); try { const result = await this.client.del(key); return result === 1; // Redis returns number of keys deleted } catch (error) { console.error(`[AppKit] Redis delete error for key "${key}":`, error.message); return false; } } /** * Clears all keys matching pattern (usually namespace-based) * @llm-rule WHEN: Namespace-based cache invalidation * @llm-rule AVOID: Using FLUSHDB - this only clears specific namespace keys */ async clear() { // Note: This is handled by the main cache class using keys() + deleteMany() // We don't implement it here to avoid accidental full cache clearing throw new Error('Clear operation should be handled by cache class using keys() + deleteMany()'); } /** * Checks if key exists in Redis * @llm-rule WHEN: Checking cache key existence without retrieving value * @llm-rule AVOID: Using get() then checking null - this is more efficient */ async has(key) { await this.ensureConnected(); try { const result = await this.client.exists(key); return result === 1; } catch (error) { console.error(`[AppKit] Redis has error for key "${key}":`, error.message); return false; } } /** * Gets all keys matching pattern (for namespace operations) * @llm-rule WHEN: Finding all keys in namespace for bulk operations * @llm-rule AVOID: Using KEYS in production with large datasets - use SCAN instead */ async keys(pattern = '*') { await this.ensureConnected(); try { // Use SCAN instead of KEYS for production safety const keys = []; let cursor = 0; do { const result = await this.client.scan(cursor, { MATCH: pattern, COUNT: 1000, // Scan in batches of 1000 }); cursor = result.cursor; keys.push(...result.keys); } while (cursor !== 0); return keys; } catch (error) { console.error(`[AppKit] Redis keys error for pattern "${pattern}":`, error.message); return []; } } /** * Deletes multiple keys efficiently * @llm-rule WHEN: Bulk deletion operations like namespace clearing * @llm-rule AVOID: Individual delete calls - batch operations are much faster */ async deleteMany(keys) { if (keys.length === 0) return 0; await this.ensureConnected(); try { // Redis DEL command accepts multiple keys const result = await this.client.del(keys); return result; // Returns number of keys deleted } catch (error) { console.error(`[AppKit] Redis deleteMany error:`, error.message); return 0; } } // Private helper methods /** * Ensures Redis connection is established */ async ensureConnected() { if (!this.connected) { await this.connect(); } } /** * Serializes value to JSON string for Redis storage */ serialize(value) { try { return JSON.stringify(value); } catch (error) { throw new Error(`Failed to serialize value: ${error.message}`); } } /** * Deserializes JSON string from Redis */ deserialize(value) { try { return JSON.parse(value); } catch (error) { // If it's not valid JSON, return as string (backward compatibility) return value; } } /** * Masks sensitive parts of Redis URL for logging */ maskUrl(url) { try { const parsed = new URL(url); if (parsed.password) { parsed.password = '***'; } return parsed.toString(); } catch { return 'redis://***'; } } } //# sourceMappingURL=redis.js.map