Skip to main content
Glama
cacheManager.js8.84 kB
import Database from 'better-sqlite3'; import NodeCache from 'node-cache'; import { createHash } from 'crypto'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import fs from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class CacheManager { constructor(options = {}) { this.cacheDir = options.cacheDir || join(__dirname, '../../.cache'); this.memoryCache = new NodeCache({ stdTTL: options.memoryTTL || 300, // 5 minutes default checkperiod: 120 }); // Ensure cache directory exists if (!fs.existsSync(this.cacheDir)) { fs.mkdirSync(this.cacheDir, { recursive: true }); } // Initialize SQLite database this.db = new Database(join(this.cacheDir, 'flutter_mcp.db')); this.initDatabase(); // TTL configurations (in seconds) this.ttlConfig = { widgetAnalysis: options.widgetAnalysisTTL || 86400, // 24 hours pubPackage: options.pubPackageTTL || 43200, // 12 hours flutterDocs: options.flutterDocsTTL || 86400, // 24 hours performance: options.performanceTTL || 3600, // 1 hour default: options.defaultTTL || 7200 // 2 hours }; // Cache statistics this.stats = { hits: 0, misses: 0, writes: 0, evictions: 0 }; } initDatabase() { // Create cache table if not exists this.db.exec(` CREATE TABLE IF NOT EXISTS cache ( key TEXT PRIMARY KEY, value TEXT, type TEXT, created_at INTEGER, expires_at INTEGER, hit_count INTEGER DEFAULT 0, last_accessed INTEGER, size INTEGER ); CREATE INDEX IF NOT EXISTS idx_expires_at ON cache(expires_at); CREATE INDEX IF NOT EXISTS idx_type ON cache(type); CREATE INDEX IF NOT EXISTS idx_last_accessed ON cache(last_accessed); `); // Create metadata table for cache statistics this.db.exec(` CREATE TABLE IF NOT EXISTS cache_metadata ( id INTEGER PRIMARY KEY AUTOINCREMENT, total_hits INTEGER DEFAULT 0, total_misses INTEGER DEFAULT 0, total_writes INTEGER DEFAULT 0, total_evictions INTEGER DEFAULT 0, last_cleanup INTEGER ); INSERT OR IGNORE INTO cache_metadata (id) VALUES (1); `); // Clean up expired entries on startup this.cleanup(); } generateKey(type, params) { const keyData = JSON.stringify({ type, ...params }); return createHash('sha256').update(keyData).digest('hex'); } async get(type, params) { const key = this.generateKey(type, params); // Check memory cache first const memoryResult = this.memoryCache.get(key); if (memoryResult) { this.stats.hits++; this.updateStats('hits'); return memoryResult; } // Check database cache const stmt = this.db.prepare(` SELECT value, expires_at FROM cache WHERE key = ? AND expires_at > ? `); const now = Date.now(); const row = stmt.get(key, now); if (row) { // Update hit count and last accessed this.db.prepare(` UPDATE cache SET hit_count = hit_count + 1, last_accessed = ? WHERE key = ? `).run(now, key); const value = JSON.parse(row.value); // Store in memory cache for faster subsequent access this.memoryCache.set(key, value, 300); // 5 minutes in memory this.stats.hits++; this.updateStats('hits'); return value; } this.stats.misses++; this.updateStats('misses'); return null; } async set(type, params, value) { const key = this.generateKey(type, params); const ttl = this.ttlConfig[type] || this.ttlConfig.default; const now = Date.now(); const expiresAt = now + (ttl * 1000); const valueStr = JSON.stringify(value); const size = Buffer.byteLength(valueStr); // Store in memory cache this.memoryCache.set(key, value, ttl); // Store in database const stmt = this.db.prepare(` INSERT OR REPLACE INTO cache (key, value, type, created_at, expires_at, hit_count, last_accessed, size) VALUES (?, ?, ?, ?, ?, 0, ?, ?) `); stmt.run(key, valueStr, type, now, expiresAt, now, size); this.stats.writes++; this.updateStats('writes'); // Trigger cleanup if needed if (this.stats.writes % 100 === 0) { this.cleanup(); } } async invalidate(type, params = null) { if (params) { // Invalidate specific entry const key = this.generateKey(type, params); this.memoryCache.del(key); this.db.prepare('DELETE FROM cache WHERE key = ?').run(key); } else { // Invalidate all entries of a type this.memoryCache.flushAll(); this.db.prepare('DELETE FROM cache WHERE type = ?').run(type); } } cleanup() { const now = Date.now(); // Remove expired entries const result = this.db.prepare('DELETE FROM cache WHERE expires_at < ?').run(now); const evicted = result.changes; if (evicted > 0) { this.stats.evictions += evicted; this.updateStats('evictions', evicted); } // Update last cleanup time this.db.prepare('UPDATE cache_metadata SET last_cleanup = ? WHERE id = 1').run(now); // If cache is too large, remove least recently used entries const maxSize = 100 * 1024 * 1024; // 100MB const totalSize = this.db.prepare('SELECT SUM(size) as total FROM cache').get().total || 0; if (totalSize > maxSize) { // Remove least recently used entries until we're under the limit const stmt = this.db.prepare(` DELETE FROM cache WHERE key IN ( SELECT key FROM cache ORDER BY last_accessed ASC LIMIT 100 ) `); while (totalSize > maxSize * 0.8) { // Keep 80% capacity const result = stmt.run(); this.stats.evictions += result.changes; this.updateStats('evictions', result.changes); } } } getStats() { // Get stats from database const dbStats = this.db.prepare( 'SELECT * FROM cache_metadata WHERE id = 1' ).get(); // Get cache size info const cacheInfo = this.db.prepare(` SELECT COUNT(*) as entries, SUM(size) as total_size, AVG(hit_count) as avg_hits, MAX(hit_count) as max_hits FROM cache `).get(); // Get type distribution const typeStats = this.db.prepare(` SELECT type, COUNT(*) as count, SUM(size) as size FROM cache GROUP BY type `).all(); return { memory: { hits: this.stats.hits, misses: this.stats.misses, writes: this.stats.writes, evictions: this.stats.evictions }, database: { totalHits: dbStats.total_hits, totalMisses: dbStats.total_misses, totalWrites: dbStats.total_writes, totalEvictions: dbStats.total_evictions, lastCleanup: new Date(dbStats.last_cleanup).toISOString() }, cache: { entries: cacheInfo.entries, totalSize: this.formatBytes(cacheInfo.total_size || 0), avgHits: Math.round(cacheInfo.avg_hits || 0), maxHits: cacheInfo.max_hits || 0 }, types: typeStats.map(t => ({ type: t.type, count: t.count, size: this.formatBytes(t.size || 0) })), hitRate: this.calculateHitRate() }; } calculateHitRate() { const total = this.stats.hits + this.stats.misses; if (total === 0) return 0; return Math.round((this.stats.hits / total) * 100); } formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } updateStats(type, count = 1) { const stmt = this.db.prepare(` UPDATE cache_metadata SET total_${type} = total_${type} + ? WHERE id = 1 `); stmt.run(count); } // Decorator for automatic caching withCache(type, fn) { return async (params) => { // Try to get from cache const cached = await this.get(type, params); if (cached) { return cached; } // Execute function and cache result const result = await fn(params); if (result) { await this.set(type, params, result); } return result; }; } close() { this.memoryCache.flushAll(); this.db.close(); } } // Singleton instance let cacheInstance = null; export function getCacheManager(options) { if (!cacheInstance) { cacheInstance = new CacheManager(options); } return cacheInstance; }

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/dvillegastech/flutter_mcp_2'

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