UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

852 lines 25.8 kB
/** * @fileoverview OrdoJS Cache Manager - Comprehensive caching and real-time data management */ /** * Default cache configuration */ const DEFAULT_CACHE_CONFIG = { type: 'memory', defaultTTL: 3600, maxSize: 100 * 1024 * 1024, // 100MB enableCompression: true, enableEncryption: false, enableRealtimeSync: false, syncInterval: 5000, enableCacheWarming: false, warmingStrategies: [], enableAnalytics: true, analyticsRetention: 30, redis: { host: 'localhost', port: 6379, database: 0, timeout: 5000, retryStrategy: 'exponential', maxRetries: 3 }, file: { directory: './cache', extension: '.cache', enableSubdirectories: true, maxFileSize: 10 * 1024 * 1024, // 10MB cleanupInterval: 3600000 // 1 hour } }; /** * Comprehensive cache manager for OrdoJS applications */ export class CacheManager { config; memoryCache = new Map(); redisClient = null; fileCache = new Map(); analytics = { totalRequests: 0, hits: 0, misses: 0, hitRate: 0, averageResponseTime: 0, memoryUsage: 0, diskUsage: 0, topKeys: [], performance: { readsPerSecond: 0, writesPerSecond: 0, deletesPerSecond: 0, averageReadTime: 0, averageWriteTime: 0, averageDeleteTime: 0 } }; syncEvents = []; warmingJobs = new Map(); cleanupInterval = null; syncInterval = null; constructor(config = {}) { this.config = { ...DEFAULT_CACHE_CONFIG, ...config }; this.initializeCache(); } /** * Set cache entry */ async set(key, data, ttl = this.config.defaultTTL, tags = [], metadata = {}) { const startTime = Date.now(); try { const entry = { key, data, expiresAt: new Date(Date.now() + ttl * 1000), createdAt: new Date(), lastAccessed: new Date(), accessCount: 0, size: this.calculateSize(data), tags, metadata }; switch (this.config.type) { case 'memory': await this.setMemoryCache(key, entry); break; case 'redis': await this.setRedisCache(key, entry); break; case 'file': await this.setFileCache(key, entry); break; case 'hybrid': await this.setHybridCache(key, entry); break; } const responseTime = Date.now() - startTime; this.updateAnalytics('write', responseTime); // Trigger real-time sync if (this.config.enableRealtimeSync) { this.triggerSyncEvent('set', key, data); } return { hit: true, data, key, ttl, metadata: { type: this.config.type, size: entry.size, responseTime } }; } catch (error) { return { hit: false, key, error: error instanceof Error ? error.message : String(error) }; } } /** * Get cache entry */ async get(key) { const startTime = Date.now(); try { let entry = null; switch (this.config.type) { case 'memory': entry = await this.getMemoryCache(key); break; case 'redis': entry = await this.getRedisCache(key); break; case 'file': entry = await this.getFileCache(key); break; case 'hybrid': entry = await this.getHybridCache(key); break; } const responseTime = Date.now() - startTime; if (entry && new Date() < entry.expiresAt) { // Update access statistics entry.lastAccessed = new Date(); entry.accessCount++; this.updateAnalytics('read', responseTime, true); this.updateTopKeys(key); return { hit: true, data: entry.data, key, ttl: Math.ceil((entry.expiresAt.getTime() - Date.now()) / 1000), metadata: { type: this.config.type, size: entry.size, responseTime, accessCount: entry.accessCount } }; } else { this.updateAnalytics('read', responseTime, false); return { hit: false, key, ttl: 0 }; } } catch (error) { const responseTime = Date.now() - startTime; this.updateAnalytics('read', responseTime, false); return { hit: false, key, error: error instanceof Error ? error.message : String(error) }; } } /** * Delete cache entry */ async delete(key) { const startTime = Date.now(); try { switch (this.config.type) { case 'memory': this.memoryCache.delete(key); break; case 'redis': await this.deleteRedisCache(key); break; case 'file': await this.deleteFileCache(key); break; case 'hybrid': await this.deleteHybridCache(key); break; } const responseTime = Date.now() - startTime; this.updateAnalytics('delete', responseTime); // Trigger real-time sync if (this.config.enableRealtimeSync) { this.triggerSyncEvent('delete', key); } return { hit: true, key, metadata: { type: this.config.type, responseTime } }; } catch (error) { return { hit: false, key, error: error instanceof Error ? error.message : String(error) }; } } /** * Clear all cache entries */ async clear() { try { switch (this.config.type) { case 'memory': this.memoryCache.clear(); break; case 'redis': await this.clearRedisCache(); break; case 'file': await this.clearFileCache(); break; case 'hybrid': await this.clearHybridCache(); break; } // Trigger real-time sync if (this.config.enableRealtimeSync) { this.triggerSyncEvent('clear', ''); } return { hit: true, key: 'all', metadata: { type: this.config.type } }; } catch (error) { return { hit: false, key: 'all', error: error instanceof Error ? error.message : String(error) }; } } /** * Get cache entries by tag */ async getByTag(tag) { const results = []; try { switch (this.config.type) { case 'memory': for (const [key, entry] of this.memoryCache) { if (entry.tags.includes(tag) && new Date() < entry.expiresAt) { results.push({ hit: true, data: [entry.data], key, ttl: Math.ceil((entry.expiresAt.getTime() - Date.now()) / 1000) }); } } break; case 'redis': results.push(...await this.getRedisCacheByTag(tag)); break; case 'file': results.push(...await this.getFileCacheByTag(tag)); break; case 'hybrid': results.push(...await this.getHybridCacheByTag(tag)); break; } } catch (error) { results.push({ hit: false, key: tag, error: error instanceof Error ? error.message : String(error) }); } return results; } /** * Delete cache entries by tag */ async deleteByTag(tag) { try { const entries = await this.getByTag(tag); let deletedCount = 0; for (const entry of entries) { if (entry.hit && entry.key) { await this.delete(entry.key); deletedCount++; } } return { hit: true, key: tag, metadata: { type: this.config.type, deletedCount } }; } catch (error) { return { hit: false, key: tag, error: error instanceof Error ? error.message : String(error) }; } } /** * Get cache statistics */ getStatistics() { const currentTime = Date.now(); const timeWindow = 60000; // 1 minute // Calculate performance metrics const recentEvents = this.syncEvents.filter(event => currentTime - event.timestamp.getTime() < timeWindow); const reads = recentEvents.filter(e => e.type === 'set').length; const writes = recentEvents.filter(e => e.type === 'set').length; const deletes = recentEvents.filter(e => e.type === 'delete').length; this.analytics.performance = { readsPerSecond: reads / 60, writesPerSecond: writes / 60, deletesPerSecond: deletes / 60, averageReadTime: this.analytics.averageResponseTime, averageWriteTime: this.analytics.averageResponseTime, averageDeleteTime: this.analytics.averageResponseTime }; return { ...this.analytics }; } /** * Warm cache */ async warmCache() { if (!this.config.enableCacheWarming) { return; } for (const strategy of this.config.warmingStrategies) { try { await this.executeWarmingStrategy(strategy); } catch (error) { console.error(`Cache warming strategy '${strategy.name}' failed:`, error); } } } /** * Add warming strategy */ addWarmingStrategy(strategy) { this.warmingJobs.set(strategy.name, strategy); this.config.warmingStrategies.push(strategy); } /** * Remove warming strategy */ removeWarmingStrategy(name) { this.warmingJobs.delete(name); this.config.warmingStrategies = this.config.warmingStrategies.filter(s => s.name !== name); } /** * Get cache metadata */ getMetadata() { const currentSize = this.calculateTotalSize(); const hitRate = this.analytics.totalRequests > 0 ? (this.analytics.hits / this.analytics.totalRequests) * 100 : 0; return { type: this.config.type, size: currentSize, hitRate, missRate: 100 - hitRate, averageAccessTime: this.analytics.averageResponseTime, lastCleanup: new Date(), memoryUsage: this.getMemoryUsage() }; } /** * Cleanup expired entries */ async cleanup() { const now = new Date(); switch (this.config.type) { case 'memory': for (const [key, entry] of this.memoryCache) { if (now > entry.expiresAt) { this.memoryCache.delete(key); } } break; case 'redis': await this.cleanupRedisCache(); break; case 'file': await this.cleanupFileCache(); break; case 'hybrid': await this.cleanupHybridCache(); break; } // Cleanup old analytics data if (this.config.enableAnalytics) { this.cleanupAnalytics(); } } /** * Initialize cache */ async initializeCache() { switch (this.config.type) { case 'redis': await this.initializeRedisCache(); break; case 'file': await this.initializeFileCache(); break; case 'hybrid': await this.initializeHybridCache(); break; } // Start cleanup interval this.cleanupInterval = setInterval(() => { this.cleanup(); }, this.config.file?.cleanupInterval || 3600000); // Start sync interval if (this.config.enableRealtimeSync) { this.syncInterval = setInterval(() => { this.syncCache(); }, this.config.syncInterval); } } /** * Initialize Redis cache */ async initializeRedisCache() { // In a real implementation, you'd use a Redis client // This is a simplified version for demonstration this.redisClient = { set: async (key, value, ttl) => { // Simulate Redis set }, get: async (key) => { // Simulate Redis get return null; }, del: async (key) => { // Simulate Redis delete }, flushdb: async () => { // Simulate Redis flush } }; } /** * Initialize file cache */ async initializeFileCache() { // In a real implementation, you'd create the cache directory // This is a simplified version for demonstration if (this.config.file) { // Ensure cache directory exists // await fs.mkdir(this.config.file.directory, { recursive: true }); } } /** * Initialize hybrid cache */ async initializeHybridCache() { await this.initializeRedisCache(); await this.initializeFileCache(); } /** * Set memory cache entry */ async setMemoryCache(key, entry) { this.memoryCache.set(key, entry); this.enforceMemoryLimit(); } /** * Get memory cache entry */ async getMemoryCache(key) { return this.memoryCache.get(key) || null; } /** * Set Redis cache entry */ async setRedisCache(key, entry) { if (!this.redisClient) { throw new Error('Redis client not initialized'); } const serialized = this.serializeEntry(entry); await this.redisClient.set(key, serialized, entry.expiresAt.getTime() - Date.now()); } /** * Get Redis cache entry */ async getRedisCache(key) { if (!this.redisClient) { throw new Error('Redis client not initialized'); } const data = await this.redisClient.get(key); if (!data) return null; return this.deserializeEntry(data); } /** * Delete Redis cache entry */ async deleteRedisCache(key) { if (!this.redisClient) { throw new Error('Redis client not initialized'); } await this.redisClient.del(key); } /** * Clear Redis cache */ async clearRedisCache() { if (!this.redisClient) { throw new Error('Redis client not initialized'); } await this.redisClient.flushdb(); } /** * Set file cache entry */ async setFileCache(key, entry) { if (!this.config.file) { throw new Error('File cache not configured'); } const filename = this.getCacheFilename(key); const serialized = this.serializeEntry(entry); // In a real implementation, you'd write to file // await fs.writeFile(filename, serialized); this.fileCache.set(key, filename); } /** * Get file cache entry */ async getFileCache(key) { if (!this.config.file) { throw new Error('File cache not configured'); } const filename = this.getCacheFilename(key); // In a real implementation, you'd read from file // const data = await fs.readFile(filename, 'utf8'); // return this.deserializeEntry<T>(data); return null; } /** * Delete file cache entry */ async deleteFileCache(key) { if (!this.config.file) { throw new Error('File cache not configured'); } const filename = this.getCacheFilename(key); // In a real implementation, you'd delete file // await fs.unlink(filename); this.fileCache.delete(key); } /** * Clear file cache */ async clearFileCache() { if (!this.config.file) { throw new Error('File cache not configured'); } // In a real implementation, you'd clear directory // await fs.rmdir(this.config.file.directory, { recursive: true }); this.fileCache.clear(); } /** * Set hybrid cache entry */ async setHybridCache(key, entry) { await Promise.all([ this.setMemoryCache(key, entry), this.setRedisCache(key, entry) ]); } /** * Get hybrid cache entry */ async getHybridCache(key) { // Try memory first, then Redis let entry = await this.getMemoryCache(key); if (!entry) { entry = await this.getRedisCache(key); if (entry) { // Populate memory cache await this.setMemoryCache(key, entry); } } return entry; } /** * Delete hybrid cache entry */ async deleteHybridCache(key) { await Promise.all([ this.memoryCache.delete(key), this.deleteRedisCache(key) ]); } /** * Clear hybrid cache */ async clearHybridCache() { await Promise.all([ this.memoryCache.clear(), this.clearRedisCache() ]); } /** * Get Redis cache by tag */ async getRedisCacheByTag(tag) { // In a real implementation, you'd use Redis SCAN or similar return []; } /** * Get file cache by tag */ async getFileCacheByTag(tag) { // In a real implementation, you'd scan files for tag metadata return []; } /** * Get hybrid cache by tag */ async getHybridCacheByTag(tag) { const results = await Promise.all([ this.getRedisCacheByTag(tag), this.getFileCacheByTag(tag) ]); return results.flat(); } /** * Serialize cache entry */ serializeEntry(entry) { let data = JSON.stringify(entry); if (this.config.enableCompression) { // In a real implementation, you'd compress the data // data = await compress(data); } if (this.config.enableEncryption && this.config.encryptionKey) { // In a real implementation, you'd encrypt the data // data = await encrypt(data, this.config.encryptionKey); } return data; } /** * Deserialize cache entry */ deserializeEntry(data) { if (this.config.enableEncryption && this.config.encryptionKey) { // In a real implementation, you'd decrypt the data // data = await decrypt(data, this.config.encryptionKey); } if (this.config.enableCompression) { // In a real implementation, you'd decompress the data // data = await decompress(data); } return JSON.parse(data); } /** * Calculate data size */ calculateSize(data) { return JSON.stringify(data).length; } /** * Calculate total cache size */ calculateTotalSize() { let totalSize = 0; for (const entry of this.memoryCache.values()) { totalSize += entry.size; } return totalSize; } /** * Enforce memory limit */ enforceMemoryLimit() { const currentSize = this.calculateTotalSize(); if (currentSize > this.config.maxSize) { // Remove oldest entries const entries = Array.from(this.memoryCache.entries()) .sort(([, a], [, b]) => a.lastAccessed.getTime() - b.lastAccessed.getTime()); for (const [key] of entries) { this.memoryCache.delete(key); if (this.calculateTotalSize() <= this.config.maxSize * 0.8) { break; } } } } /** * Get memory usage */ getMemoryUsage() { return process.memoryUsage().heapUsed; } /** * Update analytics */ updateAnalytics(operation, responseTime, hit = true) { this.analytics.totalRequests++; this.analytics.averageResponseTime = (this.analytics.averageResponseTime * (this.analytics.totalRequests - 1) + responseTime) / this.analytics.totalRequests; if (hit) { this.analytics.hits++; } else { this.analytics.misses++; } this.analytics.hitRate = (this.analytics.hits / this.analytics.totalRequests) * 100; } /** * Update top keys */ updateTopKeys(key) { const existing = this.analytics.topKeys.find(k => k.key === key); if (existing) { existing.count++; } else { this.analytics.topKeys.push({ key, count: 1 }); } // Keep only top 10 keys this.analytics.topKeys.sort((a, b) => b.count - a.count); this.analytics.topKeys = this.analytics.topKeys.slice(0, 10); } /** * Trigger sync event */ triggerSyncEvent(type, key, data) { const event = { type: type, key, data, timestamp: new Date(), sourceNode: process.pid.toString() }; this.syncEvents.push(event); // Keep only recent events if (this.syncEvents.length > 1000) { this.syncEvents = this.syncEvents.slice(-1000); } } /** * Sync cache */ async syncCache() { // In a real implementation, you'd sync with other nodes // This is a simplified version for demonstration } /** * Execute warming strategy */ async executeWarmingStrategy(strategy) { // In a real implementation, you'd execute the strategy // This is a simplified version for demonstration console.log(`Executing warming strategy: ${strategy.name}`); } /** * Get cache filename */ getCacheFilename(key) { if (!this.config.file) { throw new Error('File cache not configured'); } const hash = this.hashString(key); const subdir = this.config.file.enableSubdirectories ? hash.substr(0, 2) : ''; const filename = `${hash}${this.config.file.extension}`; return subdir ? `${this.config.file.directory}/${subdir}/${filename}` : `${this.config.file.directory}/${filename}`; } /** * Hash string */ hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(36); } /** * Cleanup analytics */ cleanupAnalytics() { const cutoffDate = new Date(Date.now() - this.config.analyticsRetention * 24 * 60 * 60 * 1000); this.syncEvents = this.syncEvents.filter(event => event.timestamp > cutoffDate); } /** * Cleanup Redis cache */ async cleanupRedisCache() { // In a real implementation, you'd use Redis EXPIRE or similar } /** * Cleanup file cache */ async cleanupFileCache() { // In a real implementation, you'd scan and remove expired files } /** * Cleanup hybrid cache */ async cleanupHybridCache() { await Promise.all([ this.cleanupRedisCache(), this.cleanupFileCache() ]); } } //# sourceMappingURL=cache-manager.js.map