/**
* LRU Cache for search results
* Improves search performance by caching common queries
*/
export interface CacheEntry<T> {
value: T
timestamp: number
hits: number
}
export interface CacheStats {
size: number
maxSize: number
hits: number
misses: number
hitRate: number
}
/**
* LRU (Least Recently Used) Cache
* Automatically evicts least recently used entries when full
*/
export class LRUCache<T> {
private cache: Map<string, CacheEntry<T>>
private maxSize: number
private ttl: number // Time to live in milliseconds
private hits = 0
private misses = 0
constructor(maxSize = 100, ttlMinutes = 5) {
this.cache = new Map()
this.maxSize = maxSize
this.ttl = ttlMinutes * 60 * 1000
}
/**
* Get value from cache
*/
get(key: string): T | undefined {
const entry = this.cache.get(key)
if (!entry) {
this.misses++
return undefined
}
// Check if entry expired
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key)
this.misses++
return undefined
}
// Update entry (move to end = most recently used)
entry.hits++
entry.timestamp = Date.now()
this.cache.delete(key)
this.cache.set(key, entry)
this.hits++
return entry.value
}
/**
* Set value in cache
*/
set(key: string, value: T): void {
// Remove old entry if exists
if (this.cache.has(key)) {
this.cache.delete(key)
}
// Evict least recently used if cache is full
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
if (firstKey !== undefined) {
this.cache.delete(firstKey)
}
}
// Add new entry (at end = most recently used)
this.cache.set(key, {
value,
timestamp: Date.now(),
hits: 0,
})
}
/**
* Clear all cache entries
*/
clear(): void {
this.cache.clear()
this.hits = 0
this.misses = 0
}
/**
* Invalidate cache (clear all entries)
* Call this when the index is updated
*/
invalidate(): void {
this.clear()
}
/**
* Get cache statistics
*/
stats(): CacheStats {
const total = this.hits + this.misses
return {
size: this.cache.size,
maxSize: this.maxSize,
hits: this.hits,
misses: this.misses,
hitRate: total > 0 ? this.hits / total : 0,
}
}
/**
* Remove expired entries
*/
cleanup(): void {
const now = Date.now()
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > this.ttl) {
this.cache.delete(key)
}
}
}
}
/**
* Create a cache key from search parameters
*/
export function createCacheKey(
query: string,
options: {
limit?: number
fileExtensions?: string[]
pathFilter?: string
excludePaths?: string[]
} = {}
): string {
const parts = [
query.toLowerCase().trim(),
options.limit?.toString() || '10',
options.fileExtensions?.sort().join(',') || '',
options.pathFilter || '',
options.excludePaths?.sort().join(',') || '',
]
return parts.join('|')
}