Ctrl K
ComunidadGuías…

internal/app/userprojection/durable_user_facts.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}

type collectiblePhoneFact struct {
	value domain.CollectiblePhone
	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
	phones   CollectiblePhoneProvider
	versions store.ReadModelVersionStore

	freezeCache *readmodelcache.Cache[int64, accountFreezeFact]
	phoneCache  *readmodelcache.Cache[int64, collectiblePhoneFact]
}

func NewDurableUserProjectionFacts(
	freezes AccountFreezeProvider,
	phones CollectiblePhoneProvider,
	versions store.ReadModelVersionStore,
	maxEntries int,
) *DurableUserProjectionFacts {
	return &DurableUserProjectionFacts{
		freezes:  freezes,
		phones:   phones,
		versions: versions,
		freezeCache: readmodelcache.New[int64, accountFreezeFact](readmodelcache.Config[int64, accountFreezeFact]{
			MaxEntries: maxEntries,
		}),
		phoneCache: readmodelcache.New[int64, collectiblePhoneFact](readmodelcache.Config[int64, collectiblePhoneFact]{
			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) OwnedCollectiblePhones(ctx context.Context, userIDs []int64) (map[int64]domain.CollectiblePhone, error) {
	out := make(map[int64]domain.CollectiblePhone)
	ids := uniqueDurableFactUserIDs(userIDs)
	if f == nil || f.phones == nil || len(ids) == 0 {
		return out, nil
	}
	hashes, err := f.factHashes(ctx, readmodel.ModelUserCollectiblePhone, ids)
	if err != nil {
		return nil, err
	}
	loaded, err := f.phoneCache.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]collectiblePhoneFact, error) {
			values, err := f.phones.OwnedCollectiblePhones(ctx, missing)
			if err != nil {
				return nil, err
			}
			entries := make(map[int64]collectiblePhoneFact, len(missing))
			for _, userID := range missing {
				entry := collectiblePhoneFact{}
				if value, ok := values[userID]; ok {
					entry = collectiblePhoneFact{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
}

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) InvalidateCollectiblePhoneFact(userID int64) {
	if f != nil && userID != 0 {
		f.phoneCache.Invalidate(userID)
	}
}

func (f *DurableUserProjectionFacts) FlushUserProjectionFactReadModel() {
	if f != nil {
		f.freezeCache.Flush()
		f.phoneCache.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
}