aboutsummaryrefslogtreecommitdiff
path: root/user/state.go
diff options
context:
space:
mode:
authorAnhgelus Morhtuuzh <william@herges.fr>2026-01-17 20:52:13 +0000
committerAnhgelus Morhtuuzh <william@herges.fr>2026-01-17 20:52:13 +0000
commita3543d79561a3754540b921c54c3c177016c2397 (patch)
tree318b29652c4f59a7f6a16ff7a566b1a9935d069d /user/state.go
parent05ec1c26fe884097efe8fe1490916c518028e597 (diff)
parentc661541e45dddd6a082af66fcf7df7ba7dfdc6a6 (diff)
Merge pull request '[Perf] Member state' (#5) from perf/member-state into main
Reviewed-on: https://git.anhgelus.world/anhgelus/les-copaings-bot/pulls/5
Diffstat (limited to 'user/state.go')
-rw-r--r--user/state.go220
1 files changed, 220 insertions, 0 deletions
diff --git a/user/state.go b/user/state.go
new file mode 100644
index 0000000..effdc80
--- /dev/null
+++ b/user/state.go
@@ -0,0 +1,220 @@
+package user
+
+import (
+ "context"
+ "errors"
+ "sync"
+ "time"
+
+ "github.com/anhgelus/gokord"
+ "github.com/nyttikord/gokord/state"
+)
+
+var ErrSyncingUnsavedData = errors.New("trying to sync unsaved data")
+
+type XPCached struct {
+ XP uint
+ Time time.Duration
+}
+
+type CopaingCached struct {
+ ID uint
+ DiscordID string
+ GuildID string
+ XP uint
+ XPs []XPCached
+ XPToAdd uint
+}
+
+// copaing turns a CopaingCached into a Copaing.
+// This operation get the copaing synced with the database.
+// It doesn't:
+// - save the copaing in the database, use CopaingCached.SaveInDB for that;
+// - sync the copaing cached, use CopaingCached.Sync for that.
+// TL;DR: don't use this method, unless you know what are you doing.
+func (cc *CopaingCached) copaing() *Copaing {
+ c := &Copaing{DiscordID: cc.DiscordID, GuildID: cc.GuildID}
+ if err := c.load(); err != nil {
+ panic(err)
+ }
+ return c
+}
+
+func (cc *CopaingCached) Sync(ctx context.Context) error {
+ if cc.mustSave() {
+ return ErrSyncingUnsavedData
+ }
+ synced := FromCopaing(cc.copaing())
+ synced.XP += cc.XPToAdd
+ synced.XPToAdd = cc.XPToAdd
+ err := synced.Save(ctx)
+ if err != nil {
+ return err
+ }
+ *cc = *synced
+ return nil
+}
+
+func (cc *CopaingCached) Save(ctx context.Context) error {
+ state := GetState(ctx)
+
+ state.mu.Lock()
+ defer state.mu.Unlock()
+
+ return state.storage.Write(KeyCopaingCachedRaw(cc.GuildID, cc.DiscordID), *cc)
+}
+
+func (cc *CopaingCached) SaveInDB(ctx context.Context) error {
+ c := cc.copaing()
+ c.CopaingXPs = append(c.CopaingXPs, CopaingXP{CopaingID: c.ID, XP: cc.XPToAdd, GuildID: c.GuildID})
+ err := c.Save()
+ if err != nil {
+ return err
+ }
+ cc.XPToAdd = 0
+ return cc.Save(ctx)
+}
+
+func (cc *CopaingCached) Delete(ctx context.Context) error {
+ c := cc.copaing()
+ err := c.Delete()
+ if err != nil {
+ return err
+ }
+ state := GetState(ctx)
+
+ state.mu.Lock()
+ defer state.mu.Unlock()
+
+ return state.storage.Delete(KeyCopaingCached(c))
+}
+
+func (cc *CopaingCached) mustSave() bool {
+ return cc.XPToAdd > 0
+}
+
+func saveStateInDB(ctx context.Context) error {
+ state := GetState(ctx)
+
+ state.saveInDB.Lock()
+ defer state.saveInDB.Unlock()
+
+ for _, v := range state.storage {
+ if v.mustSave() {
+ err := v.SaveInDB(ctx)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func FromCopaing(c *Copaing) *CopaingCached {
+ return &CopaingCached{
+ ID: c.ID,
+ DiscordID: c.DiscordID,
+ GuildID: c.GuildID,
+ XP: calcXP(c),
+ XPs: generateXPs(c),
+ XPToAdd: 0,
+ }
+}
+
+const KeyCopaingCachedPrefix = "cc:"
+
+func KeyCopaingCached(c *Copaing) state.Key {
+ return KeyCopaingCachedRaw(c.GuildID, c.DiscordID)
+}
+
+func KeyCopaingCachedRaw(guildID, copaingID string) state.Key {
+ return KeyCopaingCachedPrefix + state.Key(guildID+":"+copaingID)
+}
+
+type State struct {
+ mu sync.RWMutex
+ saveInDB sync.Mutex
+ storage state.MapStorage[CopaingCached]
+}
+
+func NewState() *State {
+ state := &State{
+ storage: state.MapStorage[CopaingCached]{},
+ }
+ var cs []*Copaing
+ err := gokord.DB.Find(&cs).Error
+ if err != nil {
+ panic(err)
+ }
+ for _, v := range cs {
+ FromCopaing(v).Save(SetState(context.Background(), state))
+ }
+ return state
+}
+
+const ContextKeyState = "state"
+
+func GetState(ctx context.Context) *State {
+ return ctx.Value(ContextKeyState).(*State)
+}
+
+func SetState(ctx context.Context, state *State) context.Context {
+ return context.WithValue(ctx, ContextKeyState, state)
+}
+
+func (s *State) Copaing(guildID, copaingID string) (*CopaingCached, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ raw, err := s.storage.Get(KeyCopaingCachedRaw(guildID, copaingID))
+ if err != nil {
+ return nil, err
+ }
+ c := raw.(CopaingCached)
+ return &c, nil
+}
+
+func (s *State) Copaings(guild string) []CopaingCached {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ var ccs []CopaingCached
+ for _, cc := range s.storage {
+ if cc.GuildID == guild {
+ ccs = append(ccs, cc)
+ }
+ }
+ return ccs
+}
+
+func calcXP(c *Copaing) uint {
+ var sum uint
+ for _, entry := range c.CopaingXPs {
+ sum += entry.XP
+ }
+ return sum
+}
+
+func generateXPs(c *Copaing) []XPCached {
+ data := map[time.Duration]XPCached{}
+ sixH := 6 * time.Hour
+ for _, xp := range c.CopaingXPs {
+ // we add sixH at the end because we want it to be rounded to ceil
+ since := time.Since(xp.CreatedAt)/sixH + sixH
+ if v, ok := data[since]; ok {
+ v.XP += xp.XP
+ } else {
+ data[since] = XPCached{
+ Time: since,
+ XP: xp.XP,
+ }
+ }
+ }
+ ccs := make([]XPCached, len(data))
+ i := 0
+ for _, v := range data {
+ ccs[i] = v
+ i++
+ }
+ return ccs
+}