feat: state is now scoped by both user id and chat id

This commit is contained in:
abelith committed 2026-10-07 15:01:18 +03:00
1 parent 4b94fcb920
commit ec12f29fdb
3 files changed
+31 -23

No files matched your search

+5 -5
View File
@@ -27,9 +27,9 @@ type EventIsolation interface {
} }
type FSMStorage interface { type FSMStorage interface {
Get(ctx context.Context, userID int) (state.Snapshot, error) Get(ctx context.Context, scope isolation.Scope) (state.Snapshot, error)
Put(ctx context.Context, state state.Snapshot) error Put(ctx context.Context, state state.Snapshot) error
Delete(ctx context.Context, userID int) error Delete(ctx context.Context, scope isolation.Scope) error
} }
type UpdateHandler interface { type UpdateHandler interface {
@@ -157,13 +157,13 @@ func (d *Dispatcher) handleUpdates(ch <-chan model.Update) {
func (d *Dispatcher) handleUpdate(ctx context.Context, upd model.Update) { func (d *Dispatcher) handleUpdate(ctx context.Context, upd model.Update) {
var st *state.Context var st *state.Context
snapshot, err := d.fsmStorage.Get(ctx, int(upd.UserID)) snapshot, err := d.fsmStorage.Get(ctx, isolation.Scope{UserID: upd.UserID, ChatID: upd.ChatID})
if err != nil && !errors.Is(err, state.ErrStateNotFound) { if err != nil && !errors.Is(err, state.ErrStateNotFound) {
log.Error().Err(err).Send() log.Error().Err(err).Send()
return return
} }
if errors.Is(err, state.ErrStateNotFound) { if errors.Is(err, state.ErrStateNotFound) {
st = state.New(int(upd.UserID)) st = state.New(isolation.Scope{UserID: upd.UserID, ChatID: upd.ChatID})
} else { } else {
st = state.Restore(snapshot) st = state.Restore(snapshot)
} }
@@ -177,7 +177,7 @@ func (d *Dispatcher) handleUpdate(ctx context.Context, upd model.Update) {
st = c.State() st = c.State()
if st.State() == state.ClearState { if st.State() == state.ClearState {
if err := d.fsmStorage.Delete(ctx, int(upd.UserID)); err != nil { if err := d.fsmStorage.Delete(ctx, isolation.Scope{UserID: upd.UserID, ChatID: upd.ChatID}); err != nil {
log.Error().Err(err).Send() log.Error().Err(err).Send()
return return
} }
+19 -5
View File
@@ -19,6 +19,11 @@ type EventIsolation struct {
ttl time.Duration ttl time.Duration
} }
type fsmKey struct {
userID int64
chatID int64
}
type eventIsolationKey struct { type eventIsolationKey struct {
userID int userID int
chatID int chatID int
@@ -34,8 +39,11 @@ func NewFSMStorage() *FSMStorage {
return &FSMStorage{} return &FSMStorage{}
} }
func (f *FSMStorage) Get(_ context.Context, userID int) (state.Snapshot, error) { func (f *FSMStorage) Get(_ context.Context, scope isolation.Scope) (state.Snapshot, error) {
st, ok := f.m.Load(userID) st, ok := f.m.Load(fsmKey{
userID: scope.UserID,
chatID: scope.ChatID,
})
if !ok { if !ok {
return state.Snapshot{}, state.ErrStateNotFound return state.Snapshot{}, state.ErrStateNotFound
} }
@@ -44,12 +52,18 @@ func (f *FSMStorage) Get(_ context.Context, userID int) (state.Snapshot, error)
} }
func (f *FSMStorage) Put(_ context.Context, snapshot state.Snapshot) error { func (f *FSMStorage) Put(_ context.Context, snapshot state.Snapshot) error {
f.m.Store(snapshot.ID, snapshot) f.m.Store(fsmKey{
userID: snapshot.Scope.UserID,
chatID: snapshot.Scope.ChatID,
}, snapshot)
return nil return nil
} }
func (f *FSMStorage) Delete(_ context.Context, userID int) error { func (f *FSMStorage) Delete(_ context.Context, scope isolation.Scope) error {
f.m.Delete(userID) f.m.Delete(fsmKey{
userID: scope.UserID,
chatID: scope.ChatID,
})
return nil return nil
} }
+7 -13
View File
@@ -3,8 +3,7 @@ package state
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/abelith/gomax/v1/isolation"
"github.com/rs/zerolog/log"
) )
var ( var (
@@ -32,13 +31,13 @@ func (JSONCodec) Unmarshal(d []byte, v any) error {
} }
type Snapshot struct { type Snapshot struct {
ID int `json:"id"` Scope isolation.Scope `json:"id"`
State State `json:"state"` State State `json:"state"`
Data map[string][]byte `json:"data"` Data map[string][]byte `json:"data"`
} }
type Context struct { type Context struct {
id int scope isolation.Scope
state State state State
data map[string][]byte data map[string][]byte
codec Codec codec Codec
@@ -48,9 +47,9 @@ type Option func(*Context)
func WithCodec(c Codec) Option { return func(sc *Context) { sc.codec = c } } func WithCodec(c Codec) Option { return func(sc *Context) { sc.codec = c } }
func New(id int, opts ...Option) *Context { func New(scope isolation.Scope, opts ...Option) *Context {
sc := &Context{ sc := &Context{
id: id, scope: scope,
data: make(map[string][]byte), data: make(map[string][]byte),
codec: JSONCodec{}, codec: JSONCodec{},
} }
@@ -61,7 +60,7 @@ func New(id int, opts ...Option) *Context {
} }
func Restore(s Snapshot, opts ...Option) *Context { func Restore(s Snapshot, opts ...Option) *Context {
sc := New(s.ID, opts...) sc := New(s.Scope, opts...)
sc.state = s.State sc.state = s.State
for k, v := range s.Data { for k, v := range s.Data {
sc.data[k] = v sc.data[k] = v
@@ -74,15 +73,10 @@ func (sc *Context) Snapshot() Snapshot {
for k, v := range sc.data { for k, v := range sc.data {
data[k] = v data[k] = v
} }
return Snapshot{ID: sc.id, State: sc.state, Data: data} return Snapshot{Scope: sc.scope, State: sc.state, Data: data}
} }
func (sc *Context) Set(state State) { func (sc *Context) Set(state State) {
log.Debug().
Int("user_id", sc.id).
Str("from", string(sc.state)).
Str("to", string(state)).
Msg("fsm state transition")
sc.state = state sc.state = state
} }