256 lines
11 KiB
Markdown
256 lines
11 KiB
Markdown
# Architecture
|
|
|
|
## Components
|
|
|
|
### Transport Layer (`internal/server/http`)
|
|
Fiber web server handling HTTP requests/responses. Implements routing, rate limiting, content-type validation, JWT authentication middleware, request parsing. Translates HTTP to internal Command objects.
|
|
|
|
### Processing Layer (`internal/server/processor`)
|
|
Central command handler containing business logic. Single `Execute(Command)` entry point decouples transport from logic. Uses synchronous UCI engine for validation, asynchronous EngineQueue for computer moves. Commands include optional user context for authenticated operations.
|
|
|
|
### Service Layer (`internal/server/service`)
|
|
In-memory live-game storage with authentication support. A mutex protects each
|
|
game transition and callers receive immutable game views rather than mutable
|
|
game pointers. The service manages game lifecycle, snapshots, player
|
|
configuration, user accounts, JWT tokens, persistence, and terminal-game
|
|
eviction. Eviction removes only the memory copy; durable replay data remains.
|
|
|
|
#### Long-Polling Registry (`internal/service/waiter.go`)
|
|
Manages clients waiting for game state changes via HTTP long-polling. Tracks move counts per client, sends notifications on state changes, enforces 30-second timeout. Non-blocking notification pattern handles slow clients gracefully. Coordinates with service layer for game updates and deletion events.
|
|
|
|
#### Authentication Module (`internal/service/user.go`, `internal/http/auth.go`)
|
|
- **Password Hashing**: Argon2id for secure password storage
|
|
- **JWT Management**: HS256 tokens with 7-day expiration
|
|
- **User Operations**: Registration, login, profile management
|
|
- **Session Tracking**: One persisted session per user, with JWT subject/session
|
|
binding and last-login timestamps
|
|
|
|
### Storage Layer (`internal/server/storage`)
|
|
SQLite persistence with ordered asynchronous writes for gameplay and
|
|
synchronous writes for authentication. A bounded channel (1,000 operations)
|
|
feeds one transactional writer. Move persistence groups the move, first-move
|
|
slot claim, and terminal result in one transaction. Replay reads insert a
|
|
barrier behind accepted writes and then read the game and moves in one SQLite
|
|
snapshot.
|
|
|
|
WAL, foreign-key enforcement, a five-second busy timeout, and NORMAL
|
|
synchronous mode are configured in the connection string so every pooled
|
|
connection receives the same settings. The pool is deliberately small (eight
|
|
open, four idle) because SQLite has one writer. A write failure or full queue
|
|
marks storage degraded; live play remains available in memory and `/health`
|
|
reports that durable history may be incomplete.
|
|
|
|
### Supporting Modules
|
|
- **Engine** (`internal/engine`): UCI protocol wrapper for Stockfish process communication
|
|
- **Game** (`internal/game`): Game state with snapshot history and player associations
|
|
- **Board** (`internal/board`): FEN parsing and ASCII generation
|
|
- **Core** (`internal/core`): Shared types, API models, error constants
|
|
- **CLI** (`cmd/chess-server/cli`): Database and user management commands
|
|
- **Client** (`cmd/chess-client-cli`, `internal/client`): Interactive debugging client with command registry, session management, and colored terminal output
|
|
|
|
## Request Flow
|
|
|
|
### User Registration
|
|
1. HTTP handler receives `POST /auth/register` with credentials
|
|
2. Validates username format and password strength
|
|
3. Service layer hashes password with Argon2id
|
|
4. Creates user record with unique ID (collision detection)
|
|
5. Generates JWT token
|
|
6. Returns token and user information
|
|
|
|
### Authenticated Game Creation
|
|
1. HTTP handler receives `POST /games` with optional Bearer token
|
|
2. Middleware validates JWT if present
|
|
3. Creates CreateGameCommand with user ID context
|
|
4. Processor creates game with user ID for human players
|
|
5. Service associates game with authenticated user
|
|
6. Returns game with player IDs matching user
|
|
|
|
### Human Move (Authenticated)
|
|
1. HTTP handler receives `POST /games/{id}/moves` with move
|
|
2. Optional JWT validation for user verification
|
|
3. Creates MakeMoveCommand, calls `processor.Execute()`
|
|
4. Processor validates move via locked validation engine
|
|
5. If legal, gets new FEN from engine
|
|
6. Calls `service.ApplyMoveWithState()` with the FEN, turn, and state that were validated
|
|
7. Service rejects a stale concurrent commit or atomically updates the move, optional slot claim, and terminal result
|
|
8. The same logical mutation is queued as one SQLite transaction
|
|
9. Returns GameResponse
|
|
|
|
### Computer Move
|
|
1. HTTP handler receives `POST /games/{id}/moves` with `{"move": "cccc"}`
|
|
2. Processor sets game state to `pending`
|
|
3. Submits task to EngineQueue, returns immediately
|
|
4. Worker goroutine calculates move with dedicated Stockfish instance
|
|
5. Callback updates game state via service
|
|
6. Client polls for completion
|
|
7. Returns GameResponse
|
|
|
|
### Long-Polling Flow
|
|
1. Client sends `GET /games/{id}?wait=true&moveCount=N`
|
|
2. Handler creates context from HTTP connection
|
|
3. Registers wait with WaitRegistry using game ID and move count
|
|
4. If game state unchanged, blocks up to 30 seconds
|
|
5. On any game update, NotifyGame sends to all waiters
|
|
6. Returns immediately with current state
|
|
7. Client disconnection cancels wait via context
|
|
8. Game deletion notifies and removes all waiters
|
|
|
|
### Durable Replay Read
|
|
1. Client requests `GET /api/v1/games/{id}/history`
|
|
2. Storage queues a barrier after all previously accepted gameplay writes
|
|
3. The writer reaches the barrier only after those transactions finish
|
|
4. Storage reads the game row and ordered moves in one read transaction
|
|
5. The API returns the initial FEN plus every UCI move and resulting FEN
|
|
6. This path works after terminal-memory eviction or a server restart
|
|
|
|
## Persistence Flow
|
|
|
|
### User Write Operations (Synchronous)
|
|
1. Service serializes public registrations within the process
|
|
2. Storage checks uniqueness and capacity in a transaction
|
|
3. At capacity, the oldest temporary user is evicted in that transaction
|
|
4. The new user and initial session commit together; any failure rolls back the entire operation
|
|
5. Login replaces the user's single session with one SQLite UPSERT
|
|
6. Other account mutations commit before success is returned
|
|
|
|
### Game Write Operations (Asynchronous)
|
|
1. Service layer calls a storage method (`RecordNewGame`, `RecordMove`, `RecordPlayers`, or `RewindGame`)
|
|
2. Operation queued to buffered channel (non-blocking)
|
|
3. Writer goroutine processes queue sequentially
|
|
4. Each logical mutation commits in one transaction
|
|
5. A full queue or write failure is logged and triggers degraded memory-only mode
|
|
6. Shutdown rejects new writes and drains every write already accepted
|
|
|
|
### Query Operations
|
|
1. Replay-sensitive game reads wait on the async-write barrier
|
|
2. User-game filtering matches creation-time player IDs and later claim IDs
|
|
3. Move lookup uses the `UNIQUE(game_id, move_number)` index prefix and returns move order directly
|
|
4. Username and email lookups are case-insensitive
|
|
5. CLI database queries format records as tabular output
|
|
|
|
## Concurrency
|
|
|
|
- **HTTP Server**: Fiber handles concurrent connections
|
|
- **Game State**: Single RWMutex protects game map (concurrent reads, serial writes)
|
|
- **Move Validation**: Optimistic FEN/state/turn checks reject a result if the game changed while Stockfish was validating
|
|
- **Engine Workers**: Fixed pool (2 workers) with dedicated Stockfish processes
|
|
- **Validation Engine**: Single mutex-protected instance for synchronous validation
|
|
- **Storage Writer**: Single goroutine processes game write queue sequentially
|
|
- **User Operations**: Direct database access with transaction isolation
|
|
- **PID Lock**: File-based exclusive lock prevents multiple instances
|
|
|
|
## Data Structures
|
|
|
|
### User Record
|
|
```go
|
|
type UserRecord struct {
|
|
UserID string
|
|
Username string
|
|
Email string
|
|
PasswordHash string
|
|
CreatedAt time.Time
|
|
LastLoginAt *time.Time
|
|
}
|
|
```
|
|
|
|
### Game Snapshot with User Context
|
|
```go
|
|
type Snapshot struct {
|
|
FEN string
|
|
PreviousMove string
|
|
NextTurnColor Color
|
|
PlayerID string // User ID or generated UUID
|
|
}
|
|
```
|
|
|
|
### JWT Claims
|
|
```go
|
|
{
|
|
"sub": "user-id",
|
|
"username": "alice",
|
|
"email": "alice@example.com",
|
|
"session_id": "session-id",
|
|
"exp": 1234567890
|
|
}
|
|
```
|
|
|
|
### Command Pattern with User Context
|
|
Commands encapsulate operations with type, arguments, and optional user ID for authenticated requests.
|
|
|
|
### Player Configuration
|
|
Players identified by UUID (authenticated users) or generated IDs (anonymous), configured with type (human/computer), skill level, and search time.
|
|
|
|
### Storage Schema
|
|
```sql
|
|
-- User authentication table
|
|
users (
|
|
user_id TEXT PRIMARY KEY,
|
|
username TEXT UNIQUE NOT NULL COLLATE NOCASE,
|
|
email TEXT COLLATE NOCASE,
|
|
password_hash TEXT NOT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_login_at DATETIME
|
|
)
|
|
|
|
-- Game storage with player associations
|
|
games (
|
|
game_id TEXT PRIMARY KEY,
|
|
initial_fen TEXT,
|
|
white_player_id TEXT, -- User ID or generated UUID
|
|
white_type INTEGER,
|
|
white_level INTEGER,
|
|
white_search_time INTEGER,
|
|
black_player_id TEXT, -- User ID or generated UUID
|
|
black_type INTEGER,
|
|
black_level INTEGER,
|
|
black_search_time INTEGER,
|
|
start_time_utc DATETIME,
|
|
result TEXT, -- white_wins, black_wins, draw, or stalemate
|
|
end_time_utc DATETIME,
|
|
white_claimed_by TEXT, -- user that claimed the slot after creation
|
|
black_claimed_by TEXT
|
|
)
|
|
|
|
-- Move history
|
|
moves (
|
|
move_id INTEGER PRIMARY KEY,
|
|
game_id TEXT,
|
|
move_number INTEGER,
|
|
move_uci TEXT,
|
|
fen_after_move TEXT,
|
|
player_color TEXT,
|
|
move_time_utc DATETIME,
|
|
FOREIGN KEY (game_id) REFERENCES games(game_id) ON DELETE CASCADE,
|
|
UNIQUE (game_id, move_number)
|
|
)
|
|
```
|
|
|
|
Schema version 2 is applied idempotently to legacy databases with guarded
|
|
`ALTER TABLE ADD COLUMN` migrations. Redundant indexes formerly duplicating
|
|
UNIQUE constraints or useful index prefixes are removed. Purpose-built partial
|
|
indexes cover non-empty email uniqueness, temporary-user cleanup, session
|
|
expiry, and post-creation game claims.
|
|
|
|
## Security Architecture
|
|
|
|
### Authentication Flow
|
|
1. Password validation enforces minimum complexity
|
|
2. Argon2id hashing prevents rainbow table attacks
|
|
3. JWT tokens expire after 7 days
|
|
4. Case-insensitive username/email matching prevents enumeration
|
|
5. Constant-time password verification prevents timing attacks
|
|
|
|
### Rate Limiting Strategy
|
|
- General API: 10 req/s per IP (20 in dev mode)
|
|
- Registration: 5 req/min per IP (prevent spam accounts)
|
|
- Login: 10 req/min per IP (prevent brute force)
|
|
- Game operations unaffected for authenticated users
|
|
|
|
### Data Protection
|
|
- Passwords never stored in plaintext
|
|
- JWT secret rotates on restart (or fixed in dev mode)
|
|
- User IDs use UUIDs with collision detection
|
|
- Transactions keep registration, sessions, moves, claims, results, and rewinds internally consistent
|
|
- Case-insensitive queries prevent duplicate accounts
|