package mempot import ( "context" "fmt" "sync" "time" ) // DefaultConfig contains all default values for a Cache. var DefaultConfig = Config{ DefaultTTL: time.Minute * 15, CleanupInterval: time.Minute * 5, } // Config allows to alter the configuration of a Cache. type Config struct { // DefaultTTL is used by Cache.Set for the Item.TTL. // If set to 0, the Item will not expire. // // Default: 15m DefaultTTL time.Duration // CleanupInterval is used for the Ticker in the cleanup goroutine. // If set to 0, no cleanup goroutine will be created. // // Default: 5m CleanupInterval time.Duration } // Cache holds the data you want to cache in memory. type Cache[K comparable, T any] struct { mut sync.RWMutex data map[K]Item[T] ctx context.Context cfg Config } // Item is a unit of typed data which can be cached and has an expiration as Unix time in milliseconds. type Item[T any] struct { // Data holds the assigned data of the Item. Data T // TTL is the expiration time as Unix time in milliseconds. // If set to 0, the Item will not expire. TTL int64 } // Expired returns true if the data of the Item has expired. func (i *Item[T]) Expired() bool { if i.TTL == 0 { return false } return time.Now().UnixMilli() > i.TTL } func newItem[T any](data T, ttl time.Duration) Item[T] { if ttl == 0 { return Item[T]{Data: data, TTL: 0} } return Item[T]{Data: data, TTL: time.Now().Add(ttl).UnixMilli()} } // NewCache create a new Cache instance with K as key and T as data. // If the context is canceled, the Cache will stop the cleanup goroutine. func NewCache[K comparable, T any](ctx context.Context, cfg Config) *Cache[K, T] { c := &Cache[K, T]{ data: make(map[K]Item[T]), ctx: ctx, cfg: DefaultConfig, } if cfg.DefaultTTL > 0 { c.cfg.DefaultTTL = cfg.DefaultTTL } if cfg.CleanupInterval > 0 { c.cfg.CleanupInterval = cfg.CleanupInterval } if c.cfg.CleanupInterval > 0 { go c.cleanup() } return c } // Set will add an Item to the Cache with the default time-to-live. func (c *Cache[K, T]) Set(key K, value T) { c.SetWithTTL(key, value, c.cfg.DefaultTTL) } // SetWithTTL will add an Item to the Cache with the given time-to-live. func (c *Cache[K, T]) SetWithTTL(key K, data T, ttl time.Duration) { c.mut.Lock() c.data[key] = newItem(data, ttl) c.mut.Unlock() } // Get returns an Item and true if the Item was found in the Cache and has not been expired. // An empty Item and false is returned when the Item was not found or has been expired. func (c *Cache[K, T]) Get(key K) (Item[T], bool) { c.mut.RLock() item, ok := c.data[key] c.mut.RUnlock() if item.Expired() { return Item[T]{}, false } return item, ok } // QueryFunc is a function to retrieve data which will be put into the Cache. type QueryFunc[K comparable, T any] func(key K) (T, error) // Remember tries to get the Item from the Cache, if the Item is not found or expired QueryFunc is called // to retrieve the data from source and put it into the Cache. func (c *Cache[K, T]) Remember(key K, query QueryFunc[K, T]) (Item[T], error) { return c.RememberWithTTL(key, query, c.cfg.DefaultTTL) } // RememberWithTTL tries to get the Item from the Cache, if the Item is not found or expired QueryFunc is called // to retrieve the data from source and put it into the Cache with the given time-to-live. func (c *Cache[K, T]) RememberWithTTL(key K, query QueryFunc[K, T], ttl time.Duration) (Item[T], error) { item, ok := c.Get(key) if ok { return item, nil } data, err := query(key) if err != nil { return Item[T]{}, fmt.Errorf("failed to query data: %w", err) } c.SetWithTTL(key, data, ttl) return newItem(data, ttl), nil } // Delete removes an Item from the Cache. func (c *Cache[K, T]) Delete(key K) { c.mut.Lock() delete(c.data, key) c.mut.Unlock() } // Reset removes all Items from the Cache. func (c *Cache[K, T]) Reset() { c.mut.Lock() c.data = make(map[K]Item[T]) c.mut.Unlock() } func (c *Cache[K, T]) cleanup() { ticker := time.NewTicker(c.cfg.CleanupInterval) for { select { case <-c.ctx.Done(): ticker.Stop() return case <-ticker.C: toBeDeleted := make([]K, 0) c.mut.RLock() for key, item := range c.data { if item.Expired() { toBeDeleted = append(toBeDeleted, key) } } c.mut.RUnlock() c.mut.Lock() for _, key := range toBeDeleted { delete(c.data, key) } c.mut.Unlock() } } }