141 lines
3.4 KiB
Go
141 lines
3.4 KiB
Go
package peerview
|
||
|
||
import (
|
||
"context"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// UserResolver resolves users after applying viewer-specific projection.
|
||
type UserResolver interface {
|
||
ByIDs(ctx context.Context, viewerUserID int64, userIDs []int64) ([]domain.User, error)
|
||
}
|
||
|
||
// BatchCache caches projected users only for one update-building batch.
|
||
// The cache key includes viewerUserID because contacts, privacy, phone visibility
|
||
// and personal/fallback photos are viewer-specific.
|
||
type BatchCache struct {
|
||
users UserResolver
|
||
|
||
byViewer map[int64]map[int64]domain.User
|
||
missing map[int64]map[int64]struct{}
|
||
}
|
||
|
||
func NewBatchCache(users UserResolver) *BatchCache {
|
||
return &BatchCache{
|
||
users: users,
|
||
byViewer: make(map[int64]map[int64]domain.User),
|
||
missing: make(map[int64]map[int64]struct{}),
|
||
}
|
||
}
|
||
|
||
func (c *BatchCache) UsersForView(ctx context.Context, viewerUserID int64, ids []int64) ([]domain.User, error) {
|
||
unique := uniqueIDs(ids)
|
||
if len(unique) == 0 {
|
||
return nil, nil
|
||
}
|
||
byID := c.viewerUsers(viewerUserID)
|
||
missing := c.viewerMissing(viewerUserID)
|
||
load := make([]int64, 0, len(unique))
|
||
for _, id := range unique {
|
||
if _, ok := byID[id]; ok {
|
||
continue
|
||
}
|
||
if _, ok := missing[id]; ok {
|
||
continue
|
||
}
|
||
if u, ok := domain.SystemUserByID(id); ok {
|
||
byID[id] = u
|
||
continue
|
||
}
|
||
if c.users == nil || viewerUserID == 0 {
|
||
missing[id] = struct{}{}
|
||
continue
|
||
}
|
||
load = append(load, id)
|
||
}
|
||
var err error
|
||
if len(load) > 0 {
|
||
var resolved []domain.User
|
||
resolved, err = c.users.ByIDs(ctx, viewerUserID, load)
|
||
if err == nil {
|
||
found := make(map[int64]struct{}, len(resolved))
|
||
for _, u := range resolved {
|
||
if u.ID == 0 {
|
||
continue
|
||
}
|
||
byID[u.ID] = u
|
||
found[u.ID] = struct{}{}
|
||
}
|
||
for _, id := range load {
|
||
if _, ok := found[id]; !ok {
|
||
missing[id] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out := make([]domain.User, 0, len(unique))
|
||
for _, id := range unique {
|
||
if u, ok := byID[id]; ok {
|
||
out = append(out, u)
|
||
}
|
||
}
|
||
return out, err
|
||
}
|
||
|
||
// Prime 预热某 viewer 的已投影用户(fan-out 跨 viewer 模板化把 per-recipient ForViewer 折叠成
|
||
// 一次 ForViewers 后回填)。已存在的 id 不覆盖(保留同批已解析结果),避免预热与按需解析互相打架。
|
||
// 预热的是「投影后、未叠加实时 presence」的用户——与 UsersForView 缓存层一致,presence 仍由
|
||
// 上层在输出端叠加,故预热不破坏 presence 新鲜度。
|
||
func (c *BatchCache) Prime(viewerUserID int64, users []domain.User) {
|
||
if c == nil || viewerUserID == 0 || len(users) == 0 {
|
||
return
|
||
}
|
||
byID := c.viewerUsers(viewerUserID)
|
||
for _, u := range users {
|
||
if u.ID == 0 {
|
||
continue
|
||
}
|
||
if _, ok := byID[u.ID]; ok {
|
||
continue
|
||
}
|
||
byID[u.ID] = u
|
||
}
|
||
}
|
||
|
||
func (c *BatchCache) viewerUsers(viewerUserID int64) map[int64]domain.User {
|
||
if byID, ok := c.byViewer[viewerUserID]; ok {
|
||
return byID
|
||
}
|
||
byID := make(map[int64]domain.User)
|
||
c.byViewer[viewerUserID] = byID
|
||
return byID
|
||
}
|
||
|
||
func (c *BatchCache) viewerMissing(viewerUserID int64) map[int64]struct{} {
|
||
if missing, ok := c.missing[viewerUserID]; ok {
|
||
return missing
|
||
}
|
||
missing := make(map[int64]struct{})
|
||
c.missing[viewerUserID] = missing
|
||
return missing
|
||
}
|
||
|
||
func uniqueIDs(ids []int64) []int64 {
|
||
if len(ids) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]int64, 0, len(ids))
|
||
seen := make(map[int64]struct{}, len(ids))
|
||
for _, id := range ids {
|
||
if id == 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[id]; ok {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
out = append(out, id)
|
||
}
|
||
return out
|
||
}
|