UNPKG

quality-mcp

Version:

An MCP server that analyzes to your codebase, with plugin support for DCD and Simian. 🏍️ "The only Zen you find on the tops of mountains is the Zen you bring up there."

317 lines (269 loc) 7.56 kB
/** * Analysis Cache * Simple file-based cache for Simian analysis results */ // fs is used globally, not imported import { join } from 'path'; import { createLogger } from '../../utils/logger.js'; const logger = createLogger('simian-cache'); /** * Dependency injection for testability * @returns {Object} Dependencies */ function getDeps() { return { join, logger, Date, JSON, Promise, }; } /** * Simple file-based cache for analysis results */ export class AnalysisCache { constructor(config) { this.config = config; this.cacheDir = config.directory; this.ttl = config.ttl * 1000; // Convert to milliseconds this.enabled = config.enabled; } async initialize(_getDeps = getDeps) { const { logger } = _getDeps(); if (!this.enabled) { logger.info('Cache disabled'); return; } try { // Create cache directory if it doesn't exist if (!global.fs.existsSync(this.cacheDir)) { await global.fs.mkdir(this.cacheDir, { recursive: true }); logger.info(`Created cache directory: ${this.cacheDir}`); } // Clean up expired entries on startup await this.cleanup(); logger.info('Analysis cache initialized'); } catch (error) { logger.error('Failed to initialize cache:', error); this.enabled = false; } } /** * Get cached analysis result * @param {string} key - Cache key * @returns {Promise<Object|null>} Cached result or null if not found/expired */ async get(key, _getDeps = getDeps) { const { logger, Date, JSON } = _getDeps(); if (!this.enabled) { return null; } try { const filePath = this.getFilePath(key); if (!global.fs.existsSync(filePath)) { return null; } const data = await global.fs.readFile(filePath, 'utf-8'); const cached = JSON.parse(data); // Check if expired if (Date.now() - cached.timestamp > this.ttl) { logger.debug(`Cache entry expired for key: ${key}`); await this.delete(key); return null; } logger.debug(`Cache hit for key: ${key}`); return cached.data; } catch (error) { logger.warn(`Failed to read cache for key ${key}:`, error.message); return null; } } /** * Store analysis result in cache * @param {string} key - Cache key * @param {Object} data - Data to cache * @returns {Promise<void>} */ async set(key, data, _getDeps = getDeps) { const { logger, Date, JSON } = _getDeps(); if (!this.enabled) { return; } try { const filePath = this.getFilePath(key); const cached = { timestamp: Date.now(), data, }; await global.fs.writeFile(filePath, JSON.stringify(cached, null, 2)); logger.debug(`Cached result for key: ${key}`); } catch (error) { logger.warn(`Failed to write cache for key ${key}:`, error.message); } } /** * Delete cached entry * @param {string} key - Cache key * @returns {Promise<void>} */ async delete(key, _getDeps = getDeps) { const { existsSync, logger } = _getDeps(); if (!this.enabled) { return; } try { const filePath = this.getFilePath(key); if (existsSync(filePath)) { await global.fs.unlink(filePath); logger.debug(`Deleted cache entry for key: ${key}`); } } catch (error) { logger.warn(`Failed to delete cache for key ${key}:`, error.message); } } /** * Clear all cached entries * @returns {Promise<void>} */ async clear(_getDeps = getDeps) { const { join, logger, Promise } = _getDeps(); if (!this.enabled) { return; } try { const files = await global.fs.readdir(this.cacheDir); const deletePromises = files .filter(file => { return file.endsWith('.json'); }) .map(file => { return global.fs.unlink(join(this.cacheDir, file)); }); await Promise.all(deletePromises); logger.info(`Cleared ${deletePromises.length} cache entries`); } catch (error) { logger.warn('Failed to clear cache:', error.message); } } /** * Clean up expired cache entries * @returns {Promise<void>} */ async cleanup(_getDeps = getDeps) { const { join, logger, Date, JSON } = _getDeps(); if (!this.enabled) { return; } try { const files = await global.fs.readdir(this.cacheDir); let deletedCount = 0; for (const file of files) { if (!file.endsWith('.json')) { continue; } const filePath = join(this.cacheDir, file); try { const data = await global.fs.readFile(filePath, 'utf-8'); const cached = JSON.parse(data); if (Date.now() - cached.timestamp > this.ttl) { await global.fs.unlink(filePath); deletedCount++; } } catch { // Delete corrupted cache files await global.fs.unlink(filePath); deletedCount++; } } if (deletedCount > 0) { logger.info(`Cleaned up ${deletedCount} expired cache entries`); } } catch (error) { logger.warn('Failed to cleanup cache:', error.message); } } /** * Get cache statistics * @returns {Promise<Object>} Cache statistics */ async getStats(_getDeps = getDeps) { const { join, logger, Date, JSON } = _getDeps(); if (!this.enabled) { return { enabled: false, totalEntries: 0, totalSize: 0, }; } try { const files = await global.fs.readdir(this.cacheDir); const cacheFiles = files.filter(file => { return file.endsWith('.json'); }); let totalSize = 0; let validEntries = 0; let expiredEntries = 0; for (const file of cacheFiles) { const filePath = join(this.cacheDir, file); const stats = await global.fs.stat(filePath); totalSize += stats.size; try { const data = await global.fs.readFile(filePath, 'utf-8'); const cached = JSON.parse(data); if (Date.now() - cached.timestamp > this.ttl) { expiredEntries++; } else { validEntries++; } } catch { expiredEntries++; } } return { enabled: true, totalEntries: cacheFiles.length, validEntries, expiredEntries, totalSize, directory: this.cacheDir, ttl: this.ttl / 1000, // Convert back to seconds }; } catch (error) { logger.warn('Failed to get cache stats:', error.message); return { enabled: false, error: error.message, }; } } /** * Get file path for cache key * @param {string} key - Cache key * @returns {string} File path */ getFilePath(key, _getDeps = getDeps) { const { join } = _getDeps(); // Sanitize key for filename const sanitized = key.replace(/[^a-zA-Z0-9]/g, '_'); return join(this.cacheDir, `${sanitized}.json`); } /** * Check if cache is enabled and working * @returns {boolean} Cache status */ isEnabled() { return this.enabled; } /** * Shutdown cache (cleanup if needed) * @returns {Promise<void>} */ async shutdown(_getDeps = getDeps) { const { logger } = _getDeps(); if (this.enabled) { logger.info('Shutting down analysis cache'); // Could perform final cleanup here if needed } } }