/** * LRU token cache for parsed markdown tokens. * * Extracted from Claude Code's Markdown.tsx. marked.lexer is the hot cost on * virtual-scroll remounts (~3ms per message). useMemo doesn't survive * unmount->remount, so scrolling back to a previously-visible message * re-parses. Messages are immutable in history; same content -> same tokens. * Keyed by hash to avoid retaining full content strings. */ /** * Simple FNV-1a 32-bit hash. Fast enough for cache keys; no crypto needed. */ function fnv1aHash(str: string): string { let hash = 0x811c9dc5 for (let i = 0; i < str.length; i++) { hash ^= str.charCodeAt(i) hash = (hash * 0x01000193) | 0 } // Convert to unsigned hex string return (hash >>> 0).toString(16) } export class LRUTokenCache { private readonly maxSize: number private readonly cache = new Map() constructor(maxSize = 500) { this.maxSize = maxSize } /** * Get a cached value by content string, promoting it to MRU position. * Returns undefined on miss. */ get(content: string): T | undefined { const key = fnv1aHash(content) const hit = this.cache.get(key) if (hit !== undefined) { // Promote to MRU — without this the eviction is FIFO (scrolling back // to an early message evicts the very item you're looking at). this.cache.delete(key) this.cache.set(key, hit) return hit } return undefined } /** * Store a value keyed by content string. Evicts LRU entry if at capacity. */ set(content: string, value: T): void { const key = fnv1aHash(content) if (this.cache.size >= this.maxSize) { // LRU-ish: drop oldest. Map preserves insertion order. const first = this.cache.keys().next().value if (first !== undefined) this.cache.delete(first) } this.cache.set(key, value) } /** Number of cached entries. */ get size(): number { return this.cache.size } /** Remove all cached entries. */ clear(): void { this.cache.clear() } }