Ctrl K
ComunidadGuías…

internal/app/userprojection/contact_cache.go

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

Archivo original ↗Preguntar a KiBot
package userprojection

import (
	"container/list"
	"context"
	"fmt"
	"sort"
	"sync"
	"time"

	"golang.org/x/sync/singleflight"

	"telesrv/internal/domain"
	"telesrv/internal/store"
)

const (
	// DefaultContactProjectionCacheTTL is a safety bound for out-of-band writes.
	// Normal correctness relies on write-path invalidation, not natural expiry.
	DefaultContactProjectionCacheTTL = 24 * time.Hour

	// DefaultContactSnapshotMaxViewers covers the 10k online target plus bounded
	// reconnect overlap. Eviction is exact LRU; reaching the limit must never
	// clear every viewer snapshot at once.
	DefaultContactSnapshotMaxViewers = 16_384
	contactReversePairMaxEntries     = 262144
	contactProjectionPairMaxEntries  = 262144
	// One dense request must not monopolize the pair LRU or hold the global
	// cache lock while inserting and evicting hundreds of thousands of cells.
	// Larger results are still returned; they simply are not admitted per pair.
	contactProjectionDenseAdmissionMaxCells = contactProjectionPairMaxEntries / 16
)

type contactAccountSnapshot struct {
	// contacts and ordered are immutable after the snapshot is published in
	// CachedContactStore.contacts. Readers intentionally retain shallow copies.
	contacts map[int64]domain.Contact
	ordered  []domain.Contact
	hash     int64
	expireAt time.Time
}

type personalPhotoSnapshot struct {
	// refs is immutable after the snapshot is published in personalPhotos.
	refs     map[int64]domain.ProfilePhotoRef
	expireAt time.Time
}

type reverseContactKey struct {
	ownerUserID   int64
	contactUserID int64
}

type reverseContactSnapshot struct {
	// contact is an immutable cached clone. nil is the negative-cache value.
	contact  *domain.Contact
	expireAt time.Time
}

type reverseContactEntry struct {
	key      reverseContactKey
	snapshot reverseContactSnapshot
}

type contactProjectionKey struct {
	viewerUserID  int64
	contactUserID int64
}

// cachedContactProjectionOverlay is the viewer-owned part of a contact row.
// Base user data is loaded and cached independently, so retaining domain.User
// here would multiply a large, viewer-independent value across every pair.
// Values are immutable after publication; noteEntities is cloned on both sides
// of the cache boundary.
type cachedContactProjectionOverlay struct {
	firstName    string
	lastName     string
	phone        string
	note         string
	noteEntities []domain.MessageEntity
	mutual       bool
	closeFriend  bool
}

func newCachedContactProjectionOverlay(contact domain.Contact) *cachedContactProjectionOverlay {
	return &cachedContactProjectionOverlay{
		firstName:    contact.FirstName,
		lastName:     contact.LastName,
		phone:        contact.Phone,
		note:         contact.Note,
		noteEntities: append([]domain.MessageEntity(nil), contact.NoteEntities...),
		mutual:       contact.Mutual || contact.User.Mutual,
		closeFriend:  contact.CloseFriend || contact.User.CloseFriend,
	}
}

func (o *cachedContactProjectionOverlay) domainContact(contactUserID int64) domain.Contact {
	if o == nil {
		return domain.Contact{}
	}
	return domain.Contact{
		User:         domain.User{ID: contactUserID},
		FirstName:    o.firstName,
		LastName:     o.lastName,
		Phone:        o.phone,
		Note:         o.note,
		NoteEntities: append([]domain.MessageEntity(nil), o.noteEntities...),
		Mutual:       o.mutual,
		CloseFriend:  o.closeFriend,
	}
}

type contactProjectionSnapshot struct {
	// Positive values point at immutable cached clones; nil is negative. Keeping
	// the compact viewer-owned overlay outside the entry makes negative pairs
	// consume only two pointers plus their expiry and avoids duplicating a full
	// base User for every positive pair.
	contact       *cachedContactProjectionOverlay
	personalPhoto *domain.ProfilePhotoRef
	expireAt      time.Time
}

// contactProjectionLookup is a transient, caller-owned copy. It deliberately
// retains the old value+found shape so no mutable slice from a cached pointer is
// exposed after the cache lock is released.
type contactProjectionLookup struct {
	contact            domain.Contact
	contactFound       bool
	personalPhoto      domain.ProfilePhotoRef
	personalPhotoFound bool
}

type contactProjectionEntry struct {
	key      contactProjectionKey
	snapshot contactProjectionSnapshot
}

type contactSnapshotLoadResult struct {
	snap   contactAccountSnapshot
	stored bool
}

type reverseContactLoadResult struct {
	contacts map[int64]domain.Contact
	stored   bool
}

type personalPhotoSnapshotLoadResult struct {
	snap   personalPhotoSnapshot
	stored bool
}

type contactProjectionLoadResult struct {
	batch   domain.ContactProjectionBatch
	current bool
}

type contactCacheViewerFence struct {
	userID     int64
	generation uint64
}

type contactCacheFence struct {
	flushGeneration uint64
	viewers         []contactCacheViewerFence
}

