UNPKG

xypriss-security

Version:

XyPriss Security is an advanced JavaScript security library designed for enterprise applications. It provides military-grade encryption, secure data structures, quantum-resistant cryptography, and comprehensive security utilities for modern web applicatio

1,338 lines (1,337 loc) 56.5 kB
import { EventEmitter } from 'events'; import Redis, { Cluster } from 'ioredis'; import { EncryptionService } from '../encryption/EncryptionService.js'; import { XyPrissSecurity } from 'xypriss-security'; import { initializeLogger } from '../../shared/logger/Logger.js'; import { SecureInMemoryCache as SIMC } from '../../components/cache/useCache.js'; import '../../components/cache/index.js'; /** * XyPrissJS Secure Cache Adapter * Ultra-fast hybrid cache system combining security cache with Redis clustering * * Features: * - Memory-first hybrid architecture for maximum speed * - Redis Cluster support with automatic failover * - Connection pooling and health monitoring * - Advanced tagging and invalidation * - Real-time performance metrics * - Military-grade security from XyPrissJS security cache */ /** * UF secure cache adapter */ class SecureCacheAdapter extends EventEmitter { constructor(config = {}) { super(); this.connectionPool = new Map(); this.metadata = new Map(); this.logger = initializeLogger(); this.config = { strategy: "hybrid", memory: { maxSize: 100, // 100MB maxEntries: 10000, ttl: 10 * 60 * 1000, // 10 minutes ...config.memory, }, redis: { host: "localhost", port: 6379, pool: { min: 2, max: 10, acquireTimeoutMillis: 30000, }, ...config.redis, }, performance: { batchSize: 100, compressionThreshold: 1024, hotDataThreshold: 10, prefetchEnabled: true, ...config.performance, }, security: { encryption: true, keyRotation: true, accessMonitoring: true, ...config.security, }, monitoring: { enabled: true, metricsInterval: 60000, // 1 minute alertThresholds: { memoryUsage: 90, hitRate: 80, errorRate: 5, }, ...config.monitoring, }, ...config, }; this.initializeStats(); this.initializeMasterKey(); this.initializeMemoryCache(); } /** * Initialize statistics */ initializeStats() { this.stats = { memory: { hits: 0, misses: 0, evictions: 0, totalSize: 0, entryCount: 0, hitRate: 0, totalAccesses: 0, size: 0, capacity: this.config.memory?.maxEntries || 10000, memoryUsage: { used: 0, limit: (this.config.memory?.maxSize || 100) * 1024 * 1024, percentage: 0, }, }, redis: this.config.strategy === "redis" || this.config.strategy === "hybrid" ? { connected: false, commandsProcessed: 0, operations: 0, memoryUsage: { used: 0, peak: 0, percentage: 0, }, keyspaceHits: 0, keyspaceMisses: 0, hits: 0, misses: 0, hitRate: 0, connectedClients: 0, connections: 0, keys: 0, uptime: 0, lastUpdate: 0, } : undefined, performance: { totalOperations: 0, averageResponseTime: 0, hotDataHitRate: 0, compressionRatio: 0, networkLatency: 0, }, security: { encryptedEntries: 0, keyRotations: 0, suspiciousAccess: 0, securityEvents: 0, }, }; } /** * Initialize master encryption key for consistent encryption */ initializeMasterKey() { // Generate a consistent master key for all cache operations this.masterEncryptionKey = XyPrissSecurity.generateSecureToken({ length: 32, entropy: "high", }); } /** * Initialize memory cache with security features */ initializeMemoryCache() { this.memoryCache = new SIMC(); // Listen to security events this.memoryCache.on("key_rotation", (event) => { this.stats.security.keyRotations++; this.emit("security_event", { type: "key_rotation", ...event }); }); this.memoryCache.on("suspicious_access", (event) => { this.stats.security.suspiciousAccess++; this.emit("security_event", { type: "suspicious_access", ...event, }); }); this.memoryCache.on("memory_pressure", (event) => { this.emit("performance_alert", { type: "memory_pressure", ...event, }); }); } /** * Connect to cache backends */ async connect() { try { // Memory cache is always ready // console.log(" Secure memory cache initialized"); // Initialize Redis if needed if (this.config.strategy === "redis" || this.config.strategy === "hybrid") { await this.initializeRedis(); } // Start monitoring if (this.config.monitoring?.enabled) { this.startMonitoring(); } this.emit("connected"); } catch (error) { this.emit("error", error); throw new Error(`Cache connection failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Initialize Redis with clustering and failover support */ async initializeRedis() { const redisConfig = this.config.redis; try { if (redisConfig.cluster?.enabled && redisConfig.cluster.nodes) { // Redis Cluster mode this.redisClient = new Cluster(redisConfig.cluster.nodes, { redisOptions: { password: redisConfig.password, db: redisConfig.db || 0, lazyConnect: true, }, ...redisConfig.cluster.options, }); this.logger.startup("server", " Redis Cluster initialized"); } else if (redisConfig.sentinel?.enabled) { // Redis Sentinel mode this.redisClient = new Redis({ sentinels: redisConfig.sentinel.sentinels, name: redisConfig.sentinel.name || "mymaster", password: redisConfig.password, db: redisConfig.db || 0, lazyConnect: true, }); this.logger.info("server", " Redis Sentinel initialized"); } else { // Single Redis instance this.redisClient = new Redis({ host: redisConfig.host, port: redisConfig.port, password: redisConfig.password, db: redisConfig.db || 0, lazyConnect: true, connectTimeout: 5000, // 5 second timeout commandTimeout: 5000, // 5 second command timeout retryDelayOnFailover: 100, // This property exists in ioredis maxRetriesPerRequest: 2, }); // Use type assertion to bypass strict typing this.logger.info("server", " Redis single instance initialized"); } // Setup Redis event handlers this.setupRedisEventHandlers(); // Connect to Redis with timeout await Promise.race([ this.redisClient.connect(), new Promise((_, reject) => setTimeout(() => reject(new Error("Redis connection timeout")), 10000)), ]); } catch (error) { console.error(" Redis initialization failed:", error); throw error; } } /** * Setup Redis event handlers for monitoring and failover */ setupRedisEventHandlers() { if (!this.redisClient) return; this.redisClient.on("connect", () => { this.emit("redis_connected"); }); this.redisClient.on("ready", () => { this.logger.info("server", "Connected to Redis"); this.emit("redis_ready"); }); this.redisClient.on("error", (error) => { console.error(" Redis error:", error); this.emit("redis_error", error); }); this.redisClient.on("close", () => { console.warn(" Redis connection closed"); this.emit("redis_disconnected"); }); this.redisClient.on("reconnecting", () => { this.logger.warn("server", " Redis reconnecting..."); this.emit("redis_reconnecting"); }); // Cluster-specific events if (this.redisClient instanceof Redis.Cluster) { this.redisClient.on("node error", (error, node) => { console.error(` Redis cluster node error (${node.options.host}:${node.options.port}):`, error); this.emit("cluster_node_error", { error, node }); }); this.redisClient.on("+node", (node) => { this.logger.info("server", ` Redis cluster node added: ${node.options.host}:${node.options.port}`); this.emit("cluster_node_added", node); }); this.redisClient.on("-node", (node) => { this.logger.warn("server", ` Redis cluster node removed: ${node.options.host}:${node.options.port}`); this.emit("cluster_node_removed", node); }); } } /** * Start monitoring and health checks */ startMonitoring() { // Health monitoring this.healthMonitor = setInterval(async () => { await this.performHealthCheck(); }, 30000); // Every 30 seconds // Metrics collection this.metricsCollector = setInterval(() => { this.collectMetrics(); }, this.config.monitoring?.metricsInterval || 60000); } /** * Perform health check on all cache backends */ async performHealthCheck() { try { // Check memory cache const memoryStats = this.memoryCache.getStats; // Check Redis if available if (this.redisClient) { const redisInfo = await this.redisClient.ping(); if (redisInfo !== "PONG") { this.emit("health_check_failed", { backend: "redis", reason: "ping_failed", }); } } // Check alert thresholds const thresholds = this.config.monitoring?.alertThresholds; if (thresholds) { if (memoryStats.memoryUsage.percentage > (thresholds.memoryUsage || 90)) { this.emit("performance_alert", { type: "high_memory_usage", value: memoryStats.memoryUsage.percentage, threshold: thresholds.memoryUsage, }); } if (memoryStats.hitRate < (thresholds.hitRate || 80) / 100) { this.emit("performance_alert", { type: "low_hit_rate", value: memoryStats.hitRate * 100, threshold: thresholds.hitRate, }); } } } catch (error) { this.emit("health_check_failed", { error }); } } /** * Collect performance metrics */ collectMetrics() { try { // Update memory stats this.stats.memory = this.memoryCache.getStats; // Calculate performance metrics this.updatePerformanceMetrics(); // Emit metrics event this.emit("metrics_collected", this.stats); } catch (error) { console.error("Metrics collection failed:", error); } } /** * Update performance metrics */ updatePerformanceMetrics() { // Calculate hot data hit rate let hotDataHits = 0; let totalHotAccess = 0; for (const [, meta] of this.metadata.entries()) { if (meta.isHot) { totalHotAccess += meta.accessCount; if (meta.location === "memory") { hotDataHits += meta.accessCount; } } } this.stats.performance.hotDataHitRate = totalHotAccess > 0 ? hotDataHits / totalHotAccess : 0; // Update security stats this.stats.security.encryptedEntries = this.metadata.size; } /** * Generate cache key with namespace and security */ generateKey(key) { // Validate key if (!key || typeof key !== "string") { throw new Error("Cache key must be a non-empty string"); } if (key.length > 512) { throw new Error("Cache key too long (max 512 characters)"); } // Create deterministic hash of the key using crypto const crypto = require("crypto"); const hashedKey = crypto.createHash("sha256").update(key).digest("hex"); return `XyPriss:v2:${hashedKey.substring(0, 16)}:${key}`; } /** * Determine if data should be considered "hot" (frequently accessed) */ isHotData(key) { const meta = this.metadata.get(key); if (!meta) return false; const threshold = this.config.performance?.hotDataThreshold || 10; const timeWindow = 60 * 60 * 1000; // 1 hour const now = Date.now(); return (meta.accessCount >= threshold && now - meta.lastAccessed < timeWindow); } /** * Update access metadata for performance optimization */ updateAccessMetadata(key, size = 0) { const now = Date.now(); const meta = this.metadata.get(key) || { accessCount: 0, lastAccessed: now, size, isHot: false, location: "memory", tags: [], }; meta.accessCount++; meta.lastAccessed = now; meta.isHot = this.isHotData(key); this.metadata.set(key, meta); this.stats.performance.totalOperations++; } // ======================================== // SERIALIZATION AND DATA HANDLING // ======================================== /** * Convert any data to CachedData format for SecurityCache compatibility */ toCachedData(value) { if (typeof value === "object" && value !== null && "data" in value) { // Already in CachedData format return value; } // Wrap raw data in CachedData format return { data: value, metadata: { timestamp: Date.now(), type: typeof value, size: JSON.stringify(value).length, }, }; } /** * Extract raw data from CachedData format */ fromCachedData(cachedData) { if (!cachedData) return null; // Return the actual data, not the wrapper return cachedData.data; } /** * Serialize data for Redis storage with proper encryption */ async serializeForRedis(value) { try { // First convert to JSON let serialized = JSON.stringify(value); // Apply encryption if enabled if (this.config.security?.encryption) { serialized = await EncryptionService.encrypt(value, this.masterEncryptionKey, { algorithm: "aes-256-gcm", quantumSafe: false, }); } return serialized; } catch (error) { throw new Error(`Serialization failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Deserialize data from Redis storage with proper decryption */ async deserializeFromRedis(serialized) { try { // Apply decryption if enabled if (this.config.security?.encryption) { return await EncryptionService.decrypt(serialized, this.masterEncryptionKey); } // Parse JSON if no encryption return JSON.parse(serialized); } catch (error) { throw new Error(`Deserialization failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } // ======================================== // CORE CACHE OPERATIONS // ======================================== /** * Get value from cache with ultra-fast hybrid strategy * * @param key - The cache key to retrieve * @returns Promise resolving to the cached value with proper typing, or null if not found * * @example * ```typescript * interface User { id: number; name: string; } * const user = await cache.get<User>("user:123"); * if (user) { * console.log(user.name); // TypeScript knows this is a string * } * ``` */ async get(key) { const startTime = Date.now(); try { const cacheKey = this.generateKey(key); // Strategy 1: Try memory cache first (fastest) if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { const memoryResult = await this.memoryCache.get(cacheKey); if (memoryResult !== null) { this.updateAccessMetadata(cacheKey); this.recordResponseTime(Date.now() - startTime); // Extract raw data from CachedData format return this.fromCachedData(memoryResult); } } // Strategy 2: Try Redis if memory miss if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { const redisResult = await this.getFromRedis(cacheKey); if (redisResult !== null) { // For hybrid strategy, promote hot data to memory if (this.config.strategy === "hybrid" && this.isHotData(cacheKey)) { // Convert to CachedData format for memory cache const cachedData = this.toCachedData(redisResult); await this.memoryCache.set(cacheKey, cachedData, { ttl: this.config.memory?.ttl, }); } this.updateAccessMetadata(cacheKey); this.recordResponseTime(Date.now() - startTime); return redisResult; } } // Cache miss this.recordResponseTime(Date.now() - startTime); return null; } catch (error) { this.emit("cache_error", { operation: "get", key, error }); this.recordResponseTime(Date.now() - startTime); return null; } } /** * Set value in cache with intelligent placement * * @param key - The cache key to store the value under * @param value - The value to cache with proper typing * @param options - Optional caching options * @param options.ttl - Time to live in milliseconds * @param options.tags - Array of tags for bulk invalidation * @returns Promise resolving to true if successful, false otherwise * * @example * ```typescript * interface User { id: number; name: string; email: string; } * * const user: User = { id: 123, name: "John", email: "john@example.com" }; * const success = await cache.set<User>("user:123", user, { * ttl: 3600000, // 1 hour * tags: ["users", "active"] * }); * ``` */ async set(key, value, options = {}) { const startTime = Date.now(); try { const cacheKey = this.generateKey(key); const ttl = options.ttl || this.config.memory?.ttl || 600000; // 10 minutes default // Determine storage strategy const shouldStoreInMemory = this.config.strategy === "memory" || this.config.strategy === "hybrid"; const shouldStoreInRedis = this.config.strategy === "redis" || this.config.strategy === "hybrid"; // Store in memory cache if (shouldStoreInMemory) { // Convert to CachedData format for memory cache const cachedData = this.toCachedData(value); await this.memoryCache.set(cacheKey, cachedData, { ttl }); } // Store in Redis if (shouldStoreInRedis && this.redisClient) { await this.setInRedis(cacheKey, value, ttl, options.tags); } // Update metadata this.updateAccessMetadata(cacheKey, JSON.stringify(value).length); if (options.tags) { const meta = this.metadata.get(cacheKey); if (meta) { meta.tags = options.tags; this.metadata.set(cacheKey, meta); } } this.recordResponseTime(Date.now() - startTime); return true; } catch (error) { this.emit("cache_error", { operation: "set", key, error }); this.recordResponseTime(Date.now() - startTime); return false; } } /** * Set encrypted session data with expiration (SETEX for sessions) * * This method provides secure session storage with automatic encryption, * expiration, and session-specific security features. * * @param sessionKey - The session key (will be prefixed with 'session:') * @param sessionData - The session data to encrypt and store * @param ttlSeconds - Time to live in seconds (Redis SETEX style) * @param options - Optional session-specific options * @param options.userId - User ID for session tracking * @param options.ipAddress - IP address for session validation * @param options.userAgent - User agent for session validation * @param options.encryptionKey - Custom encryption key (uses default if not provided) * @param options.tags - Array of tags for bulk invalidation * @returns Promise resolving to true if successful, false otherwise * * @example * ```typescript * interface SessionData { * userId: string; * username: string; * roles: string[]; * lastActivity: number; * } * * const sessionData: SessionData = { * userId: "user123", * username: "john_doe", * roles: ["user", "admin"], * lastActivity: Date.now() * }; * * // Store encrypted session for 1 hour * const success = await cache.setex("abc123def456", sessionData, 3600, { * userId: "user123", * ipAddress: "192.168.1.100", * userAgent: "Mozilla/5.0...", * tags: ["sessions", "user123"] * }); * ``` */ async setex(sessionKey, sessionData, ttlSeconds, options = {}) { const startTime = Date.now(); try { // Import XyPriss Security for encryption const { XyPrissSecurity, Hash } = await import('xypriss-security'); const { EncryptionService } = await import('../encryption/EncryptionService.js'); // Validate inputs if (!sessionKey || typeof sessionKey !== "string") { throw new Error("Session key must be a non-empty string"); } if (ttlSeconds <= 0 || ttlSeconds > 86400 * 30) { // Max 30 days throw new Error("TTL must be between 1 second and 30 days"); } // Generate session-specific cache key const cacheKey = this.generateKey(`session:${sessionKey}`); const ttlMs = ttlSeconds * 1000; // Create session metadata const sessionMetadata = { sessionId: sessionKey, userId: options.userId, ipAddress: options.ipAddress, userAgent: options.userAgent, createdAt: Date.now(), expiresAt: Date.now() + ttlMs, version: "1.0", }; // Create secure session payload const sessionPayload = { data: sessionData, metadata: sessionMetadata, signature: XyPrissSecurity.generateSessionToken({ userId: options.userId, ipAddress: options.ipAddress, userAgent: options.userAgent, expiresIn: ttlSeconds, }), }; // Encrypt the session payload const encryptionKey = options.encryptionKey || EncryptionService.generateSessionKey(); const encryptedPayload = await EncryptionService.encrypt(JSON.stringify(sessionPayload), encryptionKey); // Create final session object with encryption metadata const keyHashForStorage = Hash.create(encryptionKey, { algorithm: "sha256", outputFormat: "hex", }) .toString() .substring(0, 16); const secureSessionData = { encrypted: true, algorithm: "AES-256-GCM", payload: encryptedPayload, keyHash: keyHashForStorage, // First 16 chars for verification sessionMetadata: { sessionId: sessionKey, userId: options.userId, createdAt: sessionMetadata.createdAt, expiresAt: sessionMetadata.expiresAt, }, }; // Store using the standard set method with session-specific options const success = await this.set(cacheKey, secureSessionData, { ttl: ttlMs, tags: options.tags ? ["sessions", ...options.tags] : ["sessions"], }); if (success) { // Update session tracking metadata this.updateSessionMetadata(sessionKey, { userId: options.userId, ipAddress: options.ipAddress, userAgent: options.userAgent, lastAccess: Date.now(), encrypted: true, }); this.emit("session_stored", { sessionId: sessionKey, userId: options.userId, ttl: ttlSeconds, encrypted: true, }); } this.recordResponseTime(Date.now() - startTime); return success; } catch (error) { this.emit("cache_error", { operation: "setex", key: sessionKey, error, }); this.recordResponseTime(Date.now() - startTime); return false; } } /** * Get encrypted session data with automatic decryption * * Retrieves and decrypts session data stored with the setex method. * Validates session integrity and expiration. * * @param sessionKey - The session key used when storing * @param encryptionKey - The encryption key used when storing (required for decryption) * @param options - Optional validation options * @param options.validateUserId - User ID to validate against stored session * @param options.validateIpAddress - IP address to validate against stored session * @param options.validateUserAgent - User agent to validate against stored session * @returns Promise resolving to decrypted session data, or null if not found/invalid * * @example * ```typescript * // Retrieve session data * const sessionData = await cache.getSession<UserSession>( * "session_abc123def456", * encryptionKey, * { * validateUserId: "user123", * validateIpAddress: "192.168.1.100" * } * ); * * if (sessionData) { * console.log("Session valid:", sessionData.username); * } else { * console.log("Session not found or invalid"); * } * ``` */ async getSession(sessionKey, encryptionKey, options = {}) { const startTime = Date.now(); try { // Import required modules const { XyPrissSecurity, Hash } = await import('xypriss-security'); const { EncryptionService } = await import('../encryption/EncryptionService.js'); // Validate inputs if (!sessionKey || typeof sessionKey !== "string") { throw new Error("Session key must be a non-empty string"); } if (!encryptionKey || typeof encryptionKey !== "string") { throw new Error("Encryption key is required for session decryption"); } // Get the encrypted session data const cacheKey = this.generateKey(`session:${sessionKey}`); const encryptedSessionData = await this.get(cacheKey); if (!encryptedSessionData) { this.recordResponseTime(Date.now() - startTime); return null; } // Validate it's an encrypted session if (!encryptedSessionData.encrypted || !encryptedSessionData.payload) { console.warn("Retrieved data is not an encrypted session"); this.recordResponseTime(Date.now() - startTime); return null; } // Verify encryption key hash const keyHash = Hash.create(encryptionKey, { algorithm: "sha256", outputFormat: "hex", }) .toString() .substring(0, 16); if (encryptedSessionData.keyHash !== keyHash) { console.warn("Invalid encryption key for session"); this.recordResponseTime(Date.now() - startTime); return null; } // Decrypt the session payload const decryptedPayload = await EncryptionService.decrypt(encryptedSessionData.payload, encryptionKey); const sessionPayload = JSON.parse(decryptedPayload); // Validate session expiration if (sessionPayload.metadata.expiresAt < Date.now()) { console.warn("Session has expired"); // Clean up expired session await this.delete(cacheKey); this.recordResponseTime(Date.now() - startTime); return null; } // Validate session context if provided if (options.validateUserId && sessionPayload.metadata.userId !== options.validateUserId) { console.warn("Session user ID mismatch"); this.recordResponseTime(Date.now() - startTime); return null; } if (options.validateIpAddress && sessionPayload.metadata.ipAddress !== options.validateIpAddress) { console.warn("Session IP address mismatch"); this.recordResponseTime(Date.now() - startTime); return null; } if (options.validateUserAgent && sessionPayload.metadata.userAgent !== options.validateUserAgent) { console.warn("Session user agent mismatch"); this.recordResponseTime(Date.now() - startTime); return null; } // Update session access metadata this.updateSessionMetadata(sessionKey, { userId: sessionPayload.metadata.userId, ipAddress: sessionPayload.metadata.ipAddress, userAgent: sessionPayload.metadata.userAgent, lastAccess: Date.now(), encrypted: true, }); this.emit("session_accessed", { sessionId: sessionKey, userId: sessionPayload.metadata.userId, accessTime: Date.now(), }); this.recordResponseTime(Date.now() - startTime); return sessionPayload.data; } catch (error) { this.emit("cache_error", { operation: "getSession", key: sessionKey, error, }); this.recordResponseTime(Date.now() - startTime); return null; } } /** * Update session-specific metadata */ updateSessionMetadata(sessionKey, sessionInfo) { try { const cacheKey = this.generateKey(`session:${sessionKey}`); const existingMeta = this.metadata.get(cacheKey) || { accessCount: 0, lastAccessed: Date.now(), size: 0, isHot: false, location: "memory", tags: ["sessions"], }; // Update with session-specific information const updatedMeta = { ...existingMeta, sessionId: sessionKey, userId: sessionInfo.userId, ipAddress: sessionInfo.ipAddress, userAgent: sessionInfo.userAgent, lastAccessed: sessionInfo.lastAccess || Date.now(), encrypted: sessionInfo.encrypted || false, accessCount: existingMeta.accessCount + 1, tags: existingMeta.tags || ["sessions"], }; this.metadata.set(cacheKey, updatedMeta); } catch (error) { // Silently fail to avoid breaking the main operation console.warn("Failed to update session metadata:", error); } } /** * Delete value from cache */ async delete(key) { const startTime = Date.now(); try { const cacheKey = this.generateKey(key); let deleted = false; // Delete from memory cache if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { deleted = this.memoryCache.delete(cacheKey) || deleted; } // Delete from Redis if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { const redisDeleted = await this.redisClient.del(cacheKey); deleted = redisDeleted > 0 || deleted; } // Clean up metadata this.metadata.delete(cacheKey); this.recordResponseTime(Date.now() - startTime); return deleted; } catch (error) { this.emit("cache_error", { operation: "delete", key, error }); this.recordResponseTime(Date.now() - startTime); return false; } } /** * Check if key exists in cache */ async exists(key) { try { const cacheKey = this.generateKey(key); // Check memory cache first if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { if (this.memoryCache.has(cacheKey)) { return true; } } // Check Redis if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { const exists = await this.redisClient.exists(cacheKey); return exists > 0; } return false; } catch (error) { this.emit("cache_error", { operation: "exists", key, error }); return false; } } /** * Clear all cache entries */ async clear() { try { // Clear memory cache if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { this.memoryCache.clear(); } // Clear Redis if ((this.config.strategy === "redis" || this.config.strategy === "hybrid") && this.redisClient) { await this.redisClient.flushdb(); } // Clear metadata this.metadata.clear(); this.emit("cache_cleared"); } catch (error) { this.emit("cache_error", { operation: "clear", error }); throw error; } } // ======================================== // REDIS HELPER METHODS // ======================================== /** * Get value from Redis with encryption support */ async getFromRedis(key) { if (!this.redisClient) return null; try { const serialized = await this.redisClient.get(key); if (!serialized) return null; // Use consistent deserialization return await this.deserializeFromRedis(serialized); } catch (error) { console.error("Redis get error:", error); return null; } } /** * Set value in Redis with encryption and TTL support */ async setInRedis(key, value, ttl, tags) { if (!this.redisClient) return; try { // Use consistent serialization const serialized = await this.serializeForRedis(value); // Set with TTL if (ttl > 0) { await this.redisClient.setex(key, Math.floor(ttl / 1000), serialized); } else { await this.redisClient.set(key, serialized); } // Handle tags for cache invalidation if (tags && tags.length > 0) { await this.setTags(key, tags); } } catch (error) { console.error("Redis set error:", error); throw error; } } /** * Set tags for cache invalidation */ async setTags(key, tags) { if (!this.redisClient) return; try { const pipeline = this.redisClient.pipeline(); for (const tag of tags) { const tagKey = `tag:${tag}`; pipeline.sadd(tagKey, key); pipeline.expire(tagKey, 86400); // 24 hours } await pipeline.exec(); } catch (error) { console.error("Redis tag set error:", error); } } /** * Record response time for performance monitoring */ recordResponseTime(responseTime) { // Update running average const currentAvg = this.stats.performance.averageResponseTime; const totalOps = this.stats.performance.totalOperations; this.stats.performance.averageResponseTime = (currentAvg * (totalOps - 1) + responseTime) / totalOps; } // ======================================== // ADVANCED CACHE OPERATIONS // ======================================== /** * Invalidate cache entries by tags */ async invalidateByTags(tags) { if (!this.redisClient) return 0; try { let invalidatedCount = 0; for (const tag of tags) { const tagKey = `tag:${tag}`; const keys = await this.redisClient.smembers(tagKey); if (keys.length > 0) { // Delete all keys with this tag await this.redisClient.del(...keys); // Remove from memory cache too for (const key of keys) { this.memoryCache.delete(key); this.metadata.delete(key); } invalidatedCount += keys.length; } // Clean up the tag set await this.redisClient.del(tagKey); } this.emit("cache_invalidated", { tags, count: invalidatedCount }); return invalidatedCount; } catch (error) { this.emit("cache_error", { operation: "invalidateByTags", tags, error, }); return 0; } } /** * Get multiple values at once (batch operation) * * @param keys - Array of cache keys to retrieve * @returns Promise resolving to an object with key-value pairs (missing keys are omitted) * * @example * ```typescript * interface User { id: number; name: string; } * * const users = await cache.mget<User>(["user:1", "user:2", "user:3"]); * // users is Record<string, User> * * for (const [key, user] of Object.entries(users)) { * console.log(`${key}: ${user.name}`); // TypeScript knows user.name is string * } * ``` */ async mget(keys) { const results = {}; try { // Use Promise.all for parallel execution const promises = keys.map(async (key) => { const value = await this.get(key); return { key, value }; }); const resolved = await Promise.all(promises); for (const { key, value } of resolved) { if (value !== null) { results[key] = value; } } return results; } catch (error) { this.emit("cache_error", { operation: "mget", keys, error }); return {}; } } /** * Set multiple values at once (batch operation) * * @param entries - Object with key-value pairs or array of [key, value] tuples * @param options - Optional caching options applied to all entries * @param options.ttl - Time to live in milliseconds for all entries * @param options.tags - Array of tags applied to all entries * @returns Promise resolving to true if all operations successful, false otherwise * * @example * ```typescript * interface User { id: number; name: string; } * * // Using object notation * const success1 = await cache.mset<User>({ * "user:1": { id: 1, name: "Alice" }, * "user:2": { id: 2, name: "Bob" } * }, { ttl: 3600000, tags: ["users"] }); * * // Using array notation * const success2 = await cache.mset<User>([ * ["user:3", { id: 3, name: "Charlie" }], * ["user:4", { id: 4, name: "Diana" }] * ], { ttl: 3600000 }); * ``` */ async mset(entries, options = {}) { try { // Convert array format to object format if needed const entriesObj = Array.isArray(entries) ? Object.fromEntries(entries) : entries; // Use Promise.all for parallel execution const promises = Object.entries(entriesObj).map(([key, value]) => this.set(key, value, options)); const results = await Promise.all(promises); return results.every((result) => result === true); } catch (error) { this.emit("cache_error", { operation: "mset", entries: Object.keys(entries), error, }); return false; } } // ======================================== // TYPE-SAFE ALIAS METHODS // ======================================== /** * Read value from cache (alias for get method) * * @param key - The cache key to retrieve * @returns Promise resolving to the cached value with proper typing, or null if not found * * @example * ```typescript * interface User { id: number; name: string; } * const user = await cache.read<User>("user:123"); * if (user) { * console.log(user.name); // TypeScript knows this is a string * } * ``` */ async read(key) { return this.get(key); } /** * Write value to cache (alias for set method) * * @param key - The cache key to store the value under * @param value - The value to cache with proper typing * @param options - Optional caching options * @param options.ttl - Time to live in milliseconds * @param options.tags - Array of tags for bulk invalidation * @returns Promise resolving to true if successful, false otherwise * * @example * ```typescript * interface User { id: number; name: string; email: string; } * * const user: User = { id: 123, name: "John", email: "john@example.com" }; * const success = await cache.write<User>("user:123", user, { * ttl: 3600000, // 1 hour * tags: ["users", "active"] * }); * ``` */ async write(key, value, options = {}) { return this.set(key, value, options); } // ======================================== // ENHANCED CACHE METHODS // ======================================== /** * Get TTL for a specific key */ async getTTL(key) { const cacheKey = this.generateKey(key); try { // Check memory cache first by checking if key exists if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { if (this.memoryCache.has(cacheKey)) { // For memory cache, we can't get exact TTL, so return a default return 300000; // 5 minutes default } } // Check Redis cache if (this.redisClient && (this.config.strategy === "redis" || this.config.strategy === "hybrid")) { const redisTTL = await this.redisClient.ttl(cacheKey); return redisTTL > 0 ? redisTTL * 1000 : -1; // Convert to milliseconds } return -1; // Key doesn't exist or no TTL } catch (error) { console.error("Get TTL error:", error); return -1; } } /** * Set expiration time for a key */ async expire(key, ttl) { const cacheKey = this.generateKey(key); try { let success = false; // Update memory cache TTL if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { const value = await this.memoryCache.get(cacheKey); if (value !== null) { await this.memoryCache.set(cacheKey, value, { ttl }); success = true; } } // Update Redis cache TTL if (this.redisClient && (this.config.strategy === "redis" || this.config.strategy === "hybrid")) { const result = await this.redisClient.expire(cacheKey, Math.floor(ttl / 1000)); success = success || result === 1; } return success; } catch (error) { console.error("Expire error:", error); return false; } } /** * Get all keys matching a pattern */ async keys(pattern) { try { const allKeys = new Set(); // Get keys from memory cache using metadata if (this.config.strategy === "memory" || this.config.strategy === "hybrid") { // Use metadata to get all keys for (const [cacheKey] of this.metadata.entries()) { const originalKey = this.extractOriginalKey(cacheKey); if (originalKey) { allKeys.add(originalKey); } } } // Get keys from Redis cache if (this.redisClient && (this.config.strategy === "redis" || this.config.strategy === "hybrid")) { const redisPattern = pattern ? `XyPriss:v2:*:${pattern}` : "XyPriss:v2:*"; const redisKeys = await this.redisClient.keys(redisPattern); redisKeys.forEach((key) => { const originalKey = this.extractOriginalKey(key); if (originalKey) { allKeys.add(originalKey); } }); } const keysArray = Array.from(allKeys); // Apply patter