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
JavaScript
/**
* 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
}
}
}