// CachedContactStore wraps ContactStore with account-level read model snapshots.
//
// Contact data is low-churn and high-read: TDesktop repeatedly asks for the same
// viewer-scoped user projection while switching dialogs. Pair-level short TTL
// caching still lets every RPC plan new SQL for another pair; this cache loads a
// viewer's whole contact projection once, filters it in memory, and relies on
// contact write methods to invalidate the affected account snapshots.
type CachedContactStore struct {
	inner store.ContactStore
	ttl   time.Duration
	now   func() time.Time

	mu                 sync.RWMutex
	contacts           map[int64]contactAccountSnapshot
	contactLRU         *list.List
	contactElements    map[int64]*list.Element
	contactCap         int
	personalPhotos     map[int64]personalPhotoSnapshot
	personalPhotoLRU   *list.List
	personalElements   map[int64]*list.Element
	personalPhotoCap   int
	reverse            map[reverseContactKey]*list.Element
	reverseLRU         *list.List
	reverseByOwner     map[int64]map[int64]struct{}
	reverseCap         int
	projection         map[contactProjectionKey]*list.Element
	projectionLRU      *list.List
	projectionByViewer map[int64]map[int64]struct{}
	projectionByTarget map[int64]map[int64]struct{}
	projectionCap      int
	flushGeneration    uint64
	viewerGenerations  map[int64]uint64
	sf                 singleflight.Group
}

func NewCachedContactStore(inner store.ContactStore, ttl time.Duration) *CachedContactStore {
	return NewCachedContactStoreWithMaxViewers(inner, ttl, DefaultContactSnapshotMaxViewers)
}

func NewCachedContactStoreWithMaxViewers(inner store.ContactStore, ttl time.Duration, maxViewers int) *CachedContactStore {
	if inner == nil {
		return nil
	}
	if ttl <= 0 {
		ttl = DefaultContactProjectionCacheTTL
	}
	if maxViewers <= 0 {
		maxViewers = DefaultContactSnapshotMaxViewers
	}
	return &CachedContactStore{
		inner:              inner,
		ttl:                ttl,
		now:                time.Now,
		contacts:           make(map[int64]contactAccountSnapshot, 1024),
		contactLRU:         list.New(),
		contactElements:    make(map[int64]*list.Element, 1024),
		contactCap:         maxViewers,
		personalPhotos:     make(map[int64]personalPhotoSnapshot, 1024),
		personalPhotoLRU:   list.New(),
		personalElements:   make(map[int64]*list.Element, 1024),
		personalPhotoCap:   maxViewers,
		reverse:            make(map[reverseContactKey]*list.Element, 4096),
		reverseLRU:         list.New(),
		reverseByOwner:     make(map[int64]map[int64]struct{}, 1024),
		reverseCap:         contactReversePairMaxEntries,
		projection:         make(map[contactProjectionKey]*list.Element, 4096),
		projectionLRU:      list.New(),
		projectionByViewer: make(map[int64]map[int64]struct{}, 1024),
		projectionByTarget: make(map[int64]map[int64]struct{}, 1024),
		projectionCap:      contactProjectionPairMaxEntries,
		viewerGenerations:  make(map[int64]uint64, 1024),
	}
}

func (c *CachedContactStore) ListByUser(ctx context.Context, userID int64) (domain.ContactList, error) {
	if userID == 0 {
		return domain.ContactList{}, nil
	}
	snap, err := c.contactSnapshot(ctx, userID)
	if err != nil {
		return domain.ContactList{}, err
	}
	return domain.ContactList{Contacts: cloneCachedContacts(snap.ordered), Hash: snap.hash}, nil
}

func (c *CachedContactStore) Get(ctx context.Context, userID, contactUserID int64) (domain.Contact, bool, error) {
	if userID == 0 || contactUserID == 0 {
		return domain.Contact{}, false, nil
	}
	got, err := c.GetMany(ctx, userID, []int64{contactUserID})
	if err != nil {
		return domain.Contact{}, false, err
	}
	contact, ok := got[contactUserID]
	return contact, ok, nil
}

func (c *CachedContactStore) GetMany(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.Contact, error) {
	out := make(map[int64]domain.Contact, len(contactUserIDs))
	if userID == 0 || len(contactUserIDs) == 0 {
		return out, nil
	}
	snap, err := c.contactSnapshot(ctx, userID)
	if err != nil {
		return nil, err
	}
	for _, ownerID := range contactUserIDs {
		if ownerID == 0 {
			continue
		}
		if contact, ok := snap.contacts[ownerID]; ok {
			out[ownerID] = cloneCachedContact(contact)
		}
	}
	return out, nil
}

func (c *CachedContactStore) GetReverseContacts(ctx context.Context, userID int64, ownerUserIDs []int64) (map[int64]domain.Contact, error) {
	out := make(map[int64]domain.Contact, len(ownerUserIDs))
	if userID == 0 || len(ownerUserIDs) == 0 {
		return out, nil
	}
	owners := dedupContactIDs(ownerUserIDs)
	if len(owners) == 0 {
		return out, nil
	}
	missing := make([]int64, 0, len(owners))
	now := c.now()
	for _, ownerID := range owners {
		// Reuse a full owner snapshot when another hot path already loaded it.
		// Do not cold-load one full list per owner: a large projection would turn
		// into N SQL queries.
		if snap, ok := c.lookupContactSnapshot(ownerID, now); ok {
			if contact, found := snap.contacts[userID]; found {
				out[ownerID] = cloneCachedContact(contact)
			}
			continue
		}
		if contact, found, cached := c.lookupReverseContact(ownerID, userID, now); cached {
			if found {
				out[ownerID] = contact
			}
			continue
		}
		missing = append(missing, ownerID)
	}
	if len(missing) == 0 {
		return out, nil
	}
	loaded, err := c.loadReverseContacts(ctx, userID, missing)
	if err != nil {
		return nil, err
	}
	for ownerID, contact := range loaded {
		if contact.User.ID != 0 {
			out[ownerID] = cloneCachedContact(contact)
		}
	}
	return out, nil
}

