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.
This commit is contained in:
parent
2c782aab95
commit
55a6e0bb35
7 changed files with 116 additions and 0 deletions
|
|
@ -109,6 +109,60 @@ func TestMessagesGetFutureChatCreatorAfterLeaveAndCreatorLeaveTransfers(t *testi
|
|||
}
|
||||
}
|
||||
|
||||
func TestLeaveChannelInvalidatesStaleFullChannelProjection(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 9401, Phone: "15550009401", FirstName: "Owner"})
|
||||
member, _ := userStore.Create(ctx, domain.User{AccessHash: 9402, Phone: "15550009402", FirstName: "Member"})
|
||||
channelStore := memory.NewChannelStore()
|
||||
channelService := appchannels.NewService(channelStore)
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: channelService,
|
||||
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700009400, 0)})
|
||||
created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "leave projection",
|
||||
Megagroup: true,
|
||||
Date: 1700009400,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
if _, err := channelService.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{
|
||||
ChannelID: created.Channel.ID,
|
||||
Username: "leave_projection_pub",
|
||||
}); err != nil {
|
||||
t.Fatalf("publish channel: %v", err)
|
||||
}
|
||||
inputChannel := &tg.InputChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash}
|
||||
|
||||
if _, err := r.onChannelsJoinChannel(WithUserID(ctx, member.ID), inputChannel); err != nil {
|
||||
t.Fatalf("member joins: %v", err)
|
||||
}
|
||||
// Warm the channels.getFullChannel projection cache while still a member.
|
||||
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, member.ID), inputChannel)
|
||||
if err != nil {
|
||||
t.Fatalf("full channel while joined: %v", err)
|
||||
}
|
||||
if chat, ok := full.Chats[0].(*tg.Channel); !ok || chat.Left {
|
||||
t.Fatalf("joined full chat = %#v, want member (not left)", full.Chats[0])
|
||||
}
|
||||
|
||||
if _, err := r.onChannelsLeaveChannel(WithUserID(ctx, member.ID), inputChannel); err != nil {
|
||||
t.Fatalf("member leaves: %v", err)
|
||||
}
|
||||
|
||||
after, err := r.onChannelsGetFullChannel(WithUserID(ctx, member.ID), inputChannel)
|
||||
if err != nil {
|
||||
t.Fatalf("full channel after leave: %v", err)
|
||||
}
|
||||
chat, ok := after.Chats[0].(*tg.Channel)
|
||||
if !ok || !chat.Left {
|
||||
t.Fatalf("post-leave full chat = %#v, want left=true (stale projection served)", after.Chats[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesEditChatCreatorTransfersWithoutChannelPts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue