optivise
Version:
Optivise - The Ultimate Optimizely Development Assistant with AI-powered features, zero-config setup, and comprehensive development support
379 lines • 11.6 kB
JavaScript
/**
* Advanced Caching and Performance Optimization Service
* Provides intelligent caching with TTL, LRU eviction, and performance monitoring
*/
import { EventEmitter } from 'events';
export class CacheService extends EventEmitter {
cache = new Map();
accessTimes = [];
stats = {
hits: 0,
misses: 0,
evictions: 0,
entries: 0,
memoryUsage: 0,
hitRate: 0,
avgAccessTime: 0
};
cleanupTimer;
logger;
config;
constructor(logger, config) {
super();
this.logger = logger;
this.config = {
maxEntries: 10000,
maxMemoryMB: 256,
defaultTTL: 15 * 60 * 1000, // 15 minutes
cleanupInterval: 5 * 60 * 1000, // 5 minutes
enableMetrics: true,
...config
};
this.startCleanupTimer();
this.logger.info('Cache Service initialized', this.config);
}
/**
* Store value in cache with optional TTL
*/
set(key, value, ttlMs) {
const startTime = Date.now();
try {
const ttl = ttlMs || this.config.defaultTTL;
const size = this.estimateSize(value);
const entry = {
value,
timestamp: Date.now(),
ttl,
accessCount: 1,
lastAccessed: Date.now(),
size
};
// Check memory constraints before adding
if (this.wouldExceedMemoryLimit(size)) {
this.evictLRUEntries();
// If still would exceed after eviction, reject
if (this.wouldExceedMemoryLimit(size)) {
this.logger.warn('Cache entry too large, rejecting', { key, size });
return false;
}
}
// Remove existing entry if it exists
if (this.cache.has(key)) {
const oldEntry = this.cache.get(key);
this.stats.memoryUsage -= oldEntry.size;
}
this.cache.set(key, entry);
this.stats.entries = this.cache.size;
this.stats.memoryUsage += size;
this.recordAccessTime(Date.now() - startTime);
this.emit('set', { key, size, ttl });
return true;
}
catch (error) {
this.logger.error('Failed to set cache entry', error, { key });
return false;
}
}
/**
* Retrieve value from cache
*/
get(key) {
const startTime = Date.now();
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
this.recordAccessTime(Date.now() - startTime);
return null;
}
// Check if entry has expired
if (this.isExpired(entry)) {
this.cache.delete(key);
this.stats.entries = this.cache.size;
this.stats.memoryUsage -= entry.size;
this.stats.evictions++;
this.stats.misses++;
this.recordAccessTime(Date.now() - startTime);
return null;
}
// Update access statistics
entry.accessCount++;
entry.lastAccessed = Date.now();
this.stats.hits++;
this.updateHitRate();
this.recordAccessTime(Date.now() - startTime);
return entry.value;
}
/**
* Check if key exists in cache (without accessing)
*/
has(key) {
const entry = this.cache.get(key);
return entry ? !this.isExpired(entry) : false;
}
/**
* Delete specific key from cache
*/
delete(key) {
const entry = this.cache.get(key);
if (entry) {
this.cache.delete(key);
this.stats.entries = this.cache.size;
this.stats.memoryUsage -= entry.size;
this.emit('delete', { key });
return true;
}
return false;
}
/**
* Clear all cache entries
*/
clear() {
const previousEntries = this.cache.size;
this.cache.clear();
this.stats.entries = 0;
this.stats.memoryUsage = 0;
this.emit('clear', { previousEntries });
this.logger.info('Cache cleared', { previousEntries });
}
/**
* Get cache statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Get cache configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Update cache configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
this.logger.info('Cache configuration updated', this.config);
this.emit('configUpdate', this.config);
}
/**
* Get cache keys with optional filtering
*/
getKeys(pattern) {
const keys = Array.from(this.cache.keys());
return pattern ? keys.filter(key => pattern.test(key)) : keys;
}
/**
* Get memory usage breakdown by key patterns
*/
getMemoryBreakdown() {
const breakdown = {};
for (const [key, entry] of this.cache) {
const category = this.categorizeKey(key);
if (!breakdown[category]) {
breakdown[category] = { entries: 0, memory: 0 };
}
breakdown[category].entries++;
breakdown[category].memory += entry.size;
}
return breakdown;
}
/**
* Manually trigger cache cleanup
*/
cleanup() {
let cleaned = 0;
const now = Date.now();
for (const [key, entry] of this.cache) {
if (this.isExpired(entry)) {
this.cache.delete(key);
this.stats.memoryUsage -= entry.size;
this.stats.evictions++;
cleaned++;
}
}
this.stats.entries = this.cache.size;
if (cleaned > 0) {
this.logger.debug('Cache cleanup completed', { cleaned, remaining: this.cache.size });
this.emit('cleanup', { cleaned, remaining: this.cache.size });
}
return cleaned;
}
/**
* Preload cache with key-value pairs
*/
async preload(entries) {
let loaded = 0;
for (const { key, value, ttl } of entries) {
if (this.set(key, value, ttl)) {
loaded++;
}
}
this.logger.info('Cache preload completed', {
requested: entries.length,
loaded,
failed: entries.length - loaded
});
return loaded;
}
/**
* Export cache contents for backup
*/
export() {
const exports = [];
for (const [key, entry] of this.cache) {
if (!this.isExpired(entry)) {
exports.push({
key,
value: entry.value,
ttl: entry.ttl,
timestamp: entry.timestamp
});
}
}
return exports;
}
/**
* Start cleanup timer
*/
startCleanupTimer() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.cleanupTimer = setInterval(() => {
this.cleanup();
}, this.config.cleanupInterval);
}
/**
* Stop cleanup timer
*/
stopCleanupTimer() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
}
/**
* Check if entry has expired
*/
isExpired(entry) {
return Date.now() - entry.timestamp > entry.ttl;
}
/**
* Estimate size of value in bytes
*/
estimateSize(value) {
try {
if (value === null || value === undefined)
return 8;
if (typeof value === 'string')
return value.length * 2; // UTF-16
if (typeof value === 'number')
return 8;
if (typeof value === 'boolean')
return 4;
if (Buffer.isBuffer(value))
return value.length;
// For objects, use JSON string length as approximation
return JSON.stringify(value).length * 2;
}
catch {
return 1024; // Default fallback size
}
}
/**
* Check if adding entry would exceed memory limit
*/
wouldExceedMemoryLimit(entrySize) {
const maxBytes = this.config.maxMemoryMB * 1024 * 1024;
return (this.stats.memoryUsage + entrySize) > maxBytes;
}
/**
* Evict least recently used entries
*/
evictLRUEntries() {
const entries = Array.from(this.cache.entries());
// Sort by last accessed time (ascending)
entries.sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed);
const targetSize = this.config.maxMemoryMB * 1024 * 1024 * 0.8; // 80% of limit
let evicted = 0;
for (const [key, entry] of entries) {
if (this.stats.memoryUsage <= targetSize)
break;
this.cache.delete(key);
this.stats.memoryUsage -= entry.size;
this.stats.evictions++;
evicted++;
}
this.stats.entries = this.cache.size;
if (evicted > 0) {
this.logger.debug('LRU eviction completed', { evicted, remaining: this.cache.size });
this.emit('evict', { evicted, remaining: this.cache.size });
}
}
/**
* Update hit rate statistics
*/
updateHitRate() {
const total = this.stats.hits + this.stats.misses;
this.stats.hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
}
/**
* Record access time for performance metrics
*/
recordAccessTime(timeMs) {
if (!this.config.enableMetrics)
return;
this.accessTimes.push(timeMs);
// Keep only last 1000 access times
if (this.accessTimes.length > 1000) {
this.accessTimes = this.accessTimes.slice(-1000);
}
// Update average
this.stats.avgAccessTime = this.accessTimes.reduce((sum, time) => sum + time, 0) / this.accessTimes.length;
}
/**
* Categorize cache key for memory breakdown
*/
categorizeKey(key) {
if (key.startsWith('docs:'))
return 'documentation';
if (key.startsWith('analysis:'))
return 'analysis';
if (key.startsWith('embedding:'))
return 'embeddings';
if (key.startsWith('detection:'))
return 'detection';
if (key.startsWith('rule:'))
return 'rules';
return 'other';
}
/**
* Cleanup resources
*/
destroy() {
this.stopCleanupTimer();
this.clear();
this.removeAllListeners();
this.logger.info('Cache Service destroyed');
}
}
// Global cache instances for different use cases
export const analysisCache = (logger) => new CacheService(logger, {
maxEntries: 5000,
maxMemoryMB: 128,
defaultTTL: 30 * 60 * 1000, // 30 minutes
cleanupInterval: 5 * 60 * 1000
});
export const documentationCache = (logger) => new CacheService(logger, {
maxEntries: 10000,
maxMemoryMB: 256,
defaultTTL: 24 * 60 * 60 * 1000, // 24 hours
cleanupInterval: 30 * 60 * 1000 // 30 minutes
});
export const embeddingCache = (logger) => new CacheService(logger, {
maxEntries: 2000,
maxMemoryMB: 512,
defaultTTL: 7 * 24 * 60 * 60 * 1000, // 7 days
cleanupInterval: 60 * 60 * 1000 // 1 hour
});
//# sourceMappingURL=cache-service.js.map