func (c *CachedContactStore) ContactProjectionForViewers(ctx context.Context, viewerUserIDs, contactUserIDs []int64) (domain.ContactProjectionBatch, error) {
	viewers := dedupContactIDs(viewerUserIDs)
	targets := dedupContactIDs(contactUserIDs)
	if len(viewers) == 0 || len(targets) == 0 {
		return domain.ContactProjectionBatch{
			Contacts:       map[int64]map[int64]domain.Contact{},
			PersonalPhotos: map[int64]map[int64]domain.ProfilePhotoRef{},
		}, nil
	}

	for {
		out := domain.ContactProjectionBatch{
			Contacts:       make(map[int64]map[int64]domain.Contact, len(viewers)),
			PersonalPhotos: make(map[int64]map[int64]domain.ProfilePhotoRef, len(viewers)),
		}
		readFence := c.captureCacheFenceSlices(viewers, targets)
		now := c.now()
		coldViewers := make(map[int64]struct{}, len(viewers))
		coldTargets := make(map[int64]struct{}, len(targets))
		for _, viewerID := range viewers {
			var contactSnap contactAccountSnapshot
			contactsWarm := false
			if snap, ok := c.lookupContactSnapshot(viewerID, now); ok {
				contactsWarm = true
				contactSnap = snap
				for _, targetID := range targets {
					if contact, found := snap.contacts[targetID]; found {
						putContactProjectionContact(&out, viewerID, targetID, contact)
					}
				}
			}
			personalPhotosWarm := false
			if snap, ok := c.lookupPersonalPhotoSnapshot(viewerID, now); ok {
				personalPhotosWarm = true
				for _, targetID := range targets {
					if ref, found := snap.refs[targetID]; found {
						putContactProjectionPersonalPhoto(&out, viewerID, targetID, ref)
					}
				}
			}
			for _, targetID := range targets {
				if contactsWarm && personalPhotosWarm {
					continue
				}
				if contactsWarm {
					if _, found := contactSnap.contacts[targetID]; !found {
						continue
					}
				}
				if snap, ok := c.lookupContactProjectionPair(viewerID, targetID, now); ok {
					if !contactsWarm && snap.contactFound {
						putContactProjectionContact(&out, viewerID, targetID, snap.contact)
					}
					if !personalPhotosWarm && snap.personalPhotoFound {
						putContactProjectionPersonalPhoto(&out, viewerID, targetID, snap.personalPhoto)
					}
					continue
				}
				coldViewers[viewerID] = struct{}{}
				coldTargets[targetID] = struct{}{}
			}
		}
		if !c.cacheFenceCurrent(readFence) {
			if err := ctx.Err(); err != nil {
				return domain.ContactProjectionBatch{}, err
			}
			continue
		}
		if len(coldViewers) == 0 || len(coldTargets) == 0 {
			return out, nil
		}
		cold := make([]int64, 0, len(coldViewers))
		for viewerID := range coldViewers {
			cold = append(cold, viewerID)
		}
		coldIDs := make([]int64, 0, len(coldTargets))
		for targetID := range coldTargets {
			coldIDs = append(coldIDs, targetID)
		}
		loaded, err := c.loadContactProjectionForViewers(ctx, cold, coldIDs)
		if err != nil {
			return domain.ContactProjectionBatch{}, err
		}
		if !c.cacheFenceCurrent(readFence) {
			if err := ctx.Err(); err != nil {
				return domain.ContactProjectionBatch{}, err
			}
			continue
		}
		mergeContactProjectionBatch(&out, loaded)
		return out, nil
	}
}

func (c *CachedContactStore) loadContactProjectionForViewers(ctx context.Context, viewerUserIDs, contactUserIDs []int64) (domain.ContactProjectionBatch, error) {
	viewers := append([]int64(nil), viewerUserIDs...)
	targets := append([]int64(nil), contactUserIDs...)
	sort.Slice(viewers, func(i, j int) bool { return viewers[i] < viewers[j] })
	sort.Slice(targets, func(i, j int) bool { return targets[i] < targets[j] })
	sfKey := fmt.Sprintf("contact-projection:%v:%v", viewers, targets)
	for {
		v, err, _ := c.sf.Do(sfKey, func() (any, error) {
			loadFence := c.captureCacheFenceSlices(viewers, targets)
			batch, err := c.inner.ContactProjectionForViewers(ctx, viewers, targets)
			if err != nil {
				return contactProjectionLoadResult{}, err
			}
			now := c.now()
			expireAt := now.Add(c.ttl)
			admitPairs := admitDenseContactProjectionPairs(len(viewers), len(targets))
			c.mu.Lock()
			current := c.cacheFenceCurrentLocked(loadFence)
			if current && admitPairs {
				for _, viewerID := range viewers {
					for _, targetID := range targets {
						contact, contactFound := batch.Contacts[viewerID][targetID]
						ref, personalPhotoFound := batch.PersonalPhotos[viewerID][targetID]
						c.storeContactProjectionPairLocked(
							contactProjectionKey{viewerUserID: viewerID, contactUserID: targetID},
							contact, contactFound, ref, personalPhotoFound, expireAt,
						)
					}
				}
			}
			c.mu.Unlock()
			return contactProjectionLoadResult{
				batch:   cloneContactProjectionBatch(batch),
				current: current,
			}, nil
		})
		if err != nil {
			return domain.ContactProjectionBatch{}, err
		}
		result := v.(contactProjectionLoadResult)
		if result.current {
			return result.batch, nil
		}
		if err := ctx.Err(); err != nil {
			return domain.ContactProjectionBatch{}, err
		}
	}
}

