/** * Cache Manager — Hash-basierte Prose-Block-Cache * * Deterministischer Cache fuer LLM-generierte Prosa-Bloecke. * Kein TTL-basiertes Raten — stattdessen Hash-basierte Invalidierung. * * Cache-Key = SHA-256 ueber alle Eingabeparameter. * Aendert sich ein Eingabewert → neuer Hash → Cache-Miss → Neu-Generierung. */ import type { AllowedFacts } from './allowed-facts' import type { NarrativeTags } from './narrative-tags' import type { ProseBlockOutput } from './prose-validator' // ============================================================================ // Types // ============================================================================ export interface CacheEntry { block: ProseBlockOutput createdAt: string hitCount: number cacheKey: string } export interface CacheKeyParams { allowedFacts: AllowedFacts templateVersion: string terminologyVersion: string narrativeTags: NarrativeTags promptHash: string blockType: string sectionName: string } export interface CacheStats { totalEntries: number totalHits: number totalMisses: number hitRate: number oldestEntry: string | null newestEntry: string | null } // ============================================================================ // SHA-256 (Browser-kompatibel via SubtleCrypto) // ============================================================================ /** * Berechnet SHA-256 Hash eines Strings. * Nutzt SubtleCrypto (verfuegbar in Node.js 15+ und allen modernen Browsern). */ async function sha256(input: string): Promise { // In Next.js API Routes laeuft Node.js — nutze crypto if (typeof globalThis.crypto?.subtle !== 'undefined') { const encoder = new TextEncoder() const data = encoder.encode(input) const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') } // Fallback: Node.js crypto try { const { createHash } = await import('crypto') return createHash('sha256').update(input).digest('hex') } catch { // Letzer Fallback: Einfacher Hash (nicht kryptographisch) return simpleHash(input) } } /** * Synchrone SHA-256 Berechnung (Node.js only). */ function sha256Sync(input: string): string { try { // eslint-disable-next-line @typescript-eslint/no-require-imports const crypto = require('crypto') return crypto.createHash('sha256').update(input).digest('hex') } catch { return simpleHash(input) } } /** * Einfacher nicht-kryptographischer Hash als Fallback. */ function simpleHash(input: string): string { let hash = 0 for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash } return Math.abs(hash).toString(16).padStart(16, '0') } // ============================================================================ // Cache Key Computation // ============================================================================ /** * Berechnet den deterministischen Cache-Key. * Sortiert Keys um konsistente Serialisierung zu gewaehrleisten. */ export async function computeCacheKey(params: CacheKeyParams): Promise { const payload = JSON.stringify(params, Object.keys(params).sort()) return sha256(payload) } /** * Synchrone Variante fuer Cache-Key (Node.js). */ export function computeCacheKeySync(params: CacheKeyParams): string { const payload = JSON.stringify(params, Object.keys(params).sort()) return sha256Sync(payload) } // ============================================================================ // In-Memory Cache // ============================================================================ /** * In-Memory Cache fuer Prose-Bloecke. * * Sicherheitsmechanismen: * - Max Eintraege (Speicher-Limit) * - TTL als zusaetzlicher Sicherheitsmechanismus (24h default) * - LRU-artige Bereinigung bei Overflow */ export class ProseCacheManager { private cache = new Map() private hits = 0 private misses = 0 private readonly maxEntries: number private readonly ttlMs: number constructor(options?: { maxEntries?: number; ttlHours?: number }) { this.maxEntries = options?.maxEntries ?? 500 this.ttlMs = (options?.ttlHours ?? 24) * 60 * 60 * 1000 } /** * Sucht einen gecachten Block. */ async get(params: CacheKeyParams): Promise { const key = await computeCacheKey(params) return this.getByKey(key) } /** * Sucht synchron (Node.js). */ getSync(params: CacheKeyParams): ProseBlockOutput | null { const key = computeCacheKeySync(params) return this.getByKey(key) } /** * Speichert einen Block im Cache. */ async set(params: CacheKeyParams, block: ProseBlockOutput): Promise { const key = await computeCacheKey(params) this.setByKey(key, block) } /** * Speichert synchron (Node.js). */ setSync(params: CacheKeyParams, block: ProseBlockOutput): void { const key = computeCacheKeySync(params) this.setByKey(key, block) } /** * Gibt Cache-Statistiken zurueck. */ getStats(): CacheStats { const entries = Array.from(this.cache.values()) const total = this.hits + this.misses return { totalEntries: this.cache.size, totalHits: this.hits, totalMisses: this.misses, hitRate: total > 0 ? this.hits / total : 0, oldestEntry: entries.length > 0 ? entries.reduce((a, b) => a.createdAt < b.createdAt ? a : b).createdAt : null, newestEntry: entries.length > 0 ? entries.reduce((a, b) => a.createdAt > b.createdAt ? a : b).createdAt : null, } } /** * Loescht alle Eintraege. */ clear(): void { this.cache.clear() this.hits = 0 this.misses = 0 } /** * Loescht abgelaufene Eintraege. */ cleanup(): number { const now = Date.now() let removed = 0 for (const [key, entry] of this.cache.entries()) { if (now - new Date(entry.createdAt).getTime() > this.ttlMs) { this.cache.delete(key) removed++ } } return removed } // ======================================================================== // Private // ======================================================================== private getByKey(key: string): ProseBlockOutput | null { const entry = this.cache.get(key) if (!entry) { this.misses++ return null } // TTL pruefen if (Date.now() - new Date(entry.createdAt).getTime() > this.ttlMs) { this.cache.delete(key) this.misses++ return null } entry.hitCount++ this.hits++ return entry.block } private setByKey(key: string, block: ProseBlockOutput): void { // Bei Overflow: aeltesten Eintrag entfernen if (this.cache.size >= this.maxEntries) { this.evictOldest() } this.cache.set(key, { block, createdAt: new Date().toISOString(), hitCount: 0, cacheKey: key, }) } private evictOldest(): void { let oldestKey: string | null = null let oldestTime = Infinity for (const [key, entry] of this.cache.entries()) { const time = new Date(entry.createdAt).getTime() if (time < oldestTime) { oldestTime = time oldestKey = key } } if (oldestKey) { this.cache.delete(oldestKey) } } } // ============================================================================ // Checksum Utils (fuer Data Block Integritaet) // ============================================================================ /** * Berechnet Integritaets-Checksum ueber Daten. */ export async function computeChecksum(data: unknown): Promise { const serialized = JSON.stringify(data, Object.keys(data as Record).sort()) return sha256(serialized) } /** * Synchrone Checksum-Berechnung. */ export function computeChecksumSync(data: unknown): string { const serialized = JSON.stringify(data, Object.keys(data as Record).sort()) return sha256Sync(serialized) } /** * Verifiziert eine Checksum gegen Daten. */ export async function verifyChecksum(data: unknown, expectedChecksum: string): Promise { const actual = await computeChecksum(data) return actual === expectedChecksum }