You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
113 lines
3.0 KiB
Go
113 lines
3.0 KiB
Go
package state
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
sharedredis "knowfoolery/backend/shared/infra/database/redis"
|
|
)
|
|
|
|
// Store provides redis-backed ephemeral state with in-process lock fallback.
|
|
type Store struct {
|
|
redis *sharedredis.Client
|
|
|
|
mu sync.Mutex
|
|
locks map[string]time.Time
|
|
}
|
|
|
|
// NewStore creates a new state store.
|
|
func NewStore(redisClient *sharedredis.Client) *Store {
|
|
return &Store{redis: redisClient, locks: map[string]time.Time{}}
|
|
}
|
|
|
|
func activeKey(playerID string) string { return fmt.Sprintf("gs:active:%s", playerID) }
|
|
func timerKey(sessionID string) string { return fmt.Sprintf("gs:timer:%s", sessionID) }
|
|
func lockKey(sessionID string) string { return fmt.Sprintf("gs:lock:session:%s", sessionID) }
|
|
|
|
// GetActiveSession returns active session for player when available.
|
|
func (s *Store) GetActiveSession(ctx context.Context, playerID string) (string, bool) {
|
|
if s.redis == nil {
|
|
return "", false
|
|
}
|
|
value, err := s.redis.Get(ctx, activeKey(playerID))
|
|
if err != nil || strings.TrimSpace(value) == "" {
|
|
return "", false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
// SetActiveSession stores active session mapping.
|
|
func (s *Store) SetActiveSession(ctx context.Context, playerID, sessionID string, ttl time.Duration) error {
|
|
if s.redis == nil {
|
|
return nil
|
|
}
|
|
return s.redis.Set(ctx, activeKey(playerID), sessionID, ttl)
|
|
}
|
|
|
|
// ClearActiveSession clears player active session key.
|
|
func (s *Store) ClearActiveSession(ctx context.Context, playerID string) error {
|
|
if s.redis == nil {
|
|
return nil
|
|
}
|
|
return s.redis.Delete(ctx, activeKey(playerID))
|
|
}
|
|
|
|
// SetTimer stores a session expiration timestamp.
|
|
func (s *Store) SetTimer(ctx context.Context, sessionID string, expiresAt time.Time, ttl time.Duration) error {
|
|
if s.redis == nil {
|
|
return nil
|
|
}
|
|
return s.redis.Set(ctx, timerKey(sessionID), expiresAt.UTC().Format(time.RFC3339Nano), ttl)
|
|
}
|
|
|
|
// GetTimer retrieves a session expiration timestamp.
|
|
func (s *Store) GetTimer(ctx context.Context, sessionID string) (time.Time, bool) {
|
|
if s.redis == nil {
|
|
return time.Time{}, false
|
|
}
|
|
value, err := s.redis.Get(ctx, timerKey(sessionID))
|
|
if err != nil {
|
|
return time.Time{}, false
|
|
}
|
|
t, parseErr := time.Parse(time.RFC3339Nano, value)
|
|
if parseErr != nil {
|
|
return time.Time{}, false
|
|
}
|
|
return t, true
|
|
}
|
|
|
|
// ClearTimer clears a session timer key.
|
|
func (s *Store) ClearTimer(ctx context.Context, sessionID string) error {
|
|
if s.redis == nil {
|
|
return nil
|
|
}
|
|
return s.redis.Delete(ctx, timerKey(sessionID))
|
|
}
|
|
|
|
// AcquireLock acquires a best-effort local lock.
|
|
func (s *Store) AcquireLock(ctx context.Context, sessionID string, ttl time.Duration) bool {
|
|
if ttl <= 0 {
|
|
ttl = 3 * time.Second
|
|
}
|
|
now := time.Now()
|
|
expires := now.Add(ttl)
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if existing, ok := s.locks[lockKey(sessionID)]; ok && existing.After(now) {
|
|
return false
|
|
}
|
|
s.locks[lockKey(sessionID)] = expires
|
|
return true
|
|
}
|
|
|
|
// ReleaseLock releases lock for given session.
|
|
func (s *Store) ReleaseLock(ctx context.Context, sessionID string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
delete(s.locks, lockKey(sessionID))
|
|
}
|