func admitDenseContactProjectionPairs(viewerCount, targetCount int) bool {
	if viewerCount <= 0 || targetCount <= 0 || targetCount > contactProjectionDenseAdmissionMaxCells {
		return false
	}
	// Division avoids overflowing int for attacker-controlled vector lengths.
	return viewerCount <= contactProjectionDenseAdmissionMaxCells/targetCount
}

// loadReverseContacts performs at most one batched cold-store read for all
// missing owner→viewer pairs, then caches both hits and misses. Privacy
// projection therefore stays memory-only after warm-up instead of repeating a
// reverse-contact SQL query on every large user vector.
func (c *CachedContactStore) loadReverseContacts(ctx context.Context, userID int64, ownerUserIDs []int64) (map[int64]domain.Contact, error) {
	owners := append([]int64(nil), ownerUserIDs...)
	sort.Slice(owners, func(i, j int) bool { return owners[i] < owners[j] })
	sfKey := fmt.Sprintf("contact-reverse:%d:%v", userID, owners)
	for {
		v, err, _ := c.sf.Do(sfKey, func() (any, error) {
			loadFence := c.captureCacheFenceSlices(owners, []int64{userID})
			contacts, err := c.inner.GetReverseContacts(ctx, userID, owners)
			if err != nil {
				return reverseContactLoadResult{}, err
			}
			now := c.now()
			expireAt := now.Add(c.ttl)
			c.mu.Lock()
			stored := c.cacheFenceCurrentLocked(loadFence)
			if stored {
				for _, ownerID := range owners {
					key := reverseContactKey{ownerUserID: ownerID, contactUserID: userID}
					contact, found := contacts[ownerID]
					c.storeReverseContactLocked(key, contact, found, expireAt)
				}
			}
			c.mu.Unlock()
			return reverseContactLoadResult{
				contacts: cloneCachedContactMap(contacts),
				stored:   stored,
			}, nil
		})
		if err != nil {
			return nil, err
		}
		result := v.(reverseContactLoadResult)
		if result.stored {
			return result.contacts, nil
		}
		if err := ctx.Err(); err != nil {
			return nil, err
		}
	}
}

func (c *CachedContactStore) Upsert(ctx context.Context, userID int64, input domain.ContactInput) (domain.Contact, error) {
	contact, err := c.inner.Upsert(ctx, userID, input)
	if err == nil {
		// Published account snapshots are immutable. Invalidate instead of
		// modifying their inner maps/slices in place or publishing a mutation
		// payload whose cache-write order may differ from its DB commit order.
		c.InvalidateViewers(userID, input.ContactUserID)
	}
	return contact, err
}

func (c *CachedContactStore) UpsertMany(ctx context.Context, userID int64, inputs []domain.ContactInput) ([]domain.Contact, error) {
	contacts, err := c.inner.UpsertMany(ctx, userID, inputs)
	if err == nil {
		ids := make([]int64, 0, len(inputs))
		for _, input := range inputs {
			ids = append(ids, input.ContactUserID)
		}
		c.InvalidateViewers(append([]int64{userID}, ids...)...)
	}
	return contacts, err
}

func (c *CachedContactStore) UpdateNote(ctx context.Context, userID, contactUserID int64, note string, entities []domain.MessageEntity) (domain.Contact, bool, error) {
	contact, found, err := c.inner.UpdateNote(ctx, userID, contactUserID, note, entities)
	if err == nil {
		if found {
			c.InvalidateViewers(userID)
		}
	}
	return contact, found, err
}

func (c *CachedContactStore) SetCloseFriends(ctx context.Context, userID int64, contactUserIDs []int64) (domain.CloseFriendsEditResult, error) {
	res, err := c.inner.SetCloseFriends(ctx, userID, contactUserIDs)
	if err == nil {
		c.InvalidateViewers(userID)
	}
	return res, err
}

func (c *CachedContactStore) SetPersonalPhoto(ctx context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) {
	contact, found, err := c.inner.SetPersonalPhoto(ctx, userID, contactUserID, photoID, date)
	if err == nil && found {
		// Do not perform a post-commit read followed by write-through: two
		// concurrent mutations can complete their cache writes in the opposite
		// order and reinsert a stale pair after a newer NOTIFY invalidation.
		c.InvalidateViewers(userID)
	}
	return contact, found, err
}

func (c *CachedContactStore) PersonalPhotos(ctx context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
	out := make(map[int64]domain.ProfilePhotoRef, len(contactUserIDs))
	if userID == 0 || len(contactUserIDs) == 0 {
		return out, nil
	}
	snap, err := c.personalPhotoSnapshot(ctx, userID)
	if err != nil {
		return nil, err
	}
	for _, ownerID := range contactUserIDs {
		if ownerID == 0 {
			continue
		}
		if ref, ok := snap.refs[ownerID]; ok {
			out[ownerID] = cloneCachedProfilePhotoRef(ref)
		}
	}
	return out, nil
}

func (c *CachedContactStore) Delete(ctx context.Context, userID int64, contactUserIDs []int64) (int, error) {
	count, err := c.inner.Delete(ctx, userID, contactUserIDs)
	if err == nil {
		c.InvalidateViewers(append([]int64{userID}, contactUserIDs...)...)
	}
	return count, err
}

func (c *CachedContactStore) ReadBlocklistIDs(ctx context.Context, owner int64) ([]int64, error) {
	return c.inner.ReadBlocklistIDs(ctx, owner)
}

