Skip to main content
Glama
sascodiego

MCP Vibe Coding Knowledge Graph

by sascodiego
CacheInvalidationManager.js35.7 kB
/** * CONTEXT: Intelligent cache invalidation with dependency tracking and strategies * REASON: Ensure cache consistency while maximizing cache efficiency * CHANGE: Advanced invalidation strategies with graph-based dependency tracking * PREVENTION: Stale cache data, inconsistent state, performance degradation */ import { EventEmitter } from 'events'; import { logger } from '../utils/logger.js'; export class CacheInvalidationManager extends EventEmitter { constructor(cache, config = {}) { super(); this.cache = cache; this.config = { // Invalidation strategies strategies: { timeBasedInvalidation: config.strategies?.timeBasedInvalidation !== false, dependencyBasedInvalidation: config.strategies?.dependencyBasedInvalidation !== false, versionBasedInvalidation: config.strategies?.versionBasedInvalidation !== false, eventBasedInvalidation: config.strategies?.eventBasedInvalidation !== false, patternBasedInvalidation: config.strategies?.patternBasedInvalidation !== false }, // Dependency tracking dependencyTracking: { enabled: config.dependencyTracking?.enabled !== false, maxDepth: config.dependencyTracking?.maxDepth || 5, trackReverse: config.dependencyTracking?.trackReverse !== false, autoDetectDependencies: config.dependencyTracking?.autoDetectDependencies || false }, // Invalidation policies policies: { cascadeInvalidation: config.policies?.cascadeInvalidation !== false, batchInvalidation: config.policies?.batchInvalidation !== false, delayedInvalidation: config.policies?.delayedInvalidation || false, smartInvalidation: config.policies?.smartInvalidation !== false }, // Performance settings performance: { batchSize: config.performance?.batchSize || 100, invalidationDelay: config.performance?.invalidationDelay || 0, maxConcurrentInvalidations: config.performance?.maxConcurrentInvalidations || 10, enableMetrics: config.performance?.enableMetrics !== false } }; // Dependency graph this.dependencyGraph = new DependencyGraph(); // Version tracking this.versions = new Map(); // Invalidation queue this.invalidationQueue = []; this.isProcessingQueue = false; // Event subscriptions this.eventSubscriptions = new Map(); // Pattern matchers this.patternMatchers = new Map(); // Metrics this.metrics = { invalidations: { total: 0, byStrategy: { time: 0, dependency: 0, version: 0, event: 0, pattern: 0, manual: 0 }, byType: { single: 0, cascade: 0, batch: 0, pattern: 0 } }, dependencies: { totalTracked: 0, averageDepth: 0, maxDepth: 0 }, performance: { averageInvalidationTime: 0, totalInvalidationTime: 0, queueLength: 0, maxQueueLength: 0 } }; // Cleanup intervals this.cleanupIntervals = []; this.initialize(); } /** * Initialize the invalidation manager */ initialize() { // Start background tasks this.startBackgroundTasks(); // Setup cache event listeners this.setupCacheEventListeners(); logger.info('Cache invalidation manager initialized', { strategies: Object.keys(this.config.strategies).filter(k => this.config.strategies[k]), dependencyTracking: this.config.dependencyTracking.enabled }); } /** * Register a dependency between cache keys */ registerDependency(dependentKey, dependsOnKey, metadata = {}) { try { this.dependencyGraph.addDependency(dependentKey, dependsOnKey, metadata); this.metrics.dependencies.totalTracked++; // Update metrics const depth = this.dependencyGraph.getDependencyDepth(dependentKey); this.metrics.dependencies.averageDepth = (this.metrics.dependencies.averageDepth + depth) / 2; this.metrics.dependencies.maxDepth = Math.max(this.metrics.dependencies.maxDepth, depth); this.emit('dependencyRegistered', { dependentKey, dependsOnKey, metadata }); logger.debug('Dependency registered', { dependentKey, dependsOnKey, metadata }); } catch (error) { logger.error('Failed to register dependency:', { dependentKey, dependsOnKey, error: error.message }); throw error; } } /** * Remove a dependency */ removeDependency(dependentKey, dependsOnKey) { try { this.dependencyGraph.removeDependency(dependentKey, dependsOnKey); this.metrics.dependencies.totalTracked--; this.emit('dependencyRemoved', { dependentKey, dependsOnKey }); logger.debug('Dependency removed', { dependentKey, dependsOnKey }); } catch (error) { logger.error('Failed to remove dependency:', { dependentKey, dependsOnKey, error: error.message }); throw error; } } /** * Register a pattern-based invalidation rule */ registerPatternRule(name, pattern, targetPattern, options = {}) { const rule = { pattern: new RegExp(pattern), targetPattern: new RegExp(targetPattern), options: { cascadeDepth: options.cascadeDepth || 1, excludePattern: options.excludePattern ? new RegExp(options.excludePattern) : null, delayMs: options.delayMs || 0, onlyIfExists: options.onlyIfExists || false } }; this.patternMatchers.set(name, rule); logger.debug('Pattern rule registered', { name, pattern, targetPattern, options }); } /** * Remove a pattern rule */ removePatternRule(name) { this.patternMatchers.delete(name); logger.debug('Pattern rule removed', { name }); } /** * Subscribe to cache events for invalidation */ subscribeToEvent(eventName, invalidationCallback) { if (!this.eventSubscriptions.has(eventName)) { this.eventSubscriptions.set(eventName, new Set()); } this.eventSubscriptions.get(eventName).add(invalidationCallback); logger.debug('Event subscription added', { eventName }); } /** * Unsubscribe from cache events */ unsubscribeFromEvent(eventName, invalidationCallback) { const subscribers = this.eventSubscriptions.get(eventName); if (subscribers) { subscribers.delete(invalidationCallback); if (subscribers.size === 0) { this.eventSubscriptions.delete(eventName); } } logger.debug('Event subscription removed', { eventName }); } /** * Set version for a cache key */ setVersion(key, version) { this.versions.set(key, version); logger.debug('Version set', { key, version }); } /** * Get version for a cache key */ getVersion(key) { return this.versions.get(key); } /** * Invalidate cache key(s) using various strategies */ async invalidate(keys, strategy = 'manual', options = {}) { const startTime = Date.now(); try { // Normalize keys to array const keyArray = Array.isArray(keys) ? keys : [keys]; logger.debug('Starting cache invalidation', { keys: keyArray, strategy, options }); // Validate keys for (const key of keyArray) { this.validateKey(key); } // Determine invalidation type const invalidationType = this.determineInvalidationType(keyArray, options); // Execute invalidation based on strategy let invalidatedKeys = []; switch (strategy) { case 'dependency': invalidatedKeys = await this.invalidateByDependency(keyArray, options); break; case 'pattern': invalidatedKeys = await this.invalidateByPattern(keyArray, options); break; case 'version': invalidatedKeys = await this.invalidateByVersion(keyArray, options); break; case 'time': invalidatedKeys = await this.invalidateByTime(keyArray, options); break; case 'event': invalidatedKeys = await this.invalidateByEvent(keyArray, options); break; default: // manual invalidatedKeys = await this.invalidateManual(keyArray, options); break; } // Update metrics const invalidationTime = Date.now() - startTime; this.updateMetrics(strategy, invalidationType, invalidatedKeys.length, invalidationTime); this.emit('invalidationCompleted', { keys: keyArray, invalidatedKeys, strategy, invalidationType, time: invalidationTime }); logger.info('Cache invalidation completed', { requestedKeys: keyArray.length, invalidatedKeys: invalidatedKeys.length, strategy, time: `${invalidationTime}ms` }); return invalidatedKeys; } catch (error) { logger.error('Cache invalidation failed:', { keys, strategy, error: error.message }); throw error; } } /** * Invalidate by dependency relationships */ async invalidateByDependency(keys, options = {}) { const invalidatedKeys = new Set(); const processed = new Set(); for (const key of keys) { if (processed.has(key)) continue; // Get all dependent keys const dependentKeys = this.dependencyGraph.getDependents(key, { maxDepth: options.maxDepth || this.config.dependencyTracking.maxDepth, includeTransitive: options.includeTransitive !== false }); // Invalidate cascade if (this.config.policies.cascadeInvalidation) { for (const dependentKey of dependentKeys) { if (!processed.has(dependentKey)) { await this.invalidateSingle(dependentKey); invalidatedKeys.add(dependentKey); processed.add(dependentKey); } } } // Invalidate original key await this.invalidateSingle(key); invalidatedKeys.add(key); processed.add(key); } this.metrics.invalidations.byStrategy.dependency++; return Array.from(invalidatedKeys); } /** * Invalidate by pattern matching */ async invalidateByPattern(keys, options = {}) { const invalidatedKeys = new Set(); for (const key of keys) { // Apply pattern rules for (const [ruleName, rule] of this.patternMatchers) { if (rule.pattern.test(key)) { // Find matching keys in cache const allKeys = await this.getAllCacheKeys(); const matchingKeys = allKeys.filter(cacheKey => rule.targetPattern.test(cacheKey) && (!rule.options.excludePattern || !rule.options.excludePattern.test(cacheKey)) ); // Apply delay if specified if (rule.options.delayMs > 0) { setTimeout(async () => { for (const matchingKey of matchingKeys) { await this.invalidateSingle(matchingKey); invalidatedKeys.add(matchingKey); } }, rule.options.delayMs); } else { for (const matchingKey of matchingKeys) { await this.invalidateSingle(matchingKey); invalidatedKeys.add(matchingKey); } } logger.debug('Pattern rule applied', { ruleName, triggerKey: key, matchingKeys: matchingKeys.length }); } } // Invalidate original key await this.invalidateSingle(key); invalidatedKeys.add(key); } this.metrics.invalidations.byStrategy.pattern++; return Array.from(invalidatedKeys); } /** * Invalidate by version comparison */ async invalidateByVersion(keys, options = {}) { const invalidatedKeys = []; for (const key of keys) { const currentVersion = this.versions.get(key); const requiredVersion = options.version || options.versions?.[key]; if (requiredVersion && currentVersion !== requiredVersion) { await this.invalidateSingle(key); invalidatedKeys.push(key); // Update version if (options.updateVersion !== false) { this.versions.set(key, requiredVersion); } } } this.metrics.invalidations.byStrategy.version++; return invalidatedKeys; } /** * Invalidate by time-based rules */ async invalidateByTime(keys, options = {}) { const invalidatedKeys = []; const now = Date.now(); for (const key of keys) { let shouldInvalidate = false; if (options.olderThan) { // Check if cache entry is older than specified time const cacheEntry = await this.getCacheEntryMetadata(key); if (cacheEntry && (now - cacheEntry.created) > options.olderThan) { shouldInvalidate = true; } } else if (options.expiredOnly) { // Only invalidate if already expired const cacheEntry = await this.getCacheEntryMetadata(key); if (cacheEntry && cacheEntry.expires && now > cacheEntry.expires) { shouldInvalidate = true; } } else { // Default: invalidate all shouldInvalidate = true; } if (shouldInvalidate) { await this.invalidateSingle(key); invalidatedKeys.push(key); } } this.metrics.invalidations.byStrategy.time++; return invalidatedKeys; } /** * Invalidate by event triggers */ async invalidateByEvent(keys, options = {}) { const invalidatedKeys = []; const eventName = options.eventName; if (eventName) { const subscribers = this.eventSubscriptions.get(eventName); if (subscribers) { for (const callback of subscribers) { try { const result = await callback(keys, options); if (result && Array.isArray(result)) { invalidatedKeys.push(...result); } } catch (error) { logger.error('Event invalidation callback error:', { eventName, error: error.message }); } } } } // Always invalidate the requested keys for (const key of keys) { await this.invalidateSingle(key); if (!invalidatedKeys.includes(key)) { invalidatedKeys.push(key); } } this.metrics.invalidations.byStrategy.event++; return invalidatedKeys; } /** * Manual invalidation (direct) */ async invalidateManual(keys, options = {}) { const invalidatedKeys = []; if (this.config.policies.batchInvalidation && keys.length > 1) { // Batch invalidation for better performance await this.invalidateBatch(keys); invalidatedKeys.push(...keys); } else { // Individual invalidation for (const key of keys) { await this.invalidateSingle(key); invalidatedKeys.push(key); } } this.metrics.invalidations.byStrategy.manual++; return invalidatedKeys; } /** * Invalidate a single cache key */ async invalidateSingle(key) { try { await this.cache.delete(key, { invalidateDependents: false }); // Remove from dependency graph this.dependencyGraph.removeNode(key); // Remove version tracking this.versions.delete(key); this.emit('keyInvalidated', { key }); } catch (error) { logger.error('Failed to invalidate single key:', { key, error: error.message }); throw error; } } /** * Batch invalidate multiple keys */ async invalidateBatch(keys) { const batchSize = this.config.performance.batchSize; const batches = []; // Split into batches for (let i = 0; i < keys.length; i += batchSize) { batches.push(keys.slice(i, i + batchSize)); } // Process batches with concurrency limit const semaphore = new Semaphore(this.config.performance.maxConcurrentInvalidations); const promises = batches.map(batch => semaphore.acquire().then(async (release) => { try { await Promise.all(batch.map(key => this.invalidateSingle(key))); } finally { release(); } }) ); await Promise.all(promises); } /** * Queue invalidation for delayed processing */ queueInvalidation(keys, strategy, options = {}) { const invalidationRequest = { keys: Array.isArray(keys) ? keys : [keys], strategy, options, timestamp: Date.now(), id: this.generateRequestId() }; this.invalidationQueue.push(invalidationRequest); this.metrics.performance.queueLength = this.invalidationQueue.length; this.metrics.performance.maxQueueLength = Math.max( this.metrics.performance.maxQueueLength, this.invalidationQueue.length ); // Process queue if not already processing if (!this.isProcessingQueue) { this.processInvalidationQueue(); } return invalidationRequest.id; } /** * Process the invalidation queue */ async processInvalidationQueue() { if (this.isProcessingQueue) return; this.isProcessingQueue = true; try { while (this.invalidationQueue.length > 0) { const request = this.invalidationQueue.shift(); this.metrics.performance.queueLength = this.invalidationQueue.length; try { await this.invalidate(request.keys, request.strategy, request.options); } catch (error) { logger.error('Queued invalidation failed:', { requestId: request.id, error: error.message }); } // Add delay if configured if (this.config.performance.invalidationDelay > 0) { await this.delay(this.config.performance.invalidationDelay); } } } finally { this.isProcessingQueue = false; } } /** * Setup cache event listeners */ setupCacheEventListeners() { // Listen for cache set events to detect dependencies this.cache.on('set', ({ key }) => { if (this.config.dependencyTracking.autoDetectDependencies) { this.autoDetectDependencies(key); } }); // Listen for cache operations for metrics this.cache.on('get', ({ key, hit }) => { if (hit && this.config.strategies.timeBasedInvalidation) { this.checkTimeBasedInvalidation(key); } }); } /** * Auto-detect dependencies based on cache access patterns */ autoDetectDependencies(key) { // Implement heuristic-based dependency detection // This could analyze key patterns, access sequences, etc. // Example: if key contains a file path, create dependency on directory if (key.includes('/')) { const parentPath = key.substring(0, key.lastIndexOf('/')); if (parentPath) { this.registerDependency(key, parentPath, { type: 'auto-detected', pattern: 'path-hierarchy' }); } } // Example: version-based dependencies const versionMatch = key.match(/(.*):v(\d+)/); if (versionMatch) { const baseKey = versionMatch[1]; this.registerDependency(key, baseKey, { type: 'auto-detected', pattern: 'version' }); } } /** * Check time-based invalidation rules */ async checkTimeBasedInvalidation(key) { // Implement time-based invalidation logic // This could check TTL, last access time, etc. } /** * Start background maintenance tasks */ startBackgroundTasks() { // Periodic cleanup of expired dependencies const cleanupInterval = setInterval(() => { this.cleanupExpiredDependencies().catch(error => { logger.error('Dependency cleanup error:', error); }); }, 5 * 60 * 1000); // Every 5 minutes this.cleanupIntervals.push(cleanupInterval); // Periodic metrics update const metricsInterval = setInterval(() => { this.updatePerformanceMetrics(); }, 60 * 1000); // Every minute this.cleanupIntervals.push(metricsInterval); // Process invalidation queue periodically const queueInterval = setInterval(() => { if (!this.isProcessingQueue && this.invalidationQueue.length > 0) { this.processInvalidationQueue(); } }, 10 * 1000); // Every 10 seconds this.cleanupIntervals.push(queueInterval); } /** * Cleanup expired dependencies */ async cleanupExpiredDependencies() { const expiredDependencies = this.dependencyGraph.getExpiredDependencies(); for (const { dependentKey, dependsOnKey } of expiredDependencies) { this.removeDependency(dependentKey, dependsOnKey); } if (expiredDependencies.length > 0) { logger.debug(`Cleaned up ${expiredDependencies.length} expired dependencies`); } } /** * Update performance metrics */ updatePerformanceMetrics() { // Calculate averages and update metrics this.emit('metricsUpdated', this.metrics); } /** * Utility methods */ determineInvalidationType(keys, options) { if (keys.length === 1) return 'single'; if (this.config.policies.batchInvalidation) return 'batch'; if (options.pattern) return 'pattern'; if (this.config.policies.cascadeInvalidation) return 'cascade'; return 'multiple'; } async getAllCacheKeys() { // This would need to be implemented based on the cache implementation // For now, return an empty array return []; } async getCacheEntryMetadata(key) { // This would need to be implemented based on the cache implementation return null; } validateKey(key) { if (typeof key !== 'string' || key.length === 0) { throw new Error('Invalid cache key'); } } generateRequestId() { return `inv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } updateMetrics(strategy, type, count, time) { this.metrics.invalidations.total += count; this.metrics.invalidations.byStrategy[strategy] += count; this.metrics.invalidations.byType[type] += count; this.metrics.performance.totalInvalidationTime += time; this.metrics.performance.averageInvalidationTime = this.metrics.performance.totalInvalidationTime / this.metrics.invalidations.total; } /** * Get invalidation statistics */ getStatistics() { return { ...this.metrics, dependencyGraph: this.dependencyGraph.getStatistics(), queueStatus: { length: this.invalidationQueue.length, isProcessing: this.isProcessingQueue } }; } /** * Shutdown the invalidation manager */ async shutdown() { logger.info('Shutting down cache invalidation manager'); // Process remaining queue if (this.invalidationQueue.length > 0) { logger.info(`Processing ${this.invalidationQueue.length} remaining invalidation requests`); await this.processInvalidationQueue(); } // Clear intervals for (const interval of this.cleanupIntervals) { clearInterval(interval); } // Clear data structures this.dependencyGraph.clear(); this.versions.clear(); this.eventSubscriptions.clear(); this.patternMatchers.clear(); this.invalidationQueue.length = 0; this.emit('shutdown'); logger.info('Cache invalidation manager shutdown completed'); } } /** * Dependency graph for tracking cache key relationships */ class DependencyGraph { constructor() { this.dependencies = new Map(); // key -> Set of keys it depends on this.dependents = new Map(); // key -> Set of keys that depend on it this.metadata = new Map(); // edge metadata } addDependency(dependentKey, dependsOnKey, metadata = {}) { // Add to dependencies if (!this.dependencies.has(dependentKey)) { this.dependencies.set(dependentKey, new Set()); } this.dependencies.get(dependentKey).add(dependsOnKey); // Add to dependents if (!this.dependents.has(dependsOnKey)) { this.dependents.set(dependsOnKey, new Set()); } this.dependents.get(dependsOnKey).add(dependentKey); // Store metadata const edgeKey = `${dependentKey}->${dependsOnKey}`; this.metadata.set(edgeKey, { ...metadata, created: Date.now() }); } removeDependency(dependentKey, dependsOnKey) { // Remove from dependencies const deps = this.dependencies.get(dependentKey); if (deps) { deps.delete(dependsOnKey); if (deps.size === 0) { this.dependencies.delete(dependentKey); } } // Remove from dependents const dependents = this.dependents.get(dependsOnKey); if (dependents) { dependents.delete(dependentKey); if (dependents.size === 0) { this.dependents.delete(dependsOnKey); } } // Remove metadata const edgeKey = `${dependentKey}->${dependsOnKey}`; this.metadata.delete(edgeKey); } removeNode(key) { // Remove all dependencies for this key const deps = this.dependencies.get(key); if (deps) { for (const dep of deps) { this.removeDependency(key, dep); } } // Remove all dependents of this key const dependents = this.dependents.get(key); if (dependents) { for (const dependent of dependents) { this.removeDependency(dependent, key); } } } getDependents(key, options = {}) { const visited = new Set(); const result = new Set(); const maxDepth = options.maxDepth || Infinity; const traverse = (currentKey, depth) => { if (depth >= maxDepth || visited.has(currentKey)) { return; } visited.add(currentKey); const dependents = this.dependents.get(currentKey); if (dependents) { for (const dependent of dependents) { result.add(dependent); if (options.includeTransitive !== false) { traverse(dependent, depth + 1); } } } }; traverse(key, 0); return Array.from(result); } getDependencies(key, options = {}) { const visited = new Set(); const result = new Set(); const maxDepth = options.maxDepth || Infinity; const traverse = (currentKey, depth) => { if (depth >= maxDepth || visited.has(currentKey)) { return; } visited.add(currentKey); const deps = this.dependencies.get(currentKey); if (deps) { for (const dep of deps) { result.add(dep); if (options.includeTransitive !== false) { traverse(dep, depth + 1); } } } }; traverse(key, 0); return Array.from(result); } getDependencyDepth(key) { const visited = new Set(); let maxDepth = 0; const traverse = (currentKey, depth) => { if (visited.has(currentKey)) { return depth; } visited.add(currentKey); maxDepth = Math.max(maxDepth, depth); const deps = this.dependencies.get(currentKey); if (deps) { for (const dep of deps) { traverse(dep, depth + 1); } } return depth; }; traverse(key, 0); return maxDepth; } getExpiredDependencies() { const expired = []; const now = Date.now(); for (const [edgeKey, metadata] of this.metadata.entries()) { if (metadata.expires && now > metadata.expires) { const [dependentKey, dependsOnKey] = edgeKey.split('->'); expired.push({ dependentKey, dependsOnKey, metadata }); } } return expired; } getStatistics() { return { totalDependencies: this.dependencies.size, totalDependents: this.dependents.size, totalEdges: this.metadata.size, averageDependencies: this.dependencies.size > 0 ? Array.from(this.dependencies.values()).reduce((sum, deps) => sum + deps.size, 0) / this.dependencies.size : 0, averageDependents: this.dependents.size > 0 ? Array.from(this.dependents.values()).reduce((sum, deps) => sum + deps.size, 0) / this.dependents.size : 0 }; } clear() { this.dependencies.clear(); this.dependents.clear(); this.metadata.clear(); } } /** * Simple semaphore for concurrency control */ class Semaphore { constructor(max) { this.max = max; this.current = 0; this.queue = []; } async acquire() { return new Promise((resolve) => { if (this.current < this.max) { this.current++; resolve(() => this.release()); } else { this.queue.push(() => { this.current++; resolve(() => this.release()); }); } }); } release() { this.current--; if (this.queue.length > 0) { const next = this.queue.shift(); next(); } } } export default CacheInvalidationManager;

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/sascodiego/KGsMCP'

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