Ctrl K
ComunidadGuías…

internal/store/postgres/channel_boost_cache.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"time"

	"telesrv/internal/domain"
	"telesrv/internal/readmodelcache"
)

type channelBoostCacheKey struct {
	userID   int64
	peerType domain.PeerType
	peerID   int64
}

type channelBoostPeerTotalCacheKey struct {
	peerType domain.PeerType
	peerID   int64
}

// ChannelBoostCache caches the boost read projection, backed by the common cache primitive
// readmodelcache.Cache (single-entry LRU eviction / epoch guard / built-in singleflight, TTL driven by the primitive's clock).
//
// It is deliberately used only on non-transactional read paths; send permission checks and the boost apply transaction still query
// channel_boost_slots live. selfBoosts caches the viewer-scoped SelfBoostsApplied;
// peerTotals caches the channel's total active boost count for premium.getBoostsStatus.
// A boost's own expiry is filtered by SQL at load time (expires_at > now); this cache's TTL is only a freshness fallback,
// equivalent to the wall-clock now passed by the caller (at runtime now=nowUnix()), so migrating to the primitive's internal clock does not change behavior.
type ChannelBoostCache struct {
	selfBoosts *readmodelcache.Cache[channelBoostCacheKey, int]
	peerTotals *readmodelcache.Cache[channelBoostPeerTotalCacheKey, int]
}

// NewChannelBoostCache creates a boost cache with capacity max and freshness ttl; max<=0 or ttl<=0 returns nil (disabled).
func NewChannelBoostCache(max int, ttl time.Duration) *ChannelBoostCache {
	return newChannelBoostCache(max, ttl, nil)
}

// newChannelBoostCache allows injecting a clock, only for tests to advance the TTL deterministically; now=nil uses the real clock.
func newChannelBoostCache(max int, ttl time.Duration, now func() time.Time) *ChannelBoostCache {
	if ttl <= 0 {
		return nil
	}
	selfBoosts := readmodelcache.New[channelBoostCacheKey, int](readmodelcache.Config[channelBoostCacheKey, int]{
		MaxEntries: max,
		TTL:        ttl,
		Now:        now,
	})
	peerTotals := readmodelcache.New[channelBoostPeerTotalCacheKey, int](readmodelcache.Config[channelBoostPeerTotalCacheKey, int]{
		MaxEntries: max,
		TTL:        ttl,
		Now:        now,
	})
	if selfBoosts == nil || peerTotals == nil {
		return nil
	}
	return &ChannelBoostCache{selfBoosts: selfBoosts, peerTotals: peerTotals}
}

func (c *ChannelBoostCache) get(userID int64, peer domain.Peer) (int, bool) {
	key, ok := newChannelBoostCacheKey(userID, peer)
	if c == nil || !ok {
		return 0, false
	}
	return c.selfBoosts.Peek(key)
}

func (c *ChannelBoostCache) getOrLoad(ctx context.Context, userID int64, peer domain.Peer, load func() (int, error)) (int, error) {
	if c == nil {
		return load()
	}
	key, ok := newChannelBoostCacheKey(userID, peer)
	if !ok {
		return load()
	}
	return c.selfBoosts.GetOrLoad(ctx, key, load)
}

func (c *ChannelBoostCache) put(userID int64, peer domain.Peer, boosts int) {
	key, ok := newChannelBoostCacheKey(userID, peer)
	if c == nil || !ok {
		return
	}
	c.selfBoosts.Store(key, boosts)
}

func (c *ChannelBoostCache) delete(userID int64, peer domain.Peer) {
	key, ok := newChannelBoostCacheKey(userID, peer)
	if c == nil || !ok {
		return
	}
	c.selfBoosts.Invalidate(key)
}

func (c *ChannelBoostCache) deleteUser(userID int64) {
	if c == nil || userID == 0 {
		return
	}
	c.selfBoosts.InvalidateWhere(func(k channelBoostCacheKey) bool { return k.userID == userID })
}

func (c *ChannelBoostCache) getPeerTotal(peer domain.Peer) (int, bool) {
	key, ok := newChannelBoostPeerTotalCacheKey(peer)
	if c == nil || !ok {
		return 0, false
	}
	return c.peerTotals.Peek(key)
}

func (c *ChannelBoostCache) getPeerTotalOrLoad(ctx context.Context, peer domain.Peer, load func() (int, error)) (int, error) {
	if c == nil {
		return load()
	}
	key, ok := newChannelBoostPeerTotalCacheKey(peer)
	if !ok {
		return load()
	}
	return c.peerTotals.GetOrLoad(ctx, key, load)
}

func (c *ChannelBoostCache) putPeerTotal(peer domain.Peer, boosts int) {
	key, ok := newChannelBoostPeerTotalCacheKey(peer)
	if c == nil || !ok {
		return
	}
	c.peerTotals.Store(key, boosts)
}

func (c *ChannelBoostCache) deletePeerTotal(peer domain.Peer) {
	key, ok := newChannelBoostPeerTotalCacheKey(peer)
	if c == nil || !ok {
		return
	}
	c.peerTotals.Invalidate(key)
}

func (c *ChannelBoostCache) flush() {
	if c == nil {
		return
	}
	c.selfBoosts.Flush()
	c.peerTotals.Flush()
}

func newChannelBoostCacheKey(userID int64, peer domain.Peer) (channelBoostCacheKey, bool) {
	if userID == 0 || peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
		return channelBoostCacheKey{}, false
	}
	return channelBoostCacheKey{userID: userID, peerType: peer.Type, peerID: peer.ID}, true
}

func newChannelBoostPeerTotalCacheKey(peer domain.Peer) (channelBoostPeerTotalCacheKey, bool) {
	if peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
		return channelBoostPeerTotalCacheKey{}, false
	}
	return channelBoostPeerTotalCacheKey{peerType: peer.Type, peerID: peer.ID}, true
}