Skip to main content
Glama

Automation Script Generator MCP Server

cache-manager.js•14.9 kB
/** * Cache Manager Module * Provides intelligent caching and performance optimization */ import fs from 'fs-extra'; import path from 'path'; import crypto from 'crypto'; export class CacheManager { constructor(config) { this.config = config; this.memoryCache = new Map(); this.diskCacheDir = path.join(process.cwd(), '.cache'); this.stats = { hits: 0, misses: 0, evictions: 0, diskReads: 0, diskWrites: 0 }; this.maxMemoryItems = config.get('performance.caching.maxMemoryItems') || 100; this.maxDiskItems = config.get('performance.caching.maxDiskItems') || 1000; this.ttl = config.get('performance.caching.ttl') || 3600000; // 1 hour default this.initializeDiskCache(); } async initializeDiskCache() { if (this.config.get('performance.caching.enableDiskCache')) { try { await fs.ensureDir(this.diskCacheDir); await this.cleanupExpiredDiskCache(); } catch (error) { console.warn('Failed to initialize disk cache:', error.message); } } } // Generate cache key from data generateKey(data) { const hash = crypto.createHash('sha256'); hash.update(JSON.stringify(data)); return hash.digest('hex').substring(0, 16); } // Get item from cache (memory first, then disk) async get(key) { // Check memory cache first const memoryItem = this.memoryCache.get(key); if (memoryItem && !this.isExpired(memoryItem)) { this.stats.hits++; return memoryItem.data; } // Remove expired memory item if (memoryItem && this.isExpired(memoryItem)) { this.memoryCache.delete(key); } // Check disk cache if enabled if (this.config.get('performance.caching.enableDiskCache')) { const diskItem = await this.getDiskCache(key); if (diskItem && !this.isExpired(diskItem)) { // Promote to memory cache this.setMemoryCache(key, diskItem.data, diskItem.timestamp); this.stats.hits++; this.stats.diskReads++; return diskItem.data; } } this.stats.misses++; return null; } // Set item in cache async set(key, data, customTtl = null) { const timestamp = Date.now(); const ttl = customTtl || this.ttl; // Always set in memory cache this.setMemoryCache(key, data, timestamp, ttl); // Set in disk cache if enabled and data is significant if (this.config.get('performance.caching.enableDiskCache') && this.shouldCacheToDisk(data)) { await this.setDiskCache(key, data, timestamp, ttl); } } // Memory cache operations setMemoryCache(key, data, timestamp = Date.now(), ttl = null) { // Evict oldest items if cache is full if (this.memoryCache.size >= this.maxMemoryItems) { this.evictOldestMemoryItems(); } this.memoryCache.set(key, { data, timestamp, ttl: ttl || this.ttl, accessCount: 0, lastAccess: timestamp }); } evictOldestMemoryItems() { const items = Array.from(this.memoryCache.entries()); // Sort by last access time (oldest first) items.sort((a, b) => a[1].lastAccess - b[1].lastAccess); // Remove oldest 25% of items const removeCount = Math.floor(this.maxMemoryItems * 0.25); for (let i = 0; i < removeCount && items.length > 0; i++) { this.memoryCache.delete(items[i][0]); this.stats.evictions++; } } // Disk cache operations async getDiskCache(key) { try { const filePath = this.getDiskCachePath(key); if (await fs.pathExists(filePath)) { const content = await fs.readFile(filePath, 'utf8'); return JSON.parse(content); } } catch (error) { console.warn(`Failed to read disk cache for key ${key}:`, error.message); } return null; } async setDiskCache(key, data, timestamp = Date.now(), ttl = null) { try { const filePath = this.getDiskCachePath(key); const cacheItem = { data, timestamp, ttl: ttl || this.ttl, key }; await fs.writeFile(filePath, JSON.stringify(cacheItem, null, 2)); this.stats.diskWrites++; // Clean up old disk cache periodically if (Math.random() < 0.1) { // 10% chance on each write setImmediate(() => this.cleanupExpiredDiskCache()); } } catch (error) { console.warn(`Failed to write disk cache for key ${key}:`, error.message); } } getDiskCachePath(key) { return path.join(this.diskCacheDir, `${key}.json`); } // Cache invalidation and cleanup async invalidate(pattern) { // Invalidate memory cache if (typeof pattern === 'string') { this.memoryCache.delete(pattern); } else if (pattern instanceof RegExp) { for (const key of this.memoryCache.keys()) { if (pattern.test(key)) { this.memoryCache.delete(key); } } } // Invalidate disk cache if (this.config.get('performance.caching.enableDiskCache')) { await this.invalidateDiskCache(pattern); } } async invalidateDiskCache(pattern) { try { const files = await fs.readdir(this.diskCacheDir); const jsonFiles = files.filter(f => f.endsWith('.json')); for (const file of jsonFiles) { const key = path.basename(file, '.json'); if (typeof pattern === 'string' && key === pattern) { await fs.remove(path.join(this.diskCacheDir, file)); } else if (pattern instanceof RegExp && pattern.test(key)) { await fs.remove(path.join(this.diskCacheDir, file)); } } } catch (error) { console.warn('Failed to invalidate disk cache:', error.message); } } async cleanupExpiredDiskCache() { try { const files = await fs.readdir(this.diskCacheDir); const jsonFiles = files.filter(f => f.endsWith('.json')); let cleanedCount = 0; for (const file of jsonFiles) { const filePath = path.join(this.diskCacheDir, file); try { const content = await fs.readFile(filePath, 'utf8'); const cacheItem = JSON.parse(content); if (this.isExpired(cacheItem)) { await fs.remove(filePath); cleanedCount++; } } catch (error) { // Remove corrupted cache files await fs.remove(filePath); cleanedCount++; } } // If we have too many files, remove oldest ones const remainingFiles = await fs.readdir(this.diskCacheDir); const remainingJsonFiles = remainingFiles.filter(f => f.endsWith('.json')); if (remainingJsonFiles.length > this.maxDiskItems) { const fileStats = await Promise.all( remainingJsonFiles.map(async f => { const filePath = path.join(this.diskCacheDir, f); const stat = await fs.stat(filePath); return { file: f, mtime: stat.mtime }; }) ); // Sort by modification time (oldest first) fileStats.sort((a, b) => a.mtime - b.mtime); const removeCount = remainingJsonFiles.length - this.maxDiskItems; for (let i = 0; i < removeCount; i++) { await fs.remove(path.join(this.diskCacheDir, fileStats[i].file)); cleanedCount++; } } if (cleanedCount > 0) { console.log(`Cleaned up ${cleanedCount} expired cache items`); } } catch (error) { console.warn('Failed to cleanup disk cache:', error.message); } } // Smart caching strategies shouldCacheToDisk(data) { const dataSize = JSON.stringify(data).length; const minSizeForDisk = this.config.get('performance.caching.minDiskCacheSize') || 1024; // 1KB return dataSize >= minSizeForDisk; } // Cache warming async warmCache(cacheKeys) { const warmingPromises = cacheKeys.map(async keyData => { try { const key = this.generateKey(keyData.input); const cachedData = await this.get(key); if (!cachedData && keyData.generator) { const data = await keyData.generator(keyData.input); await this.set(key, data); return { key, status: 'warmed' }; } else if (cachedData) { return { key, status: 'already_cached' }; } } catch (error) { console.warn(`Failed to warm cache for key:`, error.message); return { key: 'unknown', status: 'failed', error: error.message }; } }); const results = await Promise.all(warmingPromises); console.log('Cache warming completed:', results); return results; } // Utility methods isExpired(cacheItem) { return Date.now() - cacheItem.timestamp > cacheItem.ttl; } getStats() { const hitRate = this.stats.hits / (this.stats.hits + this.stats.misses) || 0; return { ...this.stats, hitRate: Math.round(hitRate * 100) / 100, memoryItems: this.memoryCache.size, memoryUsage: this.estimateMemoryUsage() }; } estimateMemoryUsage() { let totalSize = 0; for (const [key, value] of this.memoryCache.entries()) { totalSize += JSON.stringify({ key, value }).length; } return totalSize; } // Clear all caches async clear() { this.memoryCache.clear(); if (this.config.get('performance.caching.enableDiskCache')) { try { await fs.emptyDir(this.diskCacheDir); } catch (error) { console.warn('Failed to clear disk cache:', error.message); } } // Reset stats this.stats = { hits: 0, misses: 0, evictions: 0, diskReads: 0, diskWrites: 0 }; } // Configuration updates updateConfig(newConfig) { this.config = newConfig; this.maxMemoryItems = newConfig.get('performance.caching.maxMemoryItems') || 100; this.maxDiskItems = newConfig.get('performance.caching.maxDiskItems') || 1000; this.ttl = newConfig.get('performance.caching.ttl') || 3600000; } // Export cache for analysis async exportCache() { const memoryData = Array.from(this.memoryCache.entries()).map(([key, value]) => ({ key, ...value, size: JSON.stringify(value.data).length })); let diskData = []; if (this.config.get('performance.caching.enableDiskCache')) { try { const files = await fs.readdir(this.diskCacheDir); const jsonFiles = files.filter(f => f.endsWith('.json')); diskData = await Promise.all( jsonFiles.map(async file => { try { const filePath = path.join(this.diskCacheDir, file); const content = await fs.readFile(filePath, 'utf8'); const parsed = JSON.parse(content); const stat = await fs.stat(filePath); return { key: parsed.key, timestamp: parsed.timestamp, ttl: parsed.ttl, size: stat.size, expired: this.isExpired(parsed) }; } catch (error) { return { error: error.message, file }; } }) ); } catch (error) { console.warn('Failed to read disk cache for export:', error.message); } } return { memory: memoryData, disk: diskData, stats: this.getStats(), timestamp: new Date().toISOString() }; } } // Performance monitoring utilities export class PerformanceMonitor { constructor() { this.metrics = new Map(); this.startTimes = new Map(); } start(operationId) { this.startTimes.set(operationId, performance.now()); } end(operationId, metadata = {}) { const startTime = this.startTimes.get(operationId); if (startTime) { const duration = performance.now() - startTime; if (!this.metrics.has(operationId)) { this.metrics.set(operationId, []); } this.metrics.get(operationId).push({ duration, timestamp: Date.now(), metadata }); this.startTimes.delete(operationId); return duration; } return null; } getMetrics(operationId) { const measurements = this.metrics.get(operationId) || []; if (measurements.length === 0) return null; const durations = measurements.map(m => m.duration); return { count: measurements.length, average: durations.reduce((a, b) => a + b, 0) / durations.length, min: Math.min(...durations), max: Math.max(...durations), recent: measurements.slice(-10) // Last 10 measurements }; } getAllMetrics() { const results = {}; for (const [operationId, measurements] of this.metrics.entries()) { results[operationId] = this.getMetrics(operationId); } return results; } clear() { this.metrics.clear(); this.startTimes.clear(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/raymondsambur/automation-script-generator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server