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