// Package cache provides a small, concurrency-safe TTL + LRU cache. It's a // generic building block for sharing an expensive computation across requests // (e.g. the reader shares one relevance match between /api/stream and // /api/facets): a hit returns the stored value, a miss recomputes. The TTL // bounds staleness; the capacity bounds memory. package cache import ( "container/list" "sync" "time" ) type entry[K comparable, V any] struct { key K val V at time.Time } // TTLLRU is a fixed-capacity cache that evicts the least-recently-used entry on // overflow and treats entries older than ttl as misses. The zero value is not // usable — construct with New. All methods are safe for concurrent use. type TTLLRU[K comparable, V any] struct { mu sync.Mutex ll *list.List // front = most-recently-used m map[K]*list.Element cap int ttl time.Duration } // New returns a cache holding up to capacity entries, each valid for ttl. func New[K comparable, V any](capacity int, ttl time.Duration) *TTLLRU[K, V] { if capacity < 1 { capacity = 1 } return &TTLLRU[K, V]{ ll: list.New(), m: make(map[K]*list.Element, capacity), cap: capacity, ttl: ttl, } } // Get returns the cached value for key. The boolean is false on a miss or when // the entry has expired (in which case it is also evicted). A hit refreshes the // entry's recency. func (c *TTLLRU[K, V]) Get(key K) (V, bool) { c.mu.Lock() defer c.mu.Unlock() el, ok := c.m[key] if !ok { var zero V return zero, false } ent := el.Value.(*entry[K, V]) if time.Since(ent.at) > c.ttl { c.ll.Remove(el) delete(c.m, key) var zero V return zero, false } c.ll.MoveToFront(el) return ent.val, true } // Put stores val under key, refreshing its timestamp and recency, evicting the // least-recently-used entry if that exceeds the capacity. func (c *TTLLRU[K, V]) Put(key K, val V) { c.mu.Lock() defer c.mu.Unlock() if el, ok := c.m[key]; ok { ent := el.Value.(*entry[K, V]) ent.val = val ent.at = time.Now() c.ll.MoveToFront(el) return } c.m[key] = c.ll.PushFront(&entry[K, V]{key: key, val: val, at: time.Now()}) for c.ll.Len() > c.cap { back := c.ll.Back() if back == nil { break } c.ll.Remove(back) delete(c.m, back.Value.(*entry[K, V]).key) } }