package middleware import ( "sync" "time" "github.com/treytartt/honeydue-api/internal/models" ) // userCacheEntry holds a cached user record with an expiration time. type userCacheEntry struct { user *models.User expiresAt time.Time } // UserCache is a concurrency-safe in-memory cache for User records, keyed by // user ID. Entries expire after a configurable TTL. The cache uses a sync.Map // for lock-free reads on the hot path, with periodic lazy eviction of stale // entries during Set operations. type UserCache struct { store sync.Map ttl time.Duration lastGC time.Time gcMu sync.Mutex gcEvery time.Duration } // NewUserCache creates a UserCache with the given TTL for entries. func NewUserCache(ttl time.Duration) *UserCache { return &UserCache{ ttl: ttl, lastGC: time.Now(), gcEvery: 2 * time.Minute, } } // Get returns a cached user by ID, or nil if not found or expired. func (c *UserCache) Get(userID uint) *models.User { val, ok := c.store.Load(userID) if !ok { return nil } entry := val.(*userCacheEntry) if time.Now().After(entry.expiresAt) { c.store.Delete(userID) return nil } // Return a shallow copy so callers cannot mutate the cached value. user := *entry.user return &user } // Set stores a user in the cache. It also triggers a background garbage- // collection sweep if enough time has elapsed since the last one. func (c *UserCache) Set(user *models.User) { // Store a copy to prevent external mutation of the cached object. copied := *user c.store.Store(user.ID, &userCacheEntry{ user: &copied, expiresAt: time.Now().Add(c.ttl), }) c.maybeGC() } // Invalidate removes a user from the cache by ID. func (c *UserCache) Invalidate(userID uint) { c.store.Delete(userID) } // maybeGC lazily sweeps expired entries at most once per gcEvery interval. func (c *UserCache) maybeGC() { c.gcMu.Lock() if time.Since(c.lastGC) < c.gcEvery { c.gcMu.Unlock() return } c.lastGC = time.Now() c.gcMu.Unlock() now := time.Now() c.store.Range(func(key, value any) bool { entry := value.(*userCacheEntry) if now.After(entry.expiresAt) { c.store.Delete(key) } return true }) } // TimezoneCache tracks the last-known timezone per user ID so the timezone // middleware only writes to the database when the value actually changes. type TimezoneCache struct { store sync.Map } // NewTimezoneCache creates a new TimezoneCache. func NewTimezoneCache() *TimezoneCache { return &TimezoneCache{} } // GetAndCompare returns true if the cached timezone for the user matches tz. // If the timezone is different (or not yet cached), it updates the cache and // returns false, signaling that a DB write is needed. func (tc *TimezoneCache) GetAndCompare(userID uint, tz string) (unchanged bool) { val, loaded := tc.store.Load(userID) if loaded { if cached, ok := val.(string); ok && cached == tz { return true } } tc.store.Store(userID, tz) return false }