package cache import ( "errors" "fmt" "sync/atomic" "time" "github.com/jellydator/ttlcache/v3" . "github.com/navidrome/navidrome/utils/gg" ) type SimpleCache[K comparable, V any] interface { Add(key K, value V) error AddWithTTL(key K, value V, ttl time.Duration) error Get(key K) (V, error) GetWithLoader(key K, loader func(key K) (V, time.Duration, error)) (V, error) Keys() []K Values() []V } type Options struct { SizeLimit uint64 DefaultTTL time.Duration } func NewSimpleCache[K comparable, V any](options ...Options) SimpleCache[K, V] { opts := []ttlcache.Option[K, V]{ ttlcache.WithDisableTouchOnHit[K, V](), } if len(options) > 0 { o := options[0] if o.SizeLimit > 0 { opts = append(opts, ttlcache.WithCapacity[K, V](o.SizeLimit)) } if o.DefaultTTL > 0 { opts = append(opts, ttlcache.WithTTL[K, V](o.DefaultTTL)) } } c := ttlcache.New[K, V](opts...) return &simpleCache[K, V]{ data: c, } } const evictionTimeout = 1 * time.Hour type simpleCache[K comparable, V any] struct { data *ttlcache.Cache[K, V] evictionDeadline atomic.Pointer[time.Time] } func (c *simpleCache[K, V]) Add(key K, value V) error { c.evictExpired() return c.AddWithTTL(key, value, ttlcache.DefaultTTL) } func (c *simpleCache[K, V]) AddWithTTL(key K, value V, ttl time.Duration) error { c.evictExpired() item := c.data.Set(key, value, ttl) if item == nil { return errors.New("failed to add item") } return nil } func (c *simpleCache[K, V]) Get(key K) (V, error) { item := c.data.Get(key) if item == nil { var zero V return zero, errors.New("item not found") } return item.Value(), nil } func (c *simpleCache[K, V]) GetWithLoader(key K, loader func(key K) (V, time.Duration, error)) (V, error) { var err error loaderWrapper := ttlcache.LoaderFunc[K, V]( func(t *ttlcache.Cache[K, V], key K) *ttlcache.Item[K, V] { c.evictExpired() var value V var ttl time.Duration value, ttl, err = loader(key) if err != nil { return nil } return t.Set(key, value, ttl) }, ) item := c.data.Get(key, ttlcache.WithLoader[K, V](loaderWrapper)) if item == nil { var zero V if err != nil { return zero, fmt.Errorf("cache error: loader returned %w", err) } return zero, errors.New("item not found") } return item.Value(), nil } func (c *simpleCache[K, V]) evictExpired() { if c.evictionDeadline.Load() == nil || c.evictionDeadline.Load().Before(time.Now()) { c.data.DeleteExpired() c.evictionDeadline.Store(P(time.Now().Add(evictionTimeout))) } } func (c *simpleCache[K, V]) Keys() []K { res := make([]K, 0, c.data.Len()) c.data.Range(func(item *ttlcache.Item[K, V]) bool { if !item.IsExpired() { res = append(res, item.Key()) } return true }) return res } func (c *simpleCache[K, V]) Values() []V { res := make([]V, 0, c.data.Len()) c.data.Range(func(item *ttlcache.Item[K, V]) bool { if !item.IsExpired() { res = append(res, item.Value()) } return true }) return res }