refactor(chat): simplify member search cache
This commit is contained in:
parent
8ccfd47046
commit
bdea99d4f9
4 changed files with 67 additions and 63 deletions
46
internal/cache/cache.go
vendored
46
internal/cache/cache.go
vendored
|
|
@ -1,46 +0,0 @@
|
|||
// Package cache is used for completion to not overflow the gateway with redundant search requests.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
items sync.Map
|
||||
}
|
||||
|
||||
func New() *Cache {
|
||||
return &Cache{}
|
||||
}
|
||||
|
||||
func (c *Cache) Create(query string, value uint) {
|
||||
c.items.Store(query, value)
|
||||
}
|
||||
|
||||
func (c *Cache) Get(query string) (uint, bool) {
|
||||
i, ok := c.items.Load(query)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return i.(uint), true
|
||||
}
|
||||
|
||||
// Invalidate is only needed when a member leaves and the search query reaches
|
||||
// the search limit.
|
||||
// "aa", "ab", "ac", ..., "ay" // where length is longer than the limit
|
||||
// if "ay" leaves, then "az" would not be loaded becaue it would not be
|
||||
// returned by the search results because of the search limit.
|
||||
func (c *Cache) Invalidate(name string, limit uint) {
|
||||
for name != "" {
|
||||
if v, ok := c.items.Load(name); ok && v.(uint) >= limit {
|
||||
// This query, and every shorter prefix of it, may now be missing
|
||||
// results, so drop the whole chain to force a fresh search.
|
||||
for name != "" {
|
||||
c.items.Delete(name)
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
return
|
||||
}
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import (
|
|||
"github.com/ayn2op/arikawa/v3/state"
|
||||
"github.com/ayn2op/arikawa/v3/utils/json/option"
|
||||
"github.com/ayn2op/arikawa/v3/utils/sendpart"
|
||||
"github.com/ayn2op/discordo/internal/cache"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/consts"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
|
|
@ -51,11 +50,11 @@ type composer struct {
|
|||
|
||||
cfg *config.Config
|
||||
|
||||
edit bool
|
||||
sendMessageData *api.SendMessageData
|
||||
cache *cache.Cache
|
||||
mentionsList *mentionslist.Model
|
||||
lastSearch time.Time
|
||||
edit bool
|
||||
sendMessageData *api.SendMessageData
|
||||
memberSearchCache map[string]uint
|
||||
mentionsList *mentionslist.Model
|
||||
lastSearch time.Time
|
||||
|
||||
typingUntil time.Time
|
||||
}
|
||||
|
|
@ -67,12 +66,12 @@ var _ help.KeyMap = (*composer)(nil)
|
|||
|
||||
func newComposer(cfg *config.Config, chat *Model) *composer {
|
||||
c := &composer{
|
||||
TextArea: tview.NewTextArea(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
sendMessageData: &api.SendMessageData{},
|
||||
cache: cache.New(),
|
||||
mentionsList: mentionslist.NewModel(cfg),
|
||||
TextArea: tview.NewTextArea(),
|
||||
cfg: cfg,
|
||||
chat: chat,
|
||||
sendMessageData: &api.SendMessageData{},
|
||||
memberSearchCache: make(map[string]uint),
|
||||
mentionsList: mentionslist.NewModel(cfg),
|
||||
}
|
||||
ui.ConfigureBox(c.Box, &cfg.Theme)
|
||||
c.
|
||||
|
|
@ -629,16 +628,16 @@ func (c *composer) searchMember(gID discord.GuildID, name string) tview.Cmd {
|
|||
}
|
||||
|
||||
key := gID.String() + " " + name
|
||||
if _, ok := c.cache.Get(key); ok {
|
||||
if _, ok := c.memberSearchCache[key]; ok {
|
||||
return nil
|
||||
}
|
||||
// If searching for "ab" returns less than SearchLimit,
|
||||
// then "abc" would not return anything new because we already searched
|
||||
// everything starting with "ab". This will still be true even if a new
|
||||
// member joins because arikawa loads new members into the state.
|
||||
if count, ok := c.cache.Get(key[:len(key)-1]); ok {
|
||||
if count, ok := c.memberSearchCache[key[:len(key)-1]]; ok {
|
||||
if count < c.chat.state.MemberState.SearchLimit {
|
||||
c.cache.Create(key, count)
|
||||
c.memberSearchCache[key] = count
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -671,7 +670,7 @@ func (c *composer) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) tv
|
|||
return nil
|
||||
}
|
||||
|
||||
c.cache.Create(key, uint(len(event.Members)))
|
||||
c.memberSearchCache[key] = uint(len(event.Members))
|
||||
return func() tview.Msg {
|
||||
return tabSuggestMsg{}
|
||||
}
|
||||
|
|
|
|||
41
internal/ui/chat/composer_test.go
Normal file
41
internal/ui/chat/composer_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
"github.com/ayn2op/arikawa/v3/gateway"
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
)
|
||||
|
||||
func TestMemberSearchCache(t *testing.T) {
|
||||
cfg, err := config.Load(filepath.Join(t.TempDir(), "config.toml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := NewModel(cfg, "")
|
||||
m.state.MemberState.SearchLimit = 2
|
||||
c := m.composer
|
||||
c.onGuildMembersChunk(&gateway.GuildMembersChunkEvent{
|
||||
Nonce: memberSearchNonce + "1 ab",
|
||||
Members: []discord.Member{{}},
|
||||
})
|
||||
if cmd := c.searchMember(1, "ab"); cmd != nil {
|
||||
t.Fatal("cached query returned a network command")
|
||||
}
|
||||
if cmd := c.searchMember(1, "abc"); cmd != nil || c.memberSearchCache["1 abc"] != 1 {
|
||||
t.Fatal("incomplete prefix results were not reused")
|
||||
}
|
||||
c.memberSearchCache["1 a"] = 2
|
||||
c.memberSearchCache["2 a"] = 2
|
||||
m.onGuildMemberRemove(&gateway.GuildMemberRemoveEvent{
|
||||
GuildID: 1,
|
||||
User: discord.User{Username: "abc"},
|
||||
})
|
||||
want := map[string]uint{"1 ab": 1, "1 abc": 1, "2 a": 2}
|
||||
if !maps.Equal(c.memberSearchCache, want) {
|
||||
t.Fatalf("cache after invalidation = %v, want %v", c.memberSearchCache, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -170,7 +170,17 @@ func (m *Model) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) tview
|
|||
}
|
||||
|
||||
func (m *Model) onGuildMemberRemove(event *gateway.GuildMemberRemoveEvent) {
|
||||
m.composer.cache.Invalidate(event.GuildID.String()+" "+event.User.Username, m.state.MemberState.SearchLimit)
|
||||
memberSearchCache := m.composer.memberSearchCache
|
||||
for name := event.GuildID.String() + " " + event.User.Username; name != ""; name = name[:len(name)-1] {
|
||||
if count, ok := memberSearchCache[name]; ok && count >= m.state.MemberState.SearchLimit {
|
||||
// A full result set may now be missing members; search these prefixes again.
|
||||
for name != "" {
|
||||
delete(memberSearchCache, name)
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) onTypingStart(event *gateway.TypingStartEvent) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue