180 lines
5.1 KiB
Go
180 lines
5.1 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/readmodelcache"
|
|
)
|
|
|
|
const storyPinnedStoriesCacheTTL = 30 * time.Minute
|
|
|
|
type storyPinnedStoriesCacheKey struct {
|
|
viewerUserID int64
|
|
peer domain.Peer
|
|
offsetID int
|
|
limit int
|
|
}
|
|
|
|
// storyPinnedStoriesCache 缓存某 viewer 对某 peer 的置顶故事分页结果,由统一缓存原语承载。
|
|
// key 含 (offsetID,limit) 分页维度——LRU 单条驱逐终于给这个 page-key 上界,消掉原先无界增长。
|
|
type storyPinnedStoriesCache struct {
|
|
cache *readmodelcache.Cache[storyPinnedStoriesCacheKey, domain.StoryList]
|
|
}
|
|
|
|
func newStoryPinnedStoriesCache(now func() time.Time) *storyPinnedStoriesCache {
|
|
return &storyPinnedStoriesCache{
|
|
cache: readmodelcache.New[storyPinnedStoriesCacheKey, domain.StoryList](readmodelcache.Config[storyPinnedStoriesCacheKey, domain.StoryList]{
|
|
MaxEntries: storyProjectionCacheMaxEntries,
|
|
TTL: storyPinnedStoriesCacheTTL,
|
|
Now: now,
|
|
Clone: cloneStoryListForCache,
|
|
}),
|
|
}
|
|
}
|
|
|
|
func storyPinnedStoriesKey(viewerUserID int64, peer domain.Peer, offsetID, limit int) storyPinnedStoriesCacheKey {
|
|
if offsetID < 0 {
|
|
offsetID = 0
|
|
}
|
|
if limit <= 0 || limit > domain.MaxStoryListLimit {
|
|
limit = domain.MaxStoryListLimit
|
|
}
|
|
return storyPinnedStoriesCacheKey{
|
|
viewerUserID: viewerUserID,
|
|
peer: peer,
|
|
offsetID: offsetID,
|
|
limit: limit,
|
|
}
|
|
}
|
|
|
|
func (c *storyPinnedStoriesCache) getOrLoad(ctx context.Context, key storyPinnedStoriesCacheKey, load func() (domain.StoryList, error)) (domain.StoryList, error) {
|
|
if c == nil || key.viewerUserID == 0 || key.peer.ID == 0 {
|
|
return load()
|
|
}
|
|
return c.cache.GetOrLoad(ctx, key, load)
|
|
}
|
|
|
|
func (c *storyPinnedStoriesCache) Delete(viewerUserID int64, peer domain.Peer) {
|
|
if c == nil || viewerUserID == 0 || peer.ID == 0 {
|
|
return
|
|
}
|
|
c.cache.InvalidateWhere(func(k storyPinnedStoriesCacheKey) bool {
|
|
return k.viewerUserID == viewerUserID && k.peer == peer
|
|
})
|
|
}
|
|
|
|
func (c *storyPinnedStoriesCache) DeleteViewer(viewerUserID int64) {
|
|
if c == nil || viewerUserID == 0 {
|
|
return
|
|
}
|
|
c.cache.InvalidateWhere(func(k storyPinnedStoriesCacheKey) bool { return k.viewerUserID == viewerUserID })
|
|
}
|
|
|
|
func (c *storyPinnedStoriesCache) DeletePeer(peer domain.Peer) {
|
|
if c == nil || peer.ID == 0 {
|
|
return
|
|
}
|
|
c.cache.InvalidateWhere(func(k storyPinnedStoriesCacheKey) bool { return k.peer == peer })
|
|
}
|
|
|
|
func (c *storyPinnedStoriesCache) Flush() {
|
|
if c == nil {
|
|
return
|
|
}
|
|
c.cache.Flush()
|
|
}
|
|
|
|
func cloneStoryListForCache(in domain.StoryList) domain.StoryList {
|
|
in.Stories = cloneStoriesForCache(in.Stories)
|
|
in.PinnedToTop = append([]int(nil), in.PinnedToTop...)
|
|
in.Peers = clonePeerStoriesForCache(in.Peers)
|
|
in.Users = append([]domain.User(nil), in.Users...)
|
|
in.Channels = append([]domain.Channel(nil), in.Channels...)
|
|
return in
|
|
}
|
|
|
|
func clonePeerStoriesForCache(in []domain.PeerStories) []domain.PeerStories {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]domain.PeerStories, len(in))
|
|
for i, item := range in {
|
|
out[i] = item
|
|
out[i].Stories = cloneStoriesForCache(item.Stories)
|
|
out[i].Users = append([]domain.User(nil), item.Users...)
|
|
out[i].Channels = append([]domain.Channel(nil), item.Channels...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneStoriesForCache(in []domain.Story) []domain.Story {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]domain.Story, len(in))
|
|
for i, story := range in {
|
|
out[i] = cloneStoryForCache(story)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneStoryForCache(story domain.Story) domain.Story {
|
|
story.PrivacyRules = clonePrivacyRulesForCache(story.PrivacyRules)
|
|
story.AllowUserIDs = append([]int64(nil), story.AllowUserIDs...)
|
|
story.DisallowUserIDs = append([]int64(nil), story.DisallowUserIDs...)
|
|
story.Entities = append([]domain.MessageEntity(nil), story.Entities...)
|
|
story.MediaAreas = cloneStoryMediaAreasForCache(story.MediaAreas)
|
|
if story.Forward != nil {
|
|
forward := *story.Forward
|
|
story.Forward = &forward
|
|
}
|
|
story.Views.Reactions = append([]domain.ChannelMessageReactionCount(nil), story.Views.Reactions...)
|
|
story.Views.RecentViewers = append([]int64(nil), story.Views.RecentViewers...)
|
|
if story.SentReaction != nil {
|
|
reaction := *story.SentReaction
|
|
story.SentReaction = &reaction
|
|
}
|
|
return story
|
|
}
|
|
|
|
func clonePrivacyRulesForCache(in []domain.PrivacyRule) []domain.PrivacyRule {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]domain.PrivacyRule, len(in))
|
|
for i, rule := range in {
|
|
out[i] = rule
|
|
out[i].UserIDs = append([]int64(nil), rule.UserIDs...)
|
|
out[i].ChatIDs = append([]int64(nil), rule.ChatIDs...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneStoryMediaAreasForCache(in []domain.StoryMediaArea) []domain.StoryMediaArea {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]domain.StoryMediaArea, len(in))
|
|
for i, area := range in {
|
|
out[i] = area
|
|
if area.Reaction != nil {
|
|
reaction := *area.Reaction
|
|
out[i].Reaction = &reaction
|
|
}
|
|
if area.Geo != nil {
|
|
geo := *area.Geo
|
|
out[i].Geo = &geo
|
|
}
|
|
if area.GeoAddress != nil {
|
|
address := *area.GeoAddress
|
|
out[i].GeoAddress = &address
|
|
}
|
|
if area.Venue != nil {
|
|
venue := *area.Venue
|
|
out[i].Venue = &venue
|
|
}
|
|
}
|
|
return out
|
|
}
|