package rpc import ( "context" "errors" appreadmodel "telesrv/internal/app/readmodel" "telesrv/internal/domain" "telesrv/internal/readmodelcache" "telesrv/internal/store" ) const ( defaultStoryActivePeerCacheMaxEntries = 1_000_000 defaultStoryHiddenListCacheMaxEntries = 100_000 defaultStoryHiddenListCacheMaxBytes = 64 << 20 missingStoryReadModelHash = int64(-1) ) var errStorySparseProjectionUnavailable = errors.New("story sparse projection read model unavailable") type storySparseProjectionProvider interface { ActiveStoryPeerExpirations(ctx context.Context, peers []domain.Peer, now int) (map[domain.Peer]int, error) ListHiddenStoryPeers(ctx context.Context, viewerUserID int64) ([]domain.Peer, error) } type activeStoryPeerFact struct { maxExpireAt int } type hiddenStoryPeerSet map[domain.Peer]struct{} // storySparseProjectionCache avoids a viewer×peer negative matrix. Active // existence is shared by peer; hidden preferences are stored once per viewer. type storySparseProjectionCache struct { versions store.ReadModelVersionStore active *readmodelcache.Cache[domain.Peer, activeStoryPeerFact] hidden *readmodelcache.Cache[int64, hiddenStoryPeerSet] } func newStorySparseProjectionCache( versions store.ReadModelVersionStore, activeMaxEntries int, hiddenMaxEntries int, hiddenMaxBytes int64, ) *storySparseProjectionCache { if versions == nil { return nil } if activeMaxEntries <= 0 { activeMaxEntries = defaultStoryActivePeerCacheMaxEntries } if hiddenMaxEntries <= 0 { hiddenMaxEntries = defaultStoryHiddenListCacheMaxEntries } if hiddenMaxBytes <= 0 { hiddenMaxBytes = defaultStoryHiddenListCacheMaxBytes } return &storySparseProjectionCache{ versions: versions, active: readmodelcache.New[domain.Peer, activeStoryPeerFact](readmodelcache.Config[domain.Peer, activeStoryPeerFact]{ MaxEntries: activeMaxEntries, }), hidden: readmodelcache.New[int64, hiddenStoryPeerSet](readmodelcache.Config[int64, hiddenStoryPeerSet]{ MaxEntries: hiddenMaxEntries, MaxWeight: hiddenMaxBytes, Weight: func(value hiddenStoryPeerSet) int64 { return 64 + int64(len(value))*32 }, Clone: cloneHiddenStoryPeerSet, }), } } func (c *storySparseProjectionCache) activePeers( ctx context.Context, provider storySparseProjectionProvider, peers []domain.Peer, now int, ) ([]domain.Peer, error) { if c == nil || c.versions == nil || c.active == nil || provider == nil { return nil, errStorySparseProjectionUnavailable } peers = uniqueStoryProjectionPeers(peers) if len(peers) == 0 { return nil, nil } for _, peer := range peers { if fact, ok := c.active.Peek(peer); ok && fact.maxExpireAt > 0 && fact.maxExpireAt <= now { c.active.Invalidate(peer) } } keys := make([]store.ReadModelKey, 0, len(peers)) for _, peer := range peers { keys = append(keys, store.ReadModelKey{ Model: appreadmodel.ModelStoryPeer, PeerType: peer.Type, PeerID: peer.ID, }) } hashes, err := c.versions.ReadModelHashes(ctx, keys) if err != nil { return nil, err } peerHashes := make(map[domain.Peer]int64, len(peers)) for _, key := range keys { hash := hashes[key] if hash == 0 { hash = missingStoryReadModelHash } peerHashes[domain.Peer{Type: key.PeerType, ID: key.PeerID}] = hash } values, err := c.active.GetOrLoadBatch(ctx, peers, func(peer domain.Peer) (int64, bool) { return peerHashes[peer], true }, func(ctx context.Context, missing []domain.Peer) (map[domain.Peer]activeStoryPeerFact, error) { expirations, err := provider.ActiveStoryPeerExpirations(ctx, missing, now) if err != nil { return nil, err } out := make(map[domain.Peer]activeStoryPeerFact, len(missing)) for _, peer := range missing { out[peer] = activeStoryPeerFact{maxExpireAt: expirations[peer]} } return out, nil }) if err != nil { return nil, err } out := make([]domain.Peer, 0, len(peers)) for _, peer := range peers { if values[peer].maxExpireAt > now { out = append(out, peer) } } return out, nil } func (c *storySparseProjectionCache) hiddenPeers( ctx context.Context, provider storySparseProjectionProvider, viewerUserID int64, ) (hiddenStoryPeerSet, error) { if c == nil || c.versions == nil || c.hidden == nil || provider == nil || viewerUserID == 0 { return nil, errStorySparseProjectionUnavailable } key := store.ReadModelKey{ Model: appreadmodel.ModelStoryHiddenList, OwnerUserID: viewerUserID, PeerType: domain.PeerTypeUser, PeerID: viewerUserID, } hash, _, err := c.versions.ReadModelHash(ctx, key.Model, key.OwnerUserID, key.PeerType, key.PeerID) if err != nil { return nil, err } if hash == 0 { hash = missingStoryReadModelHash } return c.hidden.GetOrLoadVersioned(ctx, viewerUserID, hash, func() (hiddenStoryPeerSet, error) { peers, err := provider.ListHiddenStoryPeers(ctx, viewerUserID) if err != nil { return nil, err } out := make(hiddenStoryPeerSet, len(peers)) for _, peer := range peers { if peer.ID != 0 { out[peer] = struct{}{} } } return out, nil }) } func (c *storySparseProjectionCache) DeletePeer(peer domain.Peer) { if c != nil && peer.ID != 0 { c.active.Invalidate(peer) } } func (c *storySparseProjectionCache) DeleteViewer(viewerUserID int64) { if c != nil && viewerUserID != 0 { c.hidden.Invalidate(viewerUserID) } } func (c *storySparseProjectionCache) Flush() { if c != nil { c.active.Flush() c.hidden.Flush() } } func cloneHiddenStoryPeerSet(in hiddenStoryPeerSet) hiddenStoryPeerSet { if in == nil { return hiddenStoryPeerSet{} } out := make(hiddenStoryPeerSet, len(in)) for peer := range in { out[peer] = struct{}{} } return out } func uniqueStoryProjectionPeers(peers []domain.Peer) []domain.Peer { out := make([]domain.Peer, 0, len(peers)) seen := make(map[domain.Peer]struct{}, len(peers)) for _, peer := range peers { if peer.ID == 0 || (peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel) { continue } if _, ok := seen[peer]; ok { continue } seen[peer] = struct{}{} out = append(out, peer) } return out }