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

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))
}