@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
257 lines • 8.84 kB
JavaScript
/**
* 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