v0.9.0 user and session management improvement, xterm.js addons

This commit is contained in:
2026-02-07 15:37:52 -05:00
parent 0a85cc88bb
commit 820ad7eb27
62 changed files with 875 additions and 446 deletions

View File

@ -1,24 +1,35 @@
// FILE: lixenwraith/chess/internal/server/service/service.go
package service
import (
"chess/internal/server/core"
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"chess/internal/server/game"
"chess/internal/server/storage"
)
// Service is a pure state manager for chess games with optional persistence
const (
MaxComputerGames = 10
MaxUsers = 100
PermanentSlots = 10
TempUserTTL = 24 * time.Hour
SessionTTL = 7 * 24 * time.Hour
CleanupJobInterval = 1 * time.Hour
)
// Service coordinates game state, user management, and storage
type Service struct {
games map[string]*game.Game
mu sync.RWMutex
store *storage.Store // nil if persistence disabled
jwtSecret []byte
waiter *WaitRegistry // Long-polling notification registry
games map[string]*game.Game
mu sync.RWMutex
store *storage.Store
jwtSecret []byte
waiter *WaitRegistry
computerGames atomic.Int32 // Active games with computer players
}
// New creates a new service instance with optional storage
@ -47,12 +58,56 @@ func (s *Service) RegisterWait(gameID string, moveCount int, ctx context.Context
return s.waiter.RegisterWait(gameID, moveCount, ctx)
}
// CanCreateComputerGame checks if a new computer game can be created
func (s *Service) CanCreateComputerGame() bool {
return s.computerGames.Load() < MaxComputerGames
}
// IncrementComputerGames increments the computer game counter
func (s *Service) IncrementComputerGames() {
s.computerGames.Add(1)
}
// DecrementComputerGames decrements the computer game counter
func (s *Service) DecrementComputerGames() {
s.computerGames.Add(-1)
}
// GetComputerGameCount returns current computer game count
func (s *Service) GetComputerGameCount() int32 {
return s.computerGames.Load()
}
// ClaimGameSlot claims a player slot for a user
func (s *Service) ClaimGameSlot(gameID string, color core.Color, userID string) error {
s.mu.Lock()
defer s.mu.Unlock()
g, ok := s.games[gameID]
if !ok {
return fmt.Errorf("game not found: %s", gameID)
}
return g.ClaimSlot(color, userID)
}
// GetSlotOwner returns the user who claimed a slot
func (s *Service) GetSlotOwner(gameID string, color core.Color) (string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
g, ok := s.games[gameID]
if !ok {
return "", fmt.Errorf("game not found: %s", gameID)
}
return g.GetSlotOwner(color), nil
}
// Shutdown gracefully shuts down the service
func (s *Service) Shutdown(timeout time.Duration) error {
// Collect all errors
var errs []error
// Shutdown wait registry
if err := s.waiter.Shutdown(timeout); err != nil {
errs = append(errs, fmt.Errorf("wait registry: %w", err))
}
@ -60,10 +115,8 @@ func (s *Service) Shutdown(timeout time.Duration) error {
s.mu.Lock()
defer s.mu.Unlock()
// Clear all games
s.games = make(map[string]*game.Game)
// Close storage if enabled
if s.store != nil {
if err := s.store.Close(); err != nil {
errs = append(errs, fmt.Errorf("storage: %w", err))
@ -71,4 +124,40 @@ func (s *Service) Shutdown(timeout time.Duration) error {
}
return errors.Join(errs...)
}
// RunCleanupJob runs periodic cleanup of expired users and sessions
func (s *Service) RunCleanupJob(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s.cleanupExpired()
}
}
}
func (s *Service) cleanupExpired() {
if s.store == nil {
return
}
// Cleanup expired temp users
if deleted, err := s.store.DeleteExpiredTempUsers(); err != nil {
// Log but don't fail
fmt.Printf("cleanup: failed to delete expired users: %v\n", err)
} else if deleted > 0 {
fmt.Printf("cleanup: deleted %d expired temp users\n", deleted)
}
// Cleanup expired sessions
if deleted, err := s.store.DeleteExpiredSessions(); err != nil {
fmt.Printf("cleanup: failed to delete expired sessions: %v\n", err)
} else if deleted > 0 {
fmt.Printf("cleanup: deleted %d expired sessions\n", deleted)
}
}