/**
* Query Result Cache
*
* Simple in-memory cache with TTL for reducing redundant searches.
* Based on metrics analysis showing ~10% of searches are repeated within 5 minutes.
*/
export interface CacheEntry<T> {
value: T;
timestamp: number;
hits: number;
}
export interface CacheStats {
hits: number;
misses: number;
size: number;
hitRate: number;
}
export class QueryCache<T> {
private cache: Map<string, CacheEntry<T>> = new Map();
private ttlMs: number;
private maxSize: number;
private hits = 0;
private misses = 0;
constructor(options: { ttlMs?: number; maxSize?: number } = {}) {
this.ttlMs = options.ttlMs ?? 5 * 60 * 1000; // 5 minutes default
this.maxSize = options.maxSize ?? 100;
}
/**
* Generate a cache key from query parameters
*/
static generateKey(tool: string, params: Record<string, unknown>): string {
// Exclude reasoning from cache key - it doesn't affect results
const { reasoning, ...rest } = params;
return `${tool}:${JSON.stringify(rest, Object.keys(rest).sort())}`;
}
/**
* Get a cached value if it exists and hasn't expired
*/
get(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
return undefined;
}
const now = Date.now();
if (now - entry.timestamp > this.ttlMs) {
this.cache.delete(key);
this.misses++;
return undefined;
}
entry.hits++;
this.hits++;
return entry.value;
}
/**
* Store a value in the cache
*/
set(key: string, value: T): void {
// Evict oldest entries if at capacity
if (this.cache.size >= this.maxSize) {
this.evictOldest();
}
this.cache.set(key, {
value,
timestamp: Date.now(),
hits: 0,
});
}
/**
* Check if a key exists and is valid
*/
has(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
const now = Date.now();
if (now - entry.timestamp > this.ttlMs) {
this.cache.delete(key);
return false;
}
return true;
}
/**
* Remove expired entries
*/
cleanup(): number {
const now = Date.now();
let removed = 0;
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.ttlMs) {
this.cache.delete(key);
removed++;
}
}
return removed;
}
/**
* Evict oldest entries to make room
*/
private evictOldest(): void {
let oldest: { key: string; timestamp: number } | null = null;
for (const [key, entry] of this.cache.entries()) {
if (!oldest || entry.timestamp < oldest.timestamp) {
oldest = { key, timestamp: entry.timestamp };
}
}
if (oldest) {
this.cache.delete(oldest.key);
}
}
/**
* Get cache statistics
*/
getStats(): CacheStats {
return {
hits: this.hits,
misses: this.misses,
size: this.cache.size,
hitRate: this.hits + this.misses > 0
? this.hits / (this.hits + this.misses)
: 0,
};
}
/**
* Clear all cached entries
*/
clear(): void {
this.cache.clear();
this.hits = 0;
this.misses = 0;
}
}
// Singleton cache instance for search results
export const searchCache = new QueryCache<unknown>({
ttlMs: 5 * 60 * 1000, // 5 minutes
maxSize: 100,
});
// Cleanup expired entries every minute
setInterval(() => {
searchCache.cleanup();
}, 60 * 1000);