func (c *CachedContactStore) MutateBlocklist(ctx context.Context, m store.BlocklistMutation, build store.DeliveryEffectsBuilder[store.BlocklistMutationSnapshot]) (store.BlocklistMutationSnapshot, error) {
	result, err := c.inner.MutateBlocklist(ctx, m, build)
	if err == nil && len(result.Changes) > 0 {
		ids := []int64{m.OwnerUserID}
		for _, change := range result.Changes {
			ids = append(ids, change.PeerUserID)
		}
		c.InvalidateViewers(ids...)
	}
	return result, err
}

func (c *CachedContactStore) IsBlocked(ctx context.Context, userID, blockedUserID int64) (bool, error) {
	return c.inner.IsBlocked(ctx, userID, blockedUserID)
}

func (c *CachedContactStore) ListBlocked(ctx context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error) {
	return c.inner.ListBlocked(ctx, userID, offset, limit)
}

func (c *CachedContactStore) contactSnapshot(ctx context.Context, userID int64) (contactAccountSnapshot, error) {
	for {
		if snap, ok := c.lookupContactSnapshot(userID, c.now()); ok {
			return snap, nil
		}
		v, err, _ := c.sf.Do(fmt.Sprintf("contact:%d", userID), func() (any, error) {
			now := c.now()
			if snap, ok := c.lookupContactSnapshot(userID, now); ok {
				return contactSnapshotLoadResult{snap: snap, stored: true}, nil
			}
			loadFence := c.captureCacheFence(userID)
			list, err := c.inner.ListByUser(ctx, userID)
			if err != nil {
				return contactSnapshotLoadResult{}, err
			}
			snap := buildContactAccountSnapshot(list, now.Add(c.ttl))
			c.mu.Lock()
			stored := c.cacheFenceCurrentLocked(loadFence)
			if stored {
				c.storeContactSnapshotLocked(userID, snap)
			}
			c.mu.Unlock()
			return contactSnapshotLoadResult{snap: snap, stored: stored}, nil
		})
		if err != nil {
			return contactAccountSnapshot{}, err
		}
		result := v.(contactSnapshotLoadResult)
		if result.stored {
			return result.snap, nil
		}
		if err := ctx.Err(); err != nil {
			return contactAccountSnapshot{}, err
		}
	}
}

func (c *CachedContactStore) lookupContactSnapshot(userID int64, now time.Time) (contactAccountSnapshot, bool) {
	c.mu.Lock()
	snap, ok := c.contacts[userID]
	if !ok {
		c.mu.Unlock()
		return contactAccountSnapshot{}, false
	}
	if !snap.expireAt.After(now) {
		c.advanceViewerGenerationLocked(userID)
		c.invalidateViewerLocked(userID)
		c.mu.Unlock()
		return contactAccountSnapshot{}, false
	}
	if element := c.contactElements[userID]; element != nil {
		c.contactLRU.MoveToFront(element)
	}
	c.mu.Unlock()
	return snap, true
}

func (c *CachedContactStore) storeContactSnapshotLocked(userID int64, snap contactAccountSnapshot) {
	if element := c.contactElements[userID]; element != nil {
		c.contacts[userID] = snap
		c.contactLRU.MoveToFront(element)
		return
	}
	c.contacts[userID] = snap
	c.contactElements[userID] = c.contactLRU.PushFront(userID)
	for c.contactLRU.Len() > c.contactCap {
		oldest := c.contactLRU.Back()
		if oldest == nil {
			break
		}
		oldestUserID := oldest.Value.(int64)
		delete(c.contacts, oldestUserID)
		delete(c.contactElements, oldestUserID)
		c.contactLRU.Remove(oldest)
	}
}

func (c *CachedContactStore) personalPhotoSnapshot(ctx context.Context, userID int64) (personalPhotoSnapshot, error) {
	for {
		if snap, ok := c.lookupPersonalPhotoSnapshot(userID, c.now()); ok {
			return snap, nil
		}
		v, err, _ := c.sf.Do(fmt.Sprintf("contact-photo:%d", userID), func() (any, error) {
			now := c.now()
			if snap, ok := c.lookupPersonalPhotoSnapshot(userID, now); ok {
				return personalPhotoSnapshotLoadResult{snap: snap, stored: true}, nil
			}
			loadFence := c.captureCacheFence(userID)
			contacts, err := c.contactSnapshot(ctx, userID)
			if err != nil {
				return personalPhotoSnapshotLoadResult{}, err
			}
			ids := make([]int64, 0, len(contacts.contacts))
			for id := range contacts.contacts {
				ids = append(ids, id)
			}
			refs := map[int64]domain.ProfilePhotoRef{}
			if len(ids) > 0 {
				refs, err = c.inner.PersonalPhotos(ctx, userID, ids)
				if err != nil {
					return personalPhotoSnapshotLoadResult{}, err
				}
			}
			snap := personalPhotoSnapshot{refs: cloneCachedProfilePhotoRefs(refs), expireAt: now.Add(c.ttl)}
			c.mu.Lock()
			stored := c.cacheFenceCurrentLocked(loadFence)
			if stored {
				c.storePersonalPhotoSnapshotLocked(userID, snap)
			}
			c.mu.Unlock()
			return personalPhotoSnapshotLoadResult{snap: snap, stored: stored}, nil
		})
		if err != nil {
			return personalPhotoSnapshot{}, err
		}
		result := v.(personalPhotoSnapshotLoadResult)
		if result.stored {
			return result.snap, nil
		}
		if err := ctx.Err(); err != nil {
			return personalPhotoSnapshot{}, err
		}
	}
}

