channels: drop stale membership caches on join/leave

After channels.leaveChannel, a client that polls channels.getFullChannel kept
receiving a projection that still showed it as an active member (left=false)
until the per-(viewer,channel) RPC projection cache and the store-level member
cache lapsed on their own or the async read-model NOTIFY landed. The client
therefore kept an open compose box while every send was already rejected with
CHANNEL_PRIVATE - most visible on public forum supergroups, where getFullChannel
keeps succeeding via the preview path instead of tearing the chat down.

Every other membership-mutating path already busts these caches synchronously;
join/leave/invite/request-approval did not. Add:

- store: invalidateChannelMembershipCaches (row + member + dialog caches),
  called post-commit from JoinChannel, LeaveChannel, ImportInvite,
  InviteToChannel.
- rpc: invalidateChannelMembershipProjection (channelFullProjectionCache pair),
  called from the join/leave/invite/hide-requests handlers for every user whose
  membership changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Astra 2026-09-09 13:28:50 +01:00
parent d55a97f0a5
commit 2419f47236
7 changed files with 116 additions and 0 deletions

View file

@ -341,6 +341,7 @@ func (r *Router) onChannelsInviteToChannel(ctx context.Context, req *tg.Channels
return nil, channelInviteErr(err)
}
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
cache := newViewerPeerCache(r)
updates := r.channelOperationUpdatesWithPeerCache(ctx, userID, res, cache)
@ -379,6 +380,7 @@ func (r *Router) onChannelsJoinChannel(ctx context.Context, input tg.InputChanne
return nil, channelInviteErr(err)
}
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
updates := r.channelOperationUpdates(ctx, userID, res)
r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates {
@ -406,6 +408,11 @@ func (r *Router) onChannelsLeaveChannel(ctx context.Context, input tg.InputChann
return nil, channelAdminErr(err)
}
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
membershipChanged := channelMemberUserIDs(res.Members)
if len(membershipChanged) == 0 {
membershipChanged = []int64{userID}
}
r.invalidateChannelMembershipProjection(res.Channel.ID, membershipChanged)
r.removeOnlineChannelMemberships(res.Channel.ID, userID)
r.recordChannelStateForUser(ctx, userID, res.Channel.ID, true)
updates := r.channelOperationUpdates(ctx, userID, res)
@ -658,6 +665,7 @@ func (r *Router) onMessagesHideChatJoinRequest(ctx context.Context, req *tg.Mess
return nil, channelInviteErr(err)
}
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
updates := r.channelOperationUpdates(ctx, userID, res)
r.appendPendingJoinRequestsUpdate(ctx, userID, updates, res.Channel)
@ -696,6 +704,7 @@ func (r *Router) onMessagesHideAllChatJoinRequests(ctx context.Context, req *tg.
return nil, channelInviteErr(err)
}
r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID)
r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members))
r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...)
updates := r.channelOperationUpdates(ctx, userID, res)
r.appendPendingJoinRequestsUpdate(ctx, userID, updates, res.Channel)