import { createHash } from 'crypto';
import { logger } from '../logger.js';
import type { CacheManager, TimedCache } from './types.js';
import { getPerformanceMonitor } from './performance-monitor.js';
/**
* Implementation of a time-based cache with TTL support and performance tracking
*/
class TimedCacheImpl<T> implements TimedCache<T> {
private cache: Map<string, { value: T; expiry: number }> = new Map();
private cleanupInterval: NodeJS.Timeout;
private hitCount = 0;
private missCount = 0;
private evictionCount = 0;
constructor(
private cacheName: string,
private defaultTTL: number = 5000,
cleanupIntervalMs: number = 30000
) {
// Periodic cleanup of expired entries
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, cleanupIntervalMs);
}
get(key: string): T | undefined {
try {
const entry = this.cache.get(key);
if (!entry) {
this.missCount++;
this.updatePerformanceMetrics();
return undefined;
}
if (Date.now() > entry.expiry) {
this.cache.delete(key);
this.evictionCount++;
this.missCount++;
this.updatePerformanceMetrics();
return undefined;
}
this.hitCount++;
this.updatePerformanceMetrics();
return entry.value;
} catch (error) {
logger.warn(`Cache get operation failed for key: ${key}`, error as Error);
this.missCount++;
this.updatePerformanceMetrics();
return undefined;
}
}
set(key: string, value: T, ttlMs?: number): void {
try {
const ttl = ttlMs ?? this.defaultTTL;
this.cache.set(key, {
value,
expiry: Date.now() + ttl
});
} catch (error) {
logger.warn(`Cache set operation failed for key: ${key}`, error as Error);
// Silently fail - cache operations should not break the main functionality
}
}
has(key: string): boolean {
try {
const entry = this.cache.get(key);
if (!entry) {
this.missCount++;
this.updatePerformanceMetrics();
return false;
}
if (Date.now() > entry.expiry) {
this.cache.delete(key);
this.evictionCount++;
this.missCount++;
this.updatePerformanceMetrics();
return false;
}
this.hitCount++;
this.updatePerformanceMetrics();
return true;
} catch (error) {
logger.warn(`Cache has operation failed for key: ${key}`, error as Error);
this.missCount++;
this.updatePerformanceMetrics();
return false;
}
}
delete(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
private cleanup(): void {
const now = Date.now();
let evicted = 0;
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiry) {
this.cache.delete(key);
evicted++;
}
}
if (evicted > 0) {
this.evictionCount += evicted;
this.updatePerformanceMetrics();
logger.debug(`Cache cleanup: evicted ${evicted} expired entries from ${this.cacheName}`);
}
}
destroy(): void {
clearInterval(this.cleanupInterval);
this.clear();
}
/**
* Get cache statistics
*/
getStats(): {
hitCount: number;
missCount: number;
hitRate: number;
evictionCount: number;
size: number;
} {
const totalRequests = this.hitCount + this.missCount;
return {
hitCount: this.hitCount,
missCount: this.missCount,
hitRate: totalRequests > 0 ? this.hitCount / totalRequests : 0,
evictionCount: this.evictionCount,
size: this.cache.size
};
}
/**
* Reset statistics
*/
resetStats(): void {
this.hitCount = 0;
this.missCount = 0;
this.evictionCount = 0;
}
/**
* Update performance metrics
*/
private updatePerformanceMetrics(): void {
try {
const performanceMonitor = getPerformanceMonitor();
const stats = this.getStats();
performanceMonitor.recordCacheMetrics(
this.cacheName,
stats.hitCount,
stats.missCount,
stats.evictionCount,
stats.size
);
} catch {
// Ignore errors if performance monitor is not available
}
}
}
/**
* Centralized cache manager for all caching needs
*/
export class CacheManagerImpl implements CacheManager {
private static instance: CacheManagerImpl;
private screenshotCache: TimedCacheImpl<any>;
private ocrCache: TimedCacheImpl<any>;
private windowCache: TimedCacheImpl<any>;
private permissionCache: TimedCacheImpl<any>;
// Performance tracking
private performanceUpdateInterval: NodeJS.Timeout | null = null;
private constructor() {
// Optimized TTLs based on operation characteristics
this.screenshotCache = new TimedCacheImpl('screenshot', 3000); // 3 seconds - screens change frequently
this.ocrCache = new TimedCacheImpl('ocr', 15000); // 15 seconds - OCR is expensive but content can change
this.windowCache = new TimedCacheImpl('window', 1500); // 1.5 seconds - windows move/resize frequently
this.permissionCache = new TimedCacheImpl('permission', 600000); // 10 minutes - permissions rarely change
// Start periodic performance reporting
this.startPerformanceReporting();
// Start cache warming asynchronously
this.warmCaches().catch(error => {
logger.warn('Initial cache warming failed', error as Error);
});
logger.debug('CacheManager initialized with optimized TTL-based caches and performance tracking');
}
static getInstance(): CacheManagerImpl {
if (!CacheManagerImpl.instance) {
CacheManagerImpl.instance = new CacheManagerImpl();
}
return CacheManagerImpl.instance;
}
getScreenshotCache(): TimedCache<any> {
return this.screenshotCache;
}
getOCRCache(): TimedCache<any> {
return this.ocrCache;
}
getWindowCache(): TimedCache<any> {
return this.windowCache;
}
getPermissionCache(): TimedCache<any> {
return this.permissionCache;
}
invalidate(pattern?: string): void {
if (!pattern) {
// Clear all caches
this.screenshotCache.clear();
this.ocrCache.clear();
this.windowCache.clear();
this.permissionCache.clear();
logger.debug('All caches cleared');
} else {
// Pattern-based invalidation would require storing keys
// For now, just clear the specific cache type
switch (pattern) {
case 'screenshot':
this.screenshotCache.clear();
break;
case 'ocr':
this.ocrCache.clear();
break;
case 'window':
this.windowCache.clear();
break;
case 'permission':
this.permissionCache.clear();
break;
}
logger.debug(`Cache cleared for pattern: ${pattern}`);
}
}
/**
* Generate a hash key for caching screenshot/OCR results
*/
static generateImageHash(imageData: Buffer | Uint8Array): string {
const hash = createHash('md5');
hash.update(Buffer.from(imageData));
return hash.digest('hex');
}
/**
* Generate a cache key for region-based operations
*/
static generateRegionKey(region?: { x: number; y: number; width: number; height: number }): string {
if (!region) {return 'fullscreen';}
return `${region.x},${region.y},${region.width},${region.height}`;
}
/**
* Get comprehensive cache statistics
*/
getAllCacheStats(): {
screenshot: ReturnType<TimedCacheImpl<any>['getStats']>;
ocr: ReturnType<TimedCacheImpl<any>['getStats']>;
window: ReturnType<TimedCacheImpl<any>['getStats']>;
permission: ReturnType<TimedCacheImpl<any>['getStats']>;
} {
return {
screenshot: (this.screenshotCache as TimedCacheImpl<any>).getStats(),
ocr: (this.ocrCache as TimedCacheImpl<any>).getStats(),
window: (this.windowCache as TimedCacheImpl<any>).getStats(),
permission: (this.permissionCache as TimedCacheImpl<any>).getStats()
};
}
/**
* Reset all cache statistics
*/
resetAllStats(): void {
(this.screenshotCache as TimedCacheImpl<any>).resetStats();
(this.ocrCache as TimedCacheImpl<any>).resetStats();
(this.windowCache as TimedCacheImpl<any>).resetStats();
(this.permissionCache as TimedCacheImpl<any>).resetStats();
logger.debug('All cache statistics reset');
}
/**
* Warm up caches with frequently accessed data
*/
async warmCaches(): Promise<void> {
logger.debug('Starting cache warming...');
try {
// Warm up common OCR patterns (empty region for full screen)
const commonRegions = [
undefined, // Full screen
{ x: 0, y: 0, width: 1920, height: 1080 }, // Common screen size
{ x: 0, y: 0, width: 2560, height: 1440 } // Another common screen size
];
// Pre-generate region keys for common scenarios
for (const region of commonRegions) {
const regionKey = CacheManagerImpl.generateRegionKey(region);
logger.debug(`Pre-generated region key for warming: ${regionKey}`);
}
// Warm window cache by pre-emptively setting common cache keys
// This doesn't actually fetch data, just prepares the cache structure
logger.debug('Cache warming completed');
} catch (error) {
logger.warn('Cache warming failed', error as Error);
}
}
/**
* Invalidate caches based on operation type for smart cache management
*/
smartInvalidate(operationType: 'window_action' | 'screen_change' | 'app_switch' | 'all'): void {
switch (operationType) {
case 'window_action':
// When windows are moved, focused, or closed, invalidate window cache
this.windowCache.clear();
logger.debug('Window cache invalidated due to window action');
break;
case 'screen_change':
// When screen content changes, invalidate screenshot and OCR caches
this.screenshotCache.clear();
this.ocrCache.clear();
logger.debug('Screenshot and OCR caches invalidated due to screen change');
break;
case 'app_switch':
// When switching apps, invalidate all visual caches but keep permissions
this.screenshotCache.clear();
this.ocrCache.clear();
this.windowCache.clear();
logger.debug('Visual caches invalidated due to app switch');
break;
case 'all':
this.invalidate();
break;
}
}
/**
* Get cache efficiency metrics
*/
getCacheEfficiency(): {
overall: number;
byCache: Record<string, number>;
recommendations: string[];
} {
const allStats = this.getAllCacheStats();
const recommendations: string[] = [];
const byCache: Record<string, number> = {};
let totalHits = 0;
let totalRequests = 0;
for (const [cacheName, stats] of Object.entries(allStats)) {
const requests = stats.hitCount + stats.missCount;
const hitRate = requests > 0 ? stats.hitCount / requests : 0;
byCache[cacheName] = hitRate;
totalHits += stats.hitCount;
totalRequests += requests;
// Generate recommendations based on hit rates
if (requests > 10) { // Only recommend for caches with sufficient activity
if (hitRate < 0.1) {
recommendations.push(`${cacheName} cache has very low hit rate (${(hitRate * 100).toFixed(1)}%) - consider longer TTL or better cache keys`);
} else if (hitRate > 0.8) {
recommendations.push(`${cacheName} cache is performing well (${(hitRate * 100).toFixed(1)}% hit rate)`);
}
}
}
const overallEfficiency = totalRequests > 0 ? totalHits / totalRequests : 0;
if (overallEfficiency < 0.2) {
recommendations.push('Overall cache efficiency is low - consider implementing cache warming or adjusting TTL values');
} else if (overallEfficiency > 0.6) {
recommendations.push('Cache system is performing well overall');
}
return {
overall: overallEfficiency,
byCache,
recommendations
};
}
/**
* Start periodic performance reporting
*/
private startPerformanceReporting(): void {
// Report cache metrics every 30 seconds
this.performanceUpdateInterval = setInterval(() => {
this.reportPerformanceMetrics();
}, 30000);
}
/**
* Report current cache metrics to performance monitor
*/
private reportPerformanceMetrics(): void {
try {
const performanceMonitor = getPerformanceMonitor();
const allStats = this.getAllCacheStats();
for (const [cacheName, stats] of Object.entries(allStats)) {
performanceMonitor.recordCacheMetrics(
cacheName,
stats.hitCount,
stats.missCount,
stats.evictionCount,
stats.size
);
}
} catch {
// Ignore errors if performance monitor is not available
logger.debug('Performance monitor not available for cache metrics reporting');
}
}
destroy(): void {
if (this.performanceUpdateInterval) {
clearInterval(this.performanceUpdateInterval);
this.performanceUpdateInterval = null;
}
this.screenshotCache.destroy();
this.ocrCache.destroy();
this.windowCache.destroy();
this.permissionCache.destroy();
}
}