func (c *CachedContactStore) lookupPersonalPhotoSnapshot(userID int64, now time.Time) (personalPhotoSnapshot, bool) {
	c.mu.Lock()
	snap, ok := c.personalPhotos[userID]
	if !ok {
		c.mu.Unlock()
		return personalPhotoSnapshot{}, false
	}
	if !snap.expireAt.After(now) {
		c.advanceViewerGenerationLocked(userID)
		c.invalidateViewerLocked(userID)
		c.mu.Unlock()
		return personalPhotoSnapshot{}, false
	}
	if element := c.personalElements[userID]; element != nil {
		c.personalPhotoLRU.MoveToFront(element)
	}
	c.mu.Unlock()
	return snap, true
}

func (c *CachedContactStore) storePersonalPhotoSnapshotLocked(userID int64, snap personalPhotoSnapshot) {
	if element := c.personalElements[userID]; element != nil {
		c.personalPhotos[userID] = snap
		c.personalPhotoLRU.MoveToFront(element)
		return
	}
	c.personalPhotos[userID] = snap
	c.personalElements[userID] = c.personalPhotoLRU.PushFront(userID)
	for c.personalPhotoLRU.Len() > c.personalPhotoCap {
		oldest := c.personalPhotoLRU.Back()
		if oldest == nil {
			break
		}
		oldestUserID := oldest.Value.(int64)
		delete(c.personalPhotos, oldestUserID)
		delete(c.personalElements, oldestUserID)
		c.personalPhotoLRU.Remove(oldest)
	}
}

func (c *CachedContactStore) lookupReverseContact(ownerUserID, contactUserID int64, now time.Time) (domain.Contact, bool, bool) {
	key := reverseContactKey{ownerUserID: ownerUserID, contactUserID: contactUserID}
	c.mu.Lock()
	element, ok := c.reverse[key]
	if !ok {
		c.mu.Unlock()
		return domain.Contact{}, false, false
	}
	entry := element.Value.(*reverseContactEntry)
	snap := entry.snapshot
	if !snap.expireAt.After(now) {
		c.removeReverseElementLocked(element)
		c.mu.Unlock()
		return domain.Contact{}, false, false
	}
	c.reverseLRU.MoveToFront(element)
	c.mu.Unlock()
	if snap.contact == nil {
		return domain.Contact{}, false, true
	}
	return cloneCachedContact(*snap.contact), true, true
}

func (c *CachedContactStore) storeReverseContactLocked(key reverseContactKey, contact domain.Contact, found bool, expireAt time.Time) {
	var cached *domain.Contact
	if found {
		clone := cloneCachedContact(contact)
		cached = &clone
	}
	snapshot := reverseContactSnapshot{contact: cached, expireAt: expireAt}
	if element, ok := c.reverse[key]; ok {
		entry := element.Value.(*reverseContactEntry)
		entry.snapshot = snapshot
		c.reverseLRU.MoveToFront(element)
		return
	}
	element := c.reverseLRU.PushFront(&reverseContactEntry{key: key, snapshot: snapshot})
	c.reverse[key] = element
	if c.reverseByOwner[key.ownerUserID] == nil {
		c.reverseByOwner[key.ownerUserID] = make(map[int64]struct{})
	}
	c.reverseByOwner[key.ownerUserID][key.contactUserID] = struct{}{}
	for c.reverseLRU.Len() > c.reverseCap {
		c.removeReverseElementLocked(c.reverseLRU.Back())
	}
}

func (c *CachedContactStore) removeReverseElementLocked(element *list.Element) {
	if element == nil {
		return
	}
	entry := element.Value.(*reverseContactEntry)
	delete(c.reverse, entry.key)
	if viewers := c.reverseByOwner[entry.key.ownerUserID]; viewers != nil {
		delete(viewers, entry.key.contactUserID)
		if len(viewers) == 0 {
			delete(c.reverseByOwner, entry.key.ownerUserID)
		}
	}
	c.reverseLRU.Remove(element)
}

func (c *CachedContactStore) lookupContactProjectionPair(viewerUserID, contactUserID int64, now time.Time) (contactProjectionLookup, bool) {
	key := contactProjectionKey{viewerUserID: viewerUserID, contactUserID: contactUserID}
	c.mu.Lock()
	element, ok := c.projection[key]
	if !ok {
		c.mu.Unlock()
		return contactProjectionLookup{}, false
	}
	entry := element.Value.(*contactProjectionEntry)
	snap := entry.snapshot
	if !snap.expireAt.After(now) {
		c.removeContactProjectionElementLocked(element)
		c.mu.Unlock()
		return contactProjectionLookup{}, false
	}
	c.projectionLRU.MoveToFront(element)
	c.mu.Unlock()
	result := contactProjectionLookup{}
	if snap.contact != nil {
		result.contact = snap.contact.domainContact(contactUserID)
		result.contactFound = true
	}
	if snap.personalPhoto != nil {
		result.personalPhoto = cloneCachedProfilePhotoRef(*snap.personalPhoto)
		result.personalPhotoFound = true
	}
	return result, true
}

