package postgres import ( "context" "telesrv/internal/domain" "telesrv/internal/readmodelcache" ) // ChannelRowCache 是「共享频道行」(channels 表一行 → domain.Channel) 的进程内强一致缓存, // 由统一缓存原语 readmodelcache.Cache 承载(LRU 单条驱逐 / epoch 守卫 / singleflight 内建)。 // // 只缓存全 viewer 通用、变更不频繁的频道行;per-viewer 的 member(权限/读态) 由 // ChannelMemberCache 以 (channel_id,user_id) 单独隔离,dialog(top/未读)、boost 仍实时查 PG。 // // 一致性靠统一 read-model PG LISTEN/NOTIFY 实时失效(见 ReadModelChangeListener)而非 TTL: // channels 表上的 read-model 触发器在提交时发布 channel_base 事件,listener 收到即 delete // 对应条目;listener (重)连接时整表 flush,兜住断连窗口内丢失的通知。无 TTL,故 epoch 守卫 // 是防永久 stale 的唯一保障(已内建于原语)。 // // 仅在连接池句柄(非事务)上消费(见 ChannelStore.cacheActive):事务内一律实时读,保证读己写。 type ChannelRowCache struct { cache *readmodelcache.Cache[int64, domain.Channel] } // NewChannelRowCache 创建容量为 max 的频道行缓存;max<=0 返回 nil(表示禁用,调用方按 nil 跳过)。 func NewChannelRowCache(max int) *ChannelRowCache { cache := readmodelcache.New[int64, domain.Channel](readmodelcache.Config[int64, domain.Channel]{ MaxEntries: max, Clone: cloneChannelRow, }) if cache == nil { return nil } return &ChannelRowCache{cache: cache} } func (c *ChannelRowCache) get(id int64) (domain.Channel, bool) { if c == nil || id == 0 { return domain.Channel{}, false } return c.cache.Peek(id) } func (c *ChannelRowCache) getOrLoad(ctx context.Context, id int64, load func() (domain.Channel, error)) (domain.Channel, error) { if c == nil { return load() } return c.cache.GetOrLoad(ctx, id, load) } func (c *ChannelRowCache) put(ch domain.Channel) { if c == nil || ch.ID == 0 { return } c.cache.Store(ch.ID, ch) } // delete 失效单个频道(listener 收到该 id 的变更通知时调用)。 func (c *ChannelRowCache) delete(id int64) { if c == nil || id == 0 { return } c.cache.Invalidate(id) } // flush 清空整表(listener 每次 (重)连接、建立 LISTEN 后调用,兜住断连窗口内丢失的通知)。 func (c *ChannelRowCache) flush() { if c == nil { return } c.cache.Flush() } // cloneChannelRow 深拷频道行的可变字段(切片),使缓存与调用方互不别名污染。 func cloneChannelRow(ch domain.Channel) domain.Channel { if ch.PhotoStripped != nil { ch.PhotoStripped = append([]byte(nil), ch.PhotoStripped...) } if ch.ReactionPolicy.Emoticons != nil { ch.ReactionPolicy.Emoticons = append([]string(nil), ch.ReactionPolicy.Emoticons...) } if ch.ReactionPolicy.CustomEmojiIDs != nil { ch.ReactionPolicy.CustomEmojiIDs = append([]int64(nil), ch.ReactionPolicy.CustomEmojiIDs...) } ch.Wallpaper = domain.CloneWallpaperPtr(ch.Wallpaper) return ch }