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/state"
|
||||||
"github.com/ayn2op/arikawa/v3/utils/json/option"
|
"github.com/ayn2op/arikawa/v3/utils/json/option"
|
||||||
"github.com/ayn2op/arikawa/v3/utils/sendpart"
|
"github.com/ayn2op/arikawa/v3/utils/sendpart"
|
||||||
"github.com/ayn2op/discordo/internal/cache"
|
|
||||||
"github.com/ayn2op/discordo/internal/config"
|
"github.com/ayn2op/discordo/internal/config"
|
||||||
"github.com/ayn2op/discordo/internal/consts"
|
"github.com/ayn2op/discordo/internal/consts"
|
||||||
"github.com/ayn2op/discordo/internal/ui"
|
"github.com/ayn2op/discordo/internal/ui"
|
||||||
|
|
@ -53,7 +52,7 @@ type composer struct {
|
||||||
|
|
||||||
edit bool
|
edit bool
|
||||||
sendMessageData *api.SendMessageData
|
sendMessageData *api.SendMessageData
|
||||||
cache *cache.Cache
|
memberSearchCache map[string]uint
|
||||||
mentionsList *mentionslist.Model
|
mentionsList *mentionslist.Model
|
||||||
lastSearch time.Time
|
lastSearch time.Time
|
||||||
|
|
||||||
|
|
@ -71,7 +70,7 @@ func newComposer(cfg *config.Config, chat *Model) *composer {
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
chat: chat,
|
chat: chat,
|
||||||
sendMessageData: &api.SendMessageData{},
|
sendMessageData: &api.SendMessageData{},
|
||||||
cache: cache.New(),
|
memberSearchCache: make(map[string]uint),
|
||||||
mentionsList: mentionslist.NewModel(cfg),
|
mentionsList: mentionslist.NewModel(cfg),
|
||||||
}
|
}
|
||||||
ui.ConfigureBox(c.Box, &cfg.Theme)
|
ui.ConfigureBox(c.Box, &cfg.Theme)
|
||||||
|
|
@ -629,16 +628,16 @@ func (c *composer) searchMember(gID discord.GuildID, name string) tview.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
key := gID.String() + " " + name
|
key := gID.String() + " " + name
|
||||||
if _, ok := c.cache.Get(key); ok {
|
if _, ok := c.memberSearchCache[key]; ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// If searching for "ab" returns less than SearchLimit,
|
// If searching for "ab" returns less than SearchLimit,
|
||||||
// then "abc" would not return anything new because we already searched
|
// then "abc" would not return anything new because we already searched
|
||||||
// everything starting with "ab". This will still be true even if a new
|
// everything starting with "ab". This will still be true even if a new
|
||||||
// member joins because arikawa loads new members into the state.
|
// 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 {
|
if count < c.chat.state.MemberState.SearchLimit {
|
||||||
c.cache.Create(key, count)
|
c.memberSearchCache[key] = count
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -671,7 +670,7 @@ func (c *composer) onGuildMembersChunk(event *gateway.GuildMembersChunkEvent) tv
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.cache.Create(key, uint(len(event.Members)))
|
c.memberSearchCache[key] = uint(len(event.Members))
|
||||||
return func() tview.Msg {
|
return func() tview.Msg {
|
||||||
return tabSuggestMsg{}
|
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) {
|
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) {
|
func (m *Model) onTypingStart(event *gateway.TypingStartEvent) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue