import NodeCache from 'node-cache';
import crypto from 'crypto';
import { RESOURCE_LIMITS } from '../constants.js';
/**
* Cache configuration options
*/
export interface CacheOptions {
/** Time-to-live in seconds */
ttl?: number;
/** Maximum number of keys */
maxKeys?: number;
/** Check period in seconds */
checkPeriod?: number;
/** Whether caching is enabled */
enabled?: boolean;
}
/**
* Centralized cache manager
* Manages multiple named caches with consistent configuration
*/
class CacheManager {
private caches: Map<string, NodeCache> = new Map();
private enabled = true;
/**
* Get or create a named cache
*
* @param name - Unique cache name
* @param options - Cache configuration options
* @returns NodeCache instance
*/
getCache(name: string, options?: CacheOptions): NodeCache {
if (!this.enabled) {
return this.createNoOpCache();
}
if (!this.caches.has(name)) {
const cache = new NodeCache({
stdTTL: options?.ttl ?? RESOURCE_LIMITS.DEFAULT_CACHE_TTL_SECONDS,
maxKeys: options?.maxKeys ?? 500,
checkperiod: options?.checkPeriod ?? 300,
deleteOnExpire: true,
useClones: false
});
this.caches.set(name, cache);
console.log(`[Cache] Created: ${name} (TTL: ${cache.options.stdTTL}s, MaxKeys: ${cache.options.maxKeys})`);
}
return this.caches.get(name)!;
}
/**
* Disable all caching (for debugging)
*/
disable(): void {
this.enabled = false;
console.log('[Cache] Disabled globally');
}
/**
* Enable all caching
*/
enable(): void {
this.enabled = true;
console.log('[Cache] Enabled globally');
}
/**
* Clear all caches
*/
clearAll(): void {
for (const [name, cache] of this.caches) {
cache.flushAll();
console.log(`[Cache] Cleared: ${name}`);
}
}
/**
* Get statistics for all caches
*
* @returns Statistics by cache name
*/
getStats(): Record<string, {
keys: number;
hits: number;
misses: number;
hitRate: number;
}> {
const stats: Record<string, {
keys: number;
hits: number;
misses: number;
hitRate: number;
}> = {};
for (const [name, cache] of this.caches) {
const cacheStats = cache.getStats();
stats[name] = {
keys: cache.keys().length,
hits: cacheStats.hits,
misses: cacheStats.misses,
hitRate: cacheStats.hits > 0
? cacheStats.hits / (cacheStats.hits + cacheStats.misses)
: 0
};
}
return stats;
}
/**
* Get formatted statistics report
*
* @returns Human-readable cache statistics
*/
getStatsReport(): string {
const stats = this.getStats();
const lines = [
'Cache Statistics',
'='.repeat(50),
''
];
if (Object.keys(stats).length === 0) {
lines.push('No caches created yet.');
return lines.join('\n');
}
for (const [name, data] of Object.entries(stats)) {
lines.push(
`${name}:`,
` Keys: ${data.keys}`,
` Hits: ${data.hits}`,
` Misses: ${data.misses}`,
` Hit Rate: ${(data.hitRate * 100).toFixed(1)}%`,
''
);
}
const totalHits = Object.values(stats).reduce((sum, s) => sum + s.hits, 0);
const totalMisses = Object.values(stats).reduce((sum, s) => sum + s.misses, 0);
const overallHitRate = totalHits > 0 ? totalHits / (totalHits + totalMisses) : 0;
lines.push(
'Overall:',
` Total Hits: ${totalHits}`,
` Total Misses: ${totalMisses}`,
` Overall Hit Rate: ${(overallHitRate * 100).toFixed(1)}%`,
` Active Caches: ${this.caches.size}`
);
return lines.join('\n');
}
/**
* Create a no-op cache that doesn't store anything
* Used when caching is disabled
*/
private createNoOpCache(): NodeCache {
const cache = new NodeCache({ stdTTL: 0, maxKeys: 0 });
cache.get = () => undefined;
cache.set = () => true;
return cache;
}
}
/**
* Global cache manager instance
*/
export const cacheManager = new CacheManager();
/**
* Named cache instances for common use cases
*/
export const binaryCache = cacheManager.getCache('binary', {
ttl: RESOURCE_LIMITS.DEFAULT_CACHE_TTL_SECONDS
});
export const fileContentCache = cacheManager.getCache('fileContent', {
ttl: 300 // 5 minutes (files change more frequently)
});
// Legacy default cache for backward compatibility
const cache = cacheManager.getCache('default');
export function generateCacheKey(prefix: string, params: unknown): string {
const paramString = createStableParamString(params);
const hash = crypto.createHash('sha256').update(paramString).digest('hex');
return `${prefix}:${hash}`;
}
function createStableParamString(params: unknown): string {
if (params === null) return 'null';
if (params === undefined) return 'undefined';
if (typeof params !== 'object') return String(params);
if (Array.isArray(params)) {
return `[${params.map(createStableParamString).join(',')}]`;
}
const sortedKeys = Object.keys(params as Record<string, unknown>).sort();
const sortedEntries = sortedKeys.map((key) => {
const value = (params as Record<string, unknown>)[key];
return `"${key}":${createStableParamString(value)}`;
});
return `{${sortedEntries.join(',')}}`;
}
export async function withDataCache<T>(
cacheKey: string,
operation: () => Promise<T>,
options: {
ttl?: number;
skipCache?: boolean;
forceRefresh?: boolean;
shouldCache?: (value: T) => boolean;
} = {}
): Promise<T> {
if (options.skipCache) {
return await operation();
}
if (!options.forceRefresh) {
const cached = cache.get<T>(cacheKey);
if (cached !== undefined) {
return cached;
}
}
const result = await operation();
const shouldCache = options.shouldCache ?? (() => true);
if (shouldCache(result)) {
const ttl = options.ttl ?? RESOURCE_LIMITS.DEFAULT_CACHE_TTL_SECONDS;
cache.set(cacheKey, result, ttl);
}
return result;
}
export function clearAllCache(): void {
cache.flushAll();
}