package zitcli
import (
"container/list"
"sync"
)
// lru is a byte-bounded, least-recently-used memo over zit reads.
//
// Bounded by BYTES, not entries, because the values are store content:
// an entry-count cap is fine for listings and meaningless for anything
// whose entries vary by orders of magnitude — one Cat can be a 30MB
// blob, and sixty-four of them is two gigabytes of resident memory. A
// diff has the same shape: a one-line fix and a thousand-file merge are
// both one entry and nothing like one cost.
//
// Per repository rather than shared across the fleet. A shared budget
// uses memory better, but it lets one busy store evict another's
// entries, so a reader on a quiet repository pays for traffic they have
// nothing to do with. Predictable beats optimal here.
//
// EVERY key must be derived from an immutable version. That is not a
// property this type can check, so it is the caller's obligation and the
// reason ops resolve names to hashes before reading — see
// Deps.resolveVersion. A moving name cached here would serve stale
// content forever.
type lru[V any] struct {
mu sync.Mutex
max int64
used int64
ll *list.List // front = most recently used
items map[string]*list.Element // key -> element holding *entry[V]
sizeOf func(V) int64
}
type entry[V any] struct {
key string
val V
size int64
}
func newLRU[V any](max int64, sizeOf func(V) int64) *lru[V] {
return &lru[V]{max: max, ll: list.New(), items: map[string]*list.Element{}, sizeOf: sizeOf}
}
// get returns a cached value and marks it recently used. The value is
// the cached one, so callers must not mutate it — every caller hands it
// straight to a reader or a parser.
func (c *lru[V]) get(key string) (V, bool) {
var zero V
if c == nil {
return zero, false
}
c.mu.Lock()
defer c.mu.Unlock()
el, ok := c.items[key]
if !ok {
return zero, false
}
c.ll.MoveToFront(el)
return el.Value.(*entry[V]).val, true
}
// put stores a value, evicting least-recently-used entries until it
// fits. A single value larger than the whole budget is NOT cached and
// not an error: admitting it would evict everything else to hold one
// item, which is worse than missing.
func (c *lru[V]) put(key string, val V) {
if c == nil {
return
}
size := c.sizeOf(val)
if size > c.max {
return
}
c.mu.Lock()
defer c.mu.Unlock()
if el, ok := c.items[key]; ok {
e := el.Value.(*entry[V])
c.used -= e.size
e.val, e.size = val, size
c.used += e.size
c.ll.MoveToFront(el)
return
}
e := &entry[V]{key: key, val: val, size: size}
c.items[key] = c.ll.PushFront(e)
c.used += e.size
for c.used > c.max {
back := c.ll.Back()
if back == nil {
return
}
old := c.ll.Remove(back).(*entry[V])
delete(c.items, old.key)
c.used -= old.size
}
}
// stats reports entries and bytes held, for tests and diagnostics.
func (c *lru[V]) stats() (entries int, bytes int64) {
if c == nil {
return 0, 0
}
c.mu.Lock()
defer c.mu.Unlock()
return len(c.items), c.used
}
// blobCache is the LRU over file contents.
type blobCache = lru[[]byte]
func newBlobCache(max int64) *blobCache {
return newLRU[[]byte](max, func(v []byte) int64 { return int64(len(v)) })
}
// diffCache is the LRU over check-in diffs.
//
// The pair of hashes is immutable, so a hit is permanently correct and
// this memo never needs invalidating — the reason a diff is worth
// caching at all. What it does need is a bound that reflects what a
// diff can weigh, which is why it is the same byte-budgeted structure
// as the blob cache rather than a count of entries.
type diffCache = lru[diffResult]
func newDiffCache(max int64) *diffCache {
return newLRU[diffResult](max, func(v diffResult) int64 {
// The unified text dominates; the entry list is names and
// statuses. Counted rather than ignored because a merge across
// a thousand files carries a thousand of them.
n := int64(len(v.unified))
for _, e := range v.entries {
n += int64(len(e.Name) + len(e.Status))
}
return n
})
}