func (c *CachedContactStore) storeContactProjectionPairLocked(
	key contactProjectionKey,
	contact domain.Contact,
	contactFound bool,
	personalPhoto domain.ProfilePhotoRef,
	personalPhotoFound bool,
	expireAt time.Time,
) {
	var cachedContact *cachedContactProjectionOverlay
	if contactFound {
		cachedContact = newCachedContactProjectionOverlay(contact)
	}
	var cachedPersonalPhoto *domain.ProfilePhotoRef
	if personalPhotoFound {
		clone := cloneCachedProfilePhotoRef(personalPhoto)
		cachedPersonalPhoto = &clone
	}
	snapshot := contactProjectionSnapshot{
		contact:       cachedContact,
		personalPhoto: cachedPersonalPhoto,
		expireAt:      expireAt,
	}
	if element, ok := c.projection[key]; ok {
		entry := element.Value.(*contactProjectionEntry)
		entry.snapshot = snapshot
		c.projectionLRU.MoveToFront(element)
		return
	}
	element := c.projectionLRU.PushFront(&contactProjectionEntry{key: key, snapshot: snapshot})
	c.projection[key] = element
	if c.projectionByViewer[key.viewerUserID] == nil {
		c.projectionByViewer[key.viewerUserID] = make(map[int64]struct{})
	}
	c.projectionByViewer[key.viewerUserID][key.contactUserID] = struct{}{}
	if c.projectionByTarget[key.contactUserID] == nil {
		c.projectionByTarget[key.contactUserID] = make(map[int64]struct{})
	}
	c.projectionByTarget[key.contactUserID][key.viewerUserID] = struct{}{}
	for c.projectionLRU.Len() > c.projectionCap {
		c.removeContactProjectionElementLocked(c.projectionLRU.Back())
	}
}

func (c *CachedContactStore) removeContactProjectionElementLocked(element *list.Element) {
	if element == nil {
		return
	}
	entry := element.Value.(*contactProjectionEntry)
	delete(c.projection, entry.key)
	if targets := c.projectionByViewer[entry.key.viewerUserID]; targets != nil {
		delete(targets, entry.key.contactUserID)
		if len(targets) == 0 {
			delete(c.projectionByViewer, entry.key.viewerUserID)
		}
	}
	if viewers := c.projectionByTarget[entry.key.contactUserID]; viewers != nil {
		delete(viewers, entry.key.viewerUserID)
		if len(viewers) == 0 {
			delete(c.projectionByTarget, entry.key.contactUserID)
		}
	}
	c.projectionLRU.Remove(element)
}

func (c *CachedContactStore) InvalidateViewers(ids ...int64) {
	if c == nil || len(ids) == 0 {
		return
	}
	c.mu.Lock()
	seen := make(map[int64]struct{}, len(ids))
	for _, id := range ids {
		if id == 0 {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		c.advanceViewerGenerationLocked(id)
		c.invalidateViewerLocked(id)
	}
	c.mu.Unlock()
}

func (c *CachedContactStore) invalidateViewerLocked(id int64) {
	delete(c.contacts, id)
	if element := c.contactElements[id]; element != nil {
		delete(c.contactElements, id)
		c.contactLRU.Remove(element)
	}
	delete(c.personalPhotos, id)
	if element := c.personalElements[id]; element != nil {
		delete(c.personalElements, id)
		c.personalPhotoLRU.Remove(element)
	}
	for contactUserID := range c.reverseByOwner[id] {
		c.removeReverseKeyLocked(reverseContactKey{ownerUserID: id, contactUserID: contactUserID})
	}
	for contactUserID := range c.projectionByViewer[id] {
		c.removeContactProjectionKeyLocked(contactProjectionKey{viewerUserID: id, contactUserID: contactUserID})
	}
	for viewerUserID := range c.projectionByTarget[id] {
		c.removeContactProjectionKeyLocked(contactProjectionKey{viewerUserID: viewerUserID, contactUserID: id})
	}
}

func (c *CachedContactStore) removeReverseKeyLocked(key reverseContactKey) {
	if element, ok := c.reverse[key]; ok {
		c.removeReverseElementLocked(element)
	}
}

func (c *CachedContactStore) removeContactProjectionKeyLocked(key contactProjectionKey) {
	if element, ok := c.projection[key]; ok {
		c.removeContactProjectionElementLocked(element)
	}
}

func (c *CachedContactStore) FlushReadModelCache() {
	if c == nil {
		return
	}
	c.mu.Lock()
	c.flushGeneration++
	c.viewerGenerations = make(map[int64]uint64, 1024)
	c.contacts = make(map[int64]contactAccountSnapshot, 1024)
	c.contactElements = make(map[int64]*list.Element, 1024)
	c.contactLRU.Init()
	c.personalPhotos = make(map[int64]personalPhotoSnapshot, 1024)
	c.personalElements = make(map[int64]*list.Element, 1024)
	c.personalPhotoLRU.Init()
	c.reverse = make(map[reverseContactKey]*list.Element, 4096)
	c.reverseLRU.Init()
	c.reverseByOwner = make(map[int64]map[int64]struct{}, 1024)
	c.projection = make(map[contactProjectionKey]*list.Element, 4096)
	c.projectionLRU.Init()
	c.projectionByViewer = make(map[int64]map[int64]struct{}, 1024)
	c.projectionByTarget = make(map[int64]map[int64]struct{}, 1024)
	c.mu.Unlock()
}

func (c *CachedContactStore) captureCacheFence(userIDs ...int64) contactCacheFence {
	return c.captureCacheFenceSlices(userIDs, nil)
}

func (c *CachedContactStore) captureCacheFenceSlices(first, second []int64) contactCacheFence {
	c.mu.RLock()
	fence := contactCacheFence{
		flushGeneration: c.flushGeneration,
		viewers:         make([]contactCacheViewerFence, 0, len(first)+len(second)),
	}
	for _, userIDs := range [][]int64{first, second} {
		for _, userID := range userIDs {
			if userID == 0 {
				continue
			}
			fence.viewers = append(fence.viewers, contactCacheViewerFence{
				userID:     userID,
				generation: c.viewerGenerations[userID],
			})
		}
	}
	c.mu.RUnlock()
	return fence
}

func (c *CachedContactStore) cacheFenceCurrent(fence contactCacheFence) bool {
	c.mu.RLock()
	current := c.cacheFenceCurrentLocked(fence)
	c.mu.RUnlock()
	return current
}

func (c *CachedContactStore) cacheFenceCurrentLocked(fence contactCacheFence) bool {
	if c.flushGeneration != fence.flushGeneration {
		return false
	}
	for _, viewer := range fence.viewers {
		if c.viewerGenerations[viewer.userID] != viewer.generation {
			return false
		}
	}
	return true
}

func (c *CachedContactStore) advanceViewerGenerationLocked(userID int64) {
	c.viewerGenerations[userID]++
}

func buildContactAccountSnapshot(list domain.ContactList, expireAt time.Time) contactAccountSnapshot {
	contacts := make(map[int64]domain.Contact, len(list.Contacts))
	ordered := make([]domain.Contact, 0, len(list.Contacts))
	for _, contact := range list.Contacts {
		if contact.User.ID == 0 {
			continue
		}
		clone := cloneCachedContact(contact)
		contacts[clone.User.ID] = clone
		ordered = append(ordered, clone)
	}
	return contactAccountSnapshot{contacts: contacts, ordered: ordered, hash: list.Hash, expireAt: expireAt}
}

func cloneCachedContactMap(in map[int64]domain.Contact) map[int64]domain.Contact {
	out := make(map[int64]domain.Contact, len(in))
	for id, contact := range in {
		out[id] = cloneCachedContact(contact)
	}
	return out
}

func cloneContactProjectionBatch(in domain.ContactProjectionBatch) domain.ContactProjectionBatch {
	out := domain.ContactProjectionBatch{
		Contacts:       make(map[int64]map[int64]domain.Contact, len(in.Contacts)),
		PersonalPhotos: make(map[int64]map[int64]domain.ProfilePhotoRef, len(in.PersonalPhotos)),
	}
	mergeContactProjectionBatch(&out, in)
	return out
}

func mergeContactProjectionBatch(dst *domain.ContactProjectionBatch, src domain.ContactProjectionBatch) {
	for viewerID, contacts := range src.Contacts {
		for targetID, contact := range contacts {
			putContactProjectionContact(dst, viewerID, targetID, contact)
		}
	}
	for viewerID, refs := range src.PersonalPhotos {
		for targetID, ref := range refs {
			putContactProjectionPersonalPhoto(dst, viewerID, targetID, ref)
		}
	}
}

func putContactProjectionContact(batch *domain.ContactProjectionBatch, viewerID, targetID int64, contact domain.Contact) {
	if batch.Contacts == nil {
		batch.Contacts = map[int64]map[int64]domain.Contact{}
	}
	if batch.Contacts[viewerID] == nil {
		batch.Contacts[viewerID] = map[int64]domain.Contact{}
	}
	batch.Contacts[viewerID][targetID] = cloneCachedContact(contact)
}

func putContactProjectionPersonalPhoto(batch *domain.ContactProjectionBatch, viewerID, targetID int64, ref domain.ProfilePhotoRef) {
	if batch.PersonalPhotos == nil {
		batch.PersonalPhotos = map[int64]map[int64]domain.ProfilePhotoRef{}
	}
	if batch.PersonalPhotos[viewerID] == nil {
		batch.PersonalPhotos[viewerID] = map[int64]domain.ProfilePhotoRef{}
	}
	batch.PersonalPhotos[viewerID][targetID] = cloneCachedProfilePhotoRef(ref)
}

func dedupContactIDs(ids []int64) []int64 {
	seen := make(map[int64]struct{}, len(ids))
	out := make([]int64, 0, 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
}

func cloneCachedContacts(in []domain.Contact) []domain.Contact {
	out := make([]domain.Contact, len(in))
	for i := range in {
		out[i] = cloneCachedContact(in[i])
	}
	return out
}

func cloneCachedContact(in domain.Contact) domain.Contact {
	in.User = cloneCachedUser(in.User)
	if in.NoteEntities != nil {
		in.NoteEntities = append([]domain.MessageEntity(nil), in.NoteEntities...)
	}
	return in
}

func cloneCachedUser(in domain.User) domain.User {
	if in.PhotoStripped != nil {
		in.PhotoStripped = append([]byte(nil), in.PhotoStripped...)
	}
	if in.ContactNoteEntities != nil {
		in.ContactNoteEntities = append([]domain.MessageEntity(nil), in.ContactNoteEntities...)
	}
	if in.RestrictionReasons != nil {
		in.RestrictionReasons = append([]domain.UserRestrictionReason(nil), in.RestrictionReasons...)
	}
	return in
}

func cloneCachedProfilePhotoRefs(in map[int64]domain.ProfilePhotoRef) map[int64]domain.ProfilePhotoRef {
	out := make(map[int64]domain.ProfilePhotoRef, len(in))
	for id, ref := range in {
		out[id] = cloneCachedProfilePhotoRef(ref)
	}
	return out
}

func cloneCachedProfilePhotoRef(in domain.ProfilePhotoRef) domain.ProfilePhotoRef {
	if in.Stripped != nil {
		in.Stripped = append([]byte(nil), in.Stripped...)
	}
	return in
}