// cache.ts - Compilation and extraction cache implementations for Imhotep bench harness // Provides in-memory caching with TTL, versioning, and explicit invalidation. export interface CacheKey { /** Content hash or deterministic identifier */ hash: string /** Cache schema version for invalidation on format changes */ version: string } export interface CacheEntry { /** Stored value */ value: T /** Unix timestamp when the entry was created */ createdAt: number /** Optional Unix timestamp when the entry expires */ expiresAt?: number } export interface Cache { get(key: CacheKey): T | undefined set(key: CacheKey, value: T, ttlMs?: number): void invalidate(key: CacheKey): boolean clear(): void size(): number } /** Simple in-memory cache backed by a Map. */ export class MemoryCache implements Cache { private store = new Map>() private makeKey(key: CacheKey): string { return `${key.version}:${key.hash}` } get(key: CacheKey): T | undefined { const k = this.makeKey(key) const entry = this.store.get(k) if (!entry) return undefined if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) { this.store.delete(k) return undefined } return entry.value } set(key: CacheKey, value: T, ttlMs?: number): void { const k = this.makeKey(key) const now = Date.now() this.store.set(k, { value, createdAt: now, expiresAt: ttlMs !== undefined ? now + ttlMs : undefined, }) } invalidate(key: CacheKey): boolean { return this.store.delete(this.makeKey(key)) } clear(): void { this.store.clear() } size(): number { // Prune expired entries before reporting size for accuracy const now = Date.now() for (const [k, entry] of this.store) { if (entry.expiresAt !== undefined && now > entry.expiresAt) { this.store.delete(k) } } return this.store.size } } /** Cache key factory for compilation inputs. */ export function makeCompilationCacheKey( source: string, version: string ): CacheKey { // Simple hash: in production this should be a real hash function let hash = 0 for (let i = 0; i < source.length; i++) { const char = source.charCodeAt(i) hash = (hash << 5) - hash + char hash |= 0 } return { hash: String(hash), version } } /** Cache key factory for extraction plans. */ export function makeExtractionCacheKey( selector: string, facts: string[], version: string ): CacheKey { const combined = `${selector}::${facts.sort().join(',')}` let hash = 0 for (let i = 0; i < combined.length; i++) { const char = combined.charCodeAt(i) hash = (hash << 5) - hash + char hash |= 0 } return { hash: String(hash), version } } /** * CompilationCache wraps a generic Cache for CompileResult-like objects. * Accepts the underlying Cache via constructor injection. */ export class CompilationCache { constructor( private cache: Cache, private version: string ) {} get(source: string): TCompileResult | undefined { return this.cache.get(makeCompilationCacheKey(source, this.version)) } set(source: string, result: TCompileResult, ttlMs?: number): void { this.cache.set(makeCompilationCacheKey(source, this.version), result, ttlMs) } invalidate(source: string): boolean { return this.cache.invalidate(makeCompilationCacheKey(source, this.version)) } clear(): void { this.cache.clear() } } /** * ExtractionCache wraps a generic Cache for extraction payloads. * Accepts the underlying Cache via constructor injection. */ export class ExtractionCache { constructor( private cache: Cache, private version: string ) {} get(selector: string, facts: string[]): TExtractResult | undefined { return this.cache.get(makeExtractionCacheKey(selector, facts, this.version)) } set( selector: string, facts: string[], result: TExtractResult, ttlMs?: number ): void { this.cache.set( makeExtractionCacheKey(selector, facts, this.version), result, ttlMs ) } invalidate(selector: string, facts: string[]): boolean { return this.cache.invalidate( makeExtractionCacheKey(selector, facts, this.version) ) } clear(): void { this.cache.clear() } }