merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -0,0 +1,142 @@
package userprojection
import (
"context"
"telesrv/internal/app/readmodel"
"telesrv/internal/domain"
"telesrv/internal/readmodelcache"
"telesrv/internal/store"
)
type accountFreezeFact struct {
value domain.AccountFreeze
found bool
}
// DurableUserProjectionFacts caches only viewer-independent durable overlays.
// Contact/privacy/presence decisions remain outside and are evaluated after
// these facts are loaded.
type DurableUserProjectionFacts struct {
freezes AccountFreezeProvider
versions store.ReadModelVersionStore
freezeCache *readmodelcache.Cache[int64, accountFreezeFact]
}
func NewDurableUserProjectionFacts(
freezes AccountFreezeProvider,
versions store.ReadModelVersionStore,
maxEntries int,
) *DurableUserProjectionFacts {
return &DurableUserProjectionFacts{
freezes: freezes,
versions: versions,
freezeCache: readmodelcache.New[int64, accountFreezeFact](readmodelcache.Config[int64, accountFreezeFact]{
MaxEntries: maxEntries,
}),
}
}
func (f *DurableUserProjectionFacts) AccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error) {
out := make(map[int64]domain.AccountFreeze)
ids := uniqueDurableFactUserIDs(userIDs)
if f == nil || f.freezes == nil || len(ids) == 0 {
return out, nil
}
hashes, err := f.factHashes(ctx, readmodel.ModelUserVisibility, ids)
if err != nil {
return nil, err
}
loaded, err := f.freezeCache.GetOrLoadBatch(ctx, ids,
func(userID int64) (int64, bool) {
hash := hashes[userID]
return hash, f.versions != nil && hash != 0
},
func(ctx context.Context, missing []int64) (map[int64]accountFreezeFact, error) {
values, err := f.freezes.AccountFreezes(ctx, missing)
if err != nil {
return nil, err
}
entries := make(map[int64]accountFreezeFact, len(missing))
for _, userID := range missing {
entry := accountFreezeFact{}
if value, ok := values[userID]; ok {
entry = accountFreezeFact{value: value, found: true}
}
entries[userID] = entry
}
return entries, nil
})
if err != nil {
return nil, err
}
for userID, entry := range loaded {
if entry.found {
out[userID] = entry.value
}
}
return out, nil
}
// AccountFreeze exposes the same versioned positive/negative cache to scalar
// RPC gates. It deliberately delegates to the batch path so gate reads and
// user/dialog projection cannot drift into separate cache semantics.
func (f *DurableUserProjectionFacts) AccountFreeze(ctx context.Context, userID int64) (domain.AccountFreeze, bool, error) {
if userID == 0 {
return domain.AccountFreeze{}, false, nil
}
items, err := f.AccountFreezes(ctx, []int64{userID})
if err != nil {
return domain.AccountFreeze{}, false, err
}
value, found := items[userID]
return value, found, nil
}
func (f *DurableUserProjectionFacts) factHashes(ctx context.Context, model string, userIDs []int64) (map[int64]int64, error) {
out := make(map[int64]int64, len(userIDs))
if f == nil || f.versions == nil {
return out, nil
}
keys := make([]store.ReadModelKey, 0, len(userIDs))
for _, userID := range userIDs {
keys = append(keys, store.ReadModelKey{Model: model, OwnerUserID: 0, PeerType: domain.PeerTypeUser, PeerID: userID})
}
rows, err := f.versions.ReadModelHashes(ctx, keys)
if err != nil {
return nil, err
}
for _, key := range keys {
out[key.PeerID] = rows[key]
}
return out, nil
}
func (f *DurableUserProjectionFacts) InvalidateAccountFreezeFact(userID int64) {
if f != nil && userID != 0 {
f.freezeCache.Invalidate(userID)
}
}
func (f *DurableUserProjectionFacts) FlushUserProjectionFactReadModel() {
if f != nil {
f.freezeCache.Flush()
}
}
func uniqueDurableFactUserIDs(ids []int64) []int64 {
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
}