1196 lines
38 KiB
Go
1196 lines
38 KiB
Go
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) Block(ctx context.Context, userID, blockedUserID int64, date int) (bool, error) {
|
|
changed, err := c.inner.Block(ctx, userID, blockedUserID, date)
|
|
if err == nil {
|
|
c.InvalidateViewers(userID, blockedUserID)
|
|
}
|
|
return changed, err
|
|
}
|
|
|
|
func (c *CachedContactStore) Unblock(ctx context.Context, userID, blockedUserID int64) (bool, error) {
|
|
changed, err := c.inner.Unblock(ctx, userID, blockedUserID)
|
|
if err == nil {
|
|
c.InvalidateViewers(userID, blockedUserID)
|
|
}
|
|
return changed, 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
|
|
}
|