package cachemap import ( "context" "iter" "time" "git.tordarus.net/tordarus/cmap" ) // CacheMap represents a map for caching values for a specific amount of time. // CacheMap is thread-safe. type CacheMap[K comparable, V any] struct { data *cmap.Map[K, cacheMapEntry[V]] defaultCacheDuration time.Duration autoCleanUpEnabled bool } // New returns a CacheMap which caches its items for the given cache duration (if not specified otherwise). // It does not collect its own garbage until Get() or CleanUp() is called func New[K comparable, V any](defaultCacheDuration time.Duration) *CacheMap[K, V] { return &CacheMap[K, V]{ data: cmap.New[K, cacheMapEntry[V]](), defaultCacheDuration: defaultCacheDuration, } } // New returns a CacheMap which caches its items for the given cache duration (if not specified otherwise). // It collects its own garbage periodically. Memory leaks are not possible but the map is locked for the cleanup process func NewSelfCleaning[K comparable, V any](ctx context.Context, defaultCacheDuration, cleanUpInterval time.Duration) *CacheMap[K, V] { cacheMap := New[K, V](defaultCacheDuration) cacheMap.AutoCleanUp(ctx, cleanUpInterval) return cacheMap } // Put puts the given key-value pair into the cache map with the CacheMaps default cache duration func (cm *CacheMap[K, V]) Put(key K, value V) { cm.data.Put(key, cacheMapEntry[V]{ Value: value, CacheTime: time.Now(), CacheDuration: cm.defaultCacheDuration, }) } // Put puts the given key-value pair into the cache map with the given cache duration func (cm *CacheMap[K, V]) PutFor(key K, value V, cacheDuration time.Duration) { cm.data.Put(key, cacheMapEntry[V]{ Value: value, CacheTime: time.Now(), CacheDuration: cacheDuration, }) } // Get returns the value associated with the given key and a boolean indicating an existing entry. // If the entry associated with the key is expired, the key is deleted from the map immediately. // KeepAlive is called for the entry if it is not expired yet. func (cm *CacheMap[K, V]) Get(key K) (V, bool) { entry, ok := cm.data.GetHas(key) if !ok { return *new(V), false } if !entry.IsAlive() { cm.data.Delete(key) return *new(V), false } cm.KeepAlive(key) return entry.Value, true } // KeepAlive restarts the cache duration for the entry associated with the given key. // This is the same as deleting and re-adding the entry with the same cache duration. func (cm *CacheMap[K, V]) KeepAlive(key K) { entry, ok := cm.data.GetHas(key) if ok { entry.CacheTime = time.Now() cm.data.Put(key, entry) } } // Iterate returns an iterator for the CacheMap to use in for-range loops func (cm *CacheMap[K, V]) Iterate() iter.Seq2[K, V] { return func(yield func(K, V) bool) { for key, value := range cm.data.Iterate() { if !yield(key, value.Value) { return } } } } // CleanUp iterates through the CacheMap and deletes all expired entries. // The CacheMap is locked during the cleanup process. func (cm *CacheMap[K, V]) CleanUp() { cm.data.Do(func(m map[K]cacheMapEntry[V]) { for key, entry := range m { if !entry.IsAlive() { delete(m, key) } } }) } // AutoCleanUp periodically calls CleanUp() to ensure that no garbage is accumulating // over long periods of time without Get calls. func (cm *CacheMap[K, V]) AutoCleanUp(ctx context.Context, interval time.Duration) { if cm.autoCleanUpEnabled { return } cm.autoCleanUpEnabled = true go func() { ticker := time.NewTicker(interval) defer ticker.Stop() defer func() { cm.autoCleanUpEnabled = false }() for { select { case <-ticker.C: cm.CleanUp() case <-ctx.Done(): return } } }() } type cacheMapEntry[T any] struct { Value T CacheTime time.Time CacheDuration time.Duration } func (e cacheMapEntry[T]) IsAlive() bool { return time.Since(e.CacheTime) < e.CacheDuration }