UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

257 lines 8.84 kB
/** * Invalidation Engine for Intelligent Query Cache * * Manages cache invalidation strategies, dependency tracking, and * integration with the sync process to ensure data consistency. */ import { getLogger } from '../../../logging/Logger.js'; import EventEmitter from 'events'; export class InvalidationEngine extends EventEmitter { logger = getLogger(); cacheOrchestrator; // Dependency graph - which entities depend on which dependencies = new Map(); // Invalidation rules rules = new Map(); // Statistics stats = { totalInvalidations: 0, invalidationsByEntity: {}, cascadeInvalidations: 0, averageInvalidationsPerMinute: 0, }; // Track invalidation history for rate calculation invalidationHistory = []; historyWindowMs = 60000; // 1 minute constructor(cacheOrchestrator) { super(); this.cacheOrchestrator = cacheOrchestrator; this.setupDefaultRules(); this.logger.info('InvalidationEngine initialized'); } /** * Setup default invalidation rules and dependencies */ setupDefaultRules() { // Flag changes affect experiments that use them this.addRule({ entity: 'flags', dependsOn: [], invalidationPattern: 'iqe:flags:*', cascadeInvalidation: true }); // Experiment changes affect their results this.addRule({ entity: 'experiments', dependsOn: ['flags'], invalidationPattern: 'iqe:experiments:*', cascadeInvalidation: true }); // Audience changes affect flags and experiments that use them this.addRule({ entity: 'audiences', dependsOn: [], invalidationPattern: 'iqe:audiences:*', cascadeInvalidation: true }); // Event changes affect experiments that track them this.addRule({ entity: 'events', dependsOn: [], invalidationPattern: 'iqe:events:*', cascadeInvalidation: true }); // Project changes affect everything this.addRule({ entity: 'projects', dependsOn: [], invalidationPattern: 'iqe:*', cascadeInvalidation: true }); // Build dependency graph this.buildDependencyGraph(); } /** * Add an invalidation rule */ addRule(rule) { this.rules.set(rule.entity, rule); this.logger.debug(`Added invalidation rule for ${rule.entity}`); } /** * Build dependency graph from rules */ buildDependencyGraph() { this.dependencies.clear(); for (const [entity, rule] of this.rules) { if (!this.dependencies.has(entity)) { this.dependencies.set(entity, new Set()); } // Add reverse dependencies if (rule.dependsOn) { for (const dep of rule.dependsOn) { if (!this.dependencies.has(dep)) { this.dependencies.set(dep, new Set()); } this.dependencies.get(dep).add(entity); } } } this.logger.debug(`Dependency graph built: ${this.dependencies.size} entities`); } /** * Handle entity change event from sync process */ async handleEntityChange(event) { this.logger.info(`Handling entity change: ${event.type} for ${event.entity}`); // Update stats this.updateStats(event); // Emit event for monitoring this.emit('invalidation', event); // Get entities to invalidate const entitiesToInvalidate = this.getEntitiesToInvalidate(event.entity); let totalInvalidated = 0; // Invalidate primary entity const primaryPattern = this.getInvalidationPattern(event.entity, event.projectId); const primaryCount = await this.cacheOrchestrator.invalidate([primaryPattern]); totalInvalidated += primaryCount; this.logger.info(`Invalidated ${primaryCount} entries for ${event.entity}`); // Cascade invalidation if needed if (this.shouldCascade(event.entity)) { for (const dependentEntity of entitiesToInvalidate) { const pattern = this.getInvalidationPattern(dependentEntity, event.projectId); const count = await this.cacheOrchestrator.invalidate([pattern]); totalInvalidated += count; this.stats.cascadeInvalidations += count; this.logger.info(`Cascade invalidated ${count} entries for ${dependentEntity}`); } } return totalInvalidated; } /** * Handle bulk changes (e.g., full sync) */ async handleBulkChange(entities, projectId) { this.logger.info(`Handling bulk change for ${entities.length} entities in project ${projectId || 'all'}`); const event = { type: 'bulk_change', entity: 'multiple', projectId, timestamp: Date.now() }; this.updateStats(event); this.emit('invalidation', event); let totalInvalidated = 0; for (const entity of entities) { const pattern = this.getInvalidationPattern(entity, projectId); const count = await this.cacheOrchestrator.invalidate([pattern]); totalInvalidated += count; } this.logger.info(`Bulk invalidated ${totalInvalidated} entries`); return totalInvalidated; } /** * Invalidate specific entity by ID */ async invalidateEntity(entity, entityId, projectId) { const event = { type: 'entity_changed', entity, entityId, projectId, timestamp: Date.now(), changeType: 'update' }; return this.handleEntityChange(event); } /** * Invalidate all caches for a project */ async invalidateProject(projectId) { this.logger.info(`Invalidating all caches for project ${projectId}`); const pattern = `iqe:*:*:p${projectId}*`; const count = await this.cacheOrchestrator.invalidate([pattern]); this.updateStats({ type: 'manual', entity: 'project', projectId, timestamp: Date.now() }); return count; } /** * Get entities that should be invalidated when a given entity changes */ getEntitiesToInvalidate(entity) { const affected = this.dependencies.get(entity); return affected ? Array.from(affected) : []; } /** * Get invalidation pattern for an entity */ getInvalidationPattern(entity, projectId) { const rule = this.rules.get(entity); let pattern = rule?.invalidationPattern || `iqe:${entity}:*`; // For now, don't add project context as cache keys don't include project ID // TODO: Update cache key generation to include project context return pattern; } /** * Check if cascade invalidation is enabled for entity */ shouldCascade(entity) { const rule = this.rules.get(entity); return rule?.cascadeInvalidation ?? false; } /** * Update statistics */ updateStats(event) { this.stats.totalInvalidations++; if (!this.stats.invalidationsByEntity[event.entity]) { this.stats.invalidationsByEntity[event.entity] = 0; } this.stats.invalidationsByEntity[event.entity]++; this.stats.lastInvalidation = new Date(); // Track invalidation time for rate calculation const now = Date.now(); this.invalidationHistory.push(now); // Clean old history const cutoff = now - this.historyWindowMs; this.invalidationHistory = this.invalidationHistory.filter(t => t > cutoff); // Calculate rate this.stats.averageInvalidationsPerMinute = this.invalidationHistory.length; } /** * Get invalidation statistics */ getStats() { return { ...this.stats }; } /** * Reset statistics */ resetStats() { this.stats = { totalInvalidations: 0, invalidationsByEntity: {}, cascadeInvalidations: 0, averageInvalidationsPerMinute: 0, }; this.invalidationHistory = []; } /** * Get dependency information */ getDependencies() { return new Map(this.dependencies); } /** * Shutdown the engine */ shutdown() { this.removeAllListeners(); this.logger.info('InvalidationEngine shutdown'); } } //# sourceMappingURL=InvalidationEngine.js.map