v0.11.0 harden persistence and prepare game replays
This commit is contained in:
+86
-3
@@ -103,7 +103,9 @@ Returns server and storage status.
|
||||
Storage states:
|
||||
- `"disabled"` - No storage path configured
|
||||
- `"ok"` - Database operational with auth enabled
|
||||
- `"degraded"` - Write failures detected, operating memory-only
|
||||
- `"degraded"` - A persistence write failed or the write queue filled; live games continue in memory, but durable history is no longer complete
|
||||
|
||||
The top-level `status` is also `"degraded"` when storage is degraded.
|
||||
|
||||
### Create Game
|
||||
`POST /games`
|
||||
@@ -171,6 +173,82 @@ Response includes all game data. Compare `moves` array length to detect changes.
|
||||
- Client disconnection cancels wait immediately
|
||||
- Game deletion notifies all waiting clients
|
||||
|
||||
### Get Durable Game History
|
||||
`GET /games/{gameId}/history`
|
||||
|
||||
Returns the persisted replay line even after the live game has been unloaded
|
||||
from memory or the server has restarted. History is public to anyone who knows
|
||||
the game ID, matching the existing public live-game read model. Persistent
|
||||
storage must be enabled.
|
||||
|
||||
The response contains the initial FEN and an ordered FEN after every move, so a
|
||||
client can replay the game without running a chess engine.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"gameId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
|
||||
"initialFen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
||||
"result": "white_wins",
|
||||
"startTimeUtc": "2026-09-07T12:00:00Z",
|
||||
"endTimeUtc": "2026-09-07T12:15:00Z",
|
||||
"players": {
|
||||
"white": {"id": "user-id", "color": 1, "type": 1, "claimedBy": "user-id"},
|
||||
"black": {"id": "player-id", "color": 2, "type": 1}
|
||||
},
|
||||
"moves": [
|
||||
{
|
||||
"moveNumber": 1,
|
||||
"moveUci": "e2e4",
|
||||
"fenAfterMove": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
|
||||
"playerColor": "w",
|
||||
"moveTimeUtc": "2026-09-07T12:00:05Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`result` is omitted while a game is ongoing. Persisted terminal values are
|
||||
`white_wins`, `black_wins`, `draw`, and `stalemate`.
|
||||
|
||||
Returns 404 when the game has no durable record and 503 when persistence is
|
||||
disabled.
|
||||
|
||||
### List My Stored Games
|
||||
`GET /users/me/games?limit=50&offset=0`
|
||||
|
||||
Returns games associated with the authenticated user at creation time or by a
|
||||
later first-move slot claim. Requires `Authorization: Bearer <token>` and
|
||||
persistent storage.
|
||||
|
||||
- `limit`: 1-100; defaults to 50
|
||||
- `offset`: 0-1,000,000; defaults to 0
|
||||
|
||||
Each item contains game ID, initial FEN, result/timestamps, players, and move
|
||||
count. `nextOffset` is present only when another page exists.
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"games": [
|
||||
{
|
||||
"gameId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
|
||||
"initialFen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
||||
"result": "white_wins",
|
||||
"startTimeUtc": "2026-09-07T12:00:00Z",
|
||||
"endTimeUtc": "2026-09-07T12:15:00Z",
|
||||
"moveCount": 41,
|
||||
"players": {
|
||||
"white": {"id": "user-id", "color": 1, "type": 1, "claimedBy": "user-id"},
|
||||
"black": {"id": "player-id", "color": 2, "type": 1}
|
||||
}
|
||||
}
|
||||
],
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Make Move
|
||||
`POST /games/{gameId}/moves`
|
||||
|
||||
@@ -204,7 +282,8 @@ Returns ASCII board visualization.
|
||||
### Delete Game
|
||||
`DELETE /games/{gameId}`
|
||||
|
||||
Removes game from memory. Returns 204 on success.
|
||||
Unloads the live game from memory. Its persisted game and move history remain
|
||||
available through the history endpoint. Returns 204 on success.
|
||||
|
||||
## Error Format
|
||||
```json
|
||||
@@ -220,6 +299,8 @@ Error codes:
|
||||
- `INVALID_MOVE` - Illegal chess move
|
||||
- `NOT_HUMAN_TURN` - Wrong player type for turn
|
||||
- `GAME_OVER` - Game already ended
|
||||
- `GAME_CONFLICT` - Game changed while a move was being validated; refresh and retry
|
||||
- `STORAGE_UNAVAILABLE` - Durable history/list storage is disabled or degraded
|
||||
- `RATE_LIMIT_EXCEEDED` - Request limit exceeded
|
||||
- `INVALID_REQUEST` - Malformed request
|
||||
- `INVALID_CONTENT_TYPE` - Missing/wrong Content-Type header
|
||||
@@ -242,4 +323,6 @@ Tokens are HS256-signed JWTs valid for 7 days. Include in Authorization header:
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Token claims include `sub` (user ID), `username`, `email`, and `exp` (expiration).
|
||||
Token claims include `sub` (user ID), `username`, `email`, `session_id`, and
|
||||
`exp` (expiration). Authentication requires the session to exist, be unexpired,
|
||||
and belong to the JWT subject.
|
||||
|
||||
+69
-27
@@ -2,14 +2,18 @@
|
||||
|
||||
## Components
|
||||
|
||||
### Transport Layer (`internal/http`)
|
||||
### 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/processor`)
|
||||
### 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/service`)
|
||||
In-memory state storage with authentication support. Thread-safe game map protected by RWMutex. Manages game lifecycle, snapshots, player configuration, user accounts, and JWT token generation. Coordinates with storage layer for persistence of both games and users.
|
||||
### 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.
|
||||
@@ -18,18 +22,31 @@ Manages clients waiting for game state changes via HTTP long-polling. Tracks mov
|
||||
- **Password Hashing**: Argon2id for secure password storage
|
||||
- **JWT Management**: HS256 tokens with 7-day expiration
|
||||
- **User Operations**: Registration, login, profile management
|
||||
- **Session Tracking**: Last login timestamps
|
||||
- **Session Tracking**: One persisted session per user, with JWT subject/session
|
||||
binding and last-login timestamps
|
||||
|
||||
### Storage Layer (`internal/storage`)
|
||||
SQLite persistence with async writes for games, synchronous writes for authentication operations. Buffered channel (1000 ops) processes game writes sequentially in background. User operations use direct database access for consistency. Graceful degradation on write failures. WAL mode for development environments.
|
||||
### 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/chessd/cli`): Database and user management commands
|
||||
- **Client** (`cmd/chess-client`, `internal/client`): Interactive debugging client with command registry, session management, and colored terminal output
|
||||
- **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
|
||||
|
||||
@@ -55,9 +72,10 @@ SQLite persistence with async writes for games, synchronous writes for authentic
|
||||
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.ApplyMove()` to update state
|
||||
7. Persists move with player identification
|
||||
8. Returns GameResponse
|
||||
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"}`
|
||||
@@ -78,32 +96,44 @@ SQLite persistence with async writes for games, synchronous writes for authentic
|
||||
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 layer calls storage method directly (CreateUser, UpdateUserPassword, etc.)
|
||||
2. Operations use database transactions for consistency
|
||||
3. Unique constraint checks within transaction
|
||||
4. Immediate commit or rollback
|
||||
5. Returns success or specific error (duplicate username, etc.)
|
||||
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 storage method (RecordNewGame, RecordMove, DeleteUndoneMoves)
|
||||
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. Transactions ensure atomicity
|
||||
5. Failures trigger degradation to memory-only mode
|
||||
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. CLI invokes Store.QueryGames or Store.GetUserByUsername with filters
|
||||
2. Direct database read (no queue)
|
||||
3. Case-insensitive matching for usernames/emails
|
||||
4. Results formatted as tabular output
|
||||
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
|
||||
@@ -140,6 +170,7 @@ type Snapshot struct {
|
||||
"sub": "user-id",
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"session_id": "session-id",
|
||||
"exp": 1234567890
|
||||
}
|
||||
```
|
||||
@@ -174,7 +205,11 @@ games (
|
||||
black_type INTEGER,
|
||||
black_level INTEGER,
|
||||
black_search_time INTEGER,
|
||||
start_time_utc DATETIME
|
||||
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
|
||||
@@ -186,10 +221,17 @@ moves (
|
||||
fen_after_move TEXT,
|
||||
player_color TEXT,
|
||||
move_time_utc DATETIME,
|
||||
FOREIGN KEY (game_id) REFERENCES games(game_id)
|
||||
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
|
||||
@@ -209,5 +251,5 @@ moves (
|
||||
- Passwords never stored in plaintext
|
||||
- JWT secret rotates on restart (or fixed in dev mode)
|
||||
- User IDs use UUIDs with collision detection
|
||||
- Transactions ensure data consistency
|
||||
- Transactions keep registration, sessions, moves, claims, results, and rewinds internally consistent
|
||||
- Case-insensitive queries prevent duplicate accounts
|
||||
|
||||
+13
-5
@@ -12,6 +12,14 @@ The chess client is an interactive command-line debugging tool for the chess ser
|
||||
- Verbose mode for detailed API request/response inspection
|
||||
- Long-polling support for real-time game updates
|
||||
|
||||
## Replay Foundation
|
||||
|
||||
The Go API client exposes `GetGameHistory(gameID)` and
|
||||
`GetMyGames(limit, offset)`, including durable result, timestamps, player
|
||||
claims, move count, ordered UCI moves, and FEN after every move. Interactive
|
||||
`games`/`replay` commands and terminal playback controls are intentionally
|
||||
deferred to the dedicated replay iteration; see [Replay Implementation Tasks](./todo.md).
|
||||
|
||||
## Building
|
||||
```bash
|
||||
go build ./cmd/chess-client-cli
|
||||
@@ -128,10 +136,10 @@ chess > state
|
||||
```
|
||||
|
||||
#### `delete` / `d`
|
||||
Delete game from server.
|
||||
Unload a live game from server memory. Durable history remains available.
|
||||
```
|
||||
chess > delete # Delete current game
|
||||
chess > delete <gameId> # Delete specific game
|
||||
chess > delete # Unload current live game
|
||||
chess > delete <gameId> # Unload specific live game
|
||||
```
|
||||
|
||||
#### `poll` / `p`
|
||||
@@ -230,9 +238,9 @@ ASCII board with colored pieces:
|
||||
```
|
||||
|
||||
### Move History
|
||||
Displayed in algebraic notation with move numbers:
|
||||
Displayed in UCI notation with move numbers:
|
||||
```
|
||||
History: 1.e4 e5 2.Nf3 Nc6 3.Bb5
|
||||
History: 1.e2e4 e7e5 2.g1f3 b8c6 3.f1b5
|
||||
```
|
||||
|
||||
## Workflows
|
||||
|
||||
+56
-32
@@ -2,7 +2,7 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.24+
|
||||
- Go 1.26+
|
||||
- Stockfish in PATH
|
||||
- SQLite3
|
||||
- Git
|
||||
@@ -14,7 +14,7 @@
|
||||
git clone https://github.com/lixenwraith/chess
|
||||
cd chess
|
||||
go build ./cmd/chess-server
|
||||
go build ./cmd/chess-client
|
||||
go build ./cmd/chess-client-cli
|
||||
```
|
||||
|
||||
## Running
|
||||
@@ -29,20 +29,30 @@ go build ./cmd/chess-client
|
||||
- `-storage-path`: SQLite database file path (enables persistence and authentication)
|
||||
- `-pid`: PID file path for process tracking
|
||||
- `-pid-lock`: Enable exclusive locking (requires -pid)
|
||||
- `-log-level`: `debug`, `info`, `warn`, or `error` (default: `info`)
|
||||
- `-log-http`: Enable API and web request logs (default: `true`)
|
||||
- `-finished-game-ttl`: How long terminal games stay in memory (default: `1h`; `0` disables eviction)
|
||||
- `-web-api-url`: Browser-visible API origin for the embedded web client; useful when its public origin differs from the listen address
|
||||
|
||||
### Modes
|
||||
```bash
|
||||
# In-memory only (no persistence or auth)
|
||||
./chessd
|
||||
./chess-server
|
||||
|
||||
# With persistence and authentication
|
||||
./chessd -storage-path ./db/chess.db
|
||||
./chess-server -storage-path ./db/chess.db
|
||||
|
||||
# Development with all features
|
||||
./chessd -dev -storage-path chess.db -pid /tmp/chessd.pid -serve
|
||||
./chess-server -dev -storage-path chess.db -pid /tmp/chess-server.pid -serve
|
||||
|
||||
# Detailed persistence, engine-queue, cleanup, and request logs
|
||||
./chess-server -dev -storage-path chess.db -serve -log-level debug -log-http=true
|
||||
|
||||
# Web UI is public at one origin while the API is exposed at another
|
||||
./chess-server -serve -web-api-url https://api.example.test
|
||||
|
||||
# Initialize database with user tables
|
||||
./chessd db init -path chess.db
|
||||
./chess-server db init -path chess.db
|
||||
```
|
||||
|
||||
## Database Management
|
||||
@@ -50,60 +60,64 @@ go build ./cmd/chess-client
|
||||
### Schema Initialization
|
||||
```bash
|
||||
# Create all tables (users, games, moves)
|
||||
./chessd db init -path chess.db
|
||||
./chess-server db init -path chess.db
|
||||
```
|
||||
|
||||
### User Management CLI
|
||||
```bash
|
||||
# Add user with password
|
||||
./chessd db user add -path chess.db -username alice -password SecurePass123
|
||||
./chess-server db user add -path chess.db -username alice -password SecurePass123
|
||||
|
||||
# Add user with email
|
||||
./chessd db user add -path chess.db -username bob -email bob@example.com -password BobPass456
|
||||
./chess-server db user add -path chess.db -username bob -email bob@example.com -password BobPass456
|
||||
|
||||
# Interactive password input
|
||||
./chessd db user add -path chess.db -username charlie -interactive
|
||||
./chess-server db user add -path chess.db -username charlie -interactive
|
||||
|
||||
# List all users
|
||||
./chessd db user list -path chess.db
|
||||
./chess-server db user list -path chess.db
|
||||
|
||||
# Update password
|
||||
./chessd db user set-password -path chess.db -username alice -password NewPass789
|
||||
./chess-server db user set-password -path chess.db -username alice -password NewPass789
|
||||
|
||||
# Update email
|
||||
./chessd db user set-email -path chess.db -username alice -email newemail@example.com
|
||||
./chess-server db user set-email -path chess.db -username alice -email newemail@example.com
|
||||
|
||||
# Update username
|
||||
./chessd db user set-username -path chess.db -current alice -new alice2
|
||||
./chess-server db user set-username -path chess.db -current alice -new alice2
|
||||
|
||||
# Import with existing Argon2 hash
|
||||
./chessd db user set-hash -path chess.db -username alice -hash '$argon2id$v=19$m=65536,t=3,p=2$...'
|
||||
./chess-server db user set-hash -path chess.db -username alice -hash '$argon2id$v=19$m=65536,t=3,p=2$...'
|
||||
|
||||
# Delete user
|
||||
./chessd db user delete -path chess.db -username alice
|
||||
./chess-server db user delete -path chess.db -username alice
|
||||
```
|
||||
|
||||
### Game Query CLI
|
||||
```bash
|
||||
# Query all games
|
||||
./chessd db query -path chess.db -gameId "*"
|
||||
./chess-server db query -path chess.db -gameId "*"
|
||||
|
||||
# Query games for specific user
|
||||
./chessd db query -path chess.db -playerId "550e8400-e29b-41d4-a716-446655440000"
|
||||
./chess-server db query -path chess.db -playerId "550e8400-e29b-41d4-a716-446655440000"
|
||||
|
||||
# Query specific game
|
||||
./chessd db query -path chess.db -gameId "a1b2c3d4-e5f6-7890-1234-567890abcdef"
|
||||
./chess-server db query -path chess.db -gameId "a1b2c3d4-e5f6-7890-1234-567890abcdef"
|
||||
|
||||
# Delete database (destructive)
|
||||
./chessd db delete -path chess.db
|
||||
./chess-server db delete -path chess.db
|
||||
```
|
||||
|
||||
## Authentication Configuration
|
||||
|
||||
### JWT Secret Management
|
||||
- **Production**: Cryptographically secure 32-byte secret generated on startup
|
||||
- **Production**: A cryptographically secure 32-byte secret is generated on
|
||||
startup. This intentionally invalidates JWTs after a restart even though the
|
||||
SQLite session rows remain; configuring a stable deployment secret is tracked
|
||||
in `doc/todo.md`.
|
||||
- **Development** (`-dev`): Fixed secret for testing consistency
|
||||
- **Sessions**: Valid for 7 days, renewed on each login
|
||||
- **Sessions**: Stored for 7 days and renewed on each login; effective token
|
||||
lifetime is also bounded by signing-key rotation
|
||||
|
||||
### Password Requirements
|
||||
- Minimum 8 characters
|
||||
@@ -171,6 +185,12 @@ See [test documentation](../test/README.md) for comprehensive test suites coveri
|
||||
|
||||
# Test real-time game updates via long-polling
|
||||
./test/test-longpoll.sh
|
||||
|
||||
# Unit, migration, and persistence tests
|
||||
go test ./...
|
||||
|
||||
# Concurrency checks for the state/persistence boundary
|
||||
go test -race ./internal/server/storage ./internal/server/service
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -180,10 +200,10 @@ See [test documentation](../test/README.md) for comprehensive test suites coveri
|
||||
- Worker count: 2 (internal/processor/processor.go)
|
||||
- Queue capacity: 100 (internal/processor/queue.go)
|
||||
- Min search time: 100ms (internal/processor/processor.go)
|
||||
- Write queue: 1000 operations (internal/storage/storage.go)
|
||||
- DB connections: 25 max, 5 idle (internal/storage/storage.go)
|
||||
- Write queue: 1000 operations (internal/server/storage/storage.go)
|
||||
- DB connections: 8 max, 4 idle (internal/server/storage/storage.go)
|
||||
- JWT expiration: 7 days (internal/service/user.go)
|
||||
- Long-poll timeout: 25 seconds (internal/service/waiter.go)
|
||||
- Long-poll timeout: 30 seconds (internal/server/service/waiter.go)
|
||||
- Long-poll channel buffer: 1 (internal/service/waiter.go)
|
||||
|
||||
### Authentication Configuration
|
||||
@@ -193,11 +213,13 @@ See [test documentation](../test/README.md) for comprehensive test suites coveri
|
||||
- Hash algorithm: Argon2id (memory-hard, side-channel resistant)
|
||||
|
||||
### Storage Configuration
|
||||
- WAL mode enabled in development for concurrency
|
||||
- Foreign key constraints enforced
|
||||
- Async write pattern for games with 2-second drain on shutdown
|
||||
- WAL mode and NORMAL synchronous mode enabled on every connection
|
||||
- Foreign key constraints and a five-second busy timeout enabled on every connection
|
||||
- Async write pattern for games; shutdown drains every accepted write
|
||||
- Replay reads wait for prior queued writes and use one read transaction
|
||||
- Synchronous writes for user operations (data consistency)
|
||||
- Degradation to memory-only on write failures
|
||||
- Registration capacity/eviction, user creation, and initial session are atomic
|
||||
- A full queue or write failure degrades to memory-only and is visible in logs and `/health`
|
||||
- Case-insensitive collation for usernames and emails
|
||||
|
||||
### Rate Limiting Configuration
|
||||
@@ -249,6 +271,8 @@ See [test documentation](../test/README.md) for comprehensive test suites coveri
|
||||
- No password recovery mechanism
|
||||
- No email verification for registration
|
||||
- Fixed worker pool size for engine calculations
|
||||
- No real-time game updates (polling required)
|
||||
- Long-polling limited to 25 seconds per request
|
||||
- REST API only
|
||||
- No push-based game updates (30-second long-polling is used)
|
||||
- Live games are not rehydrated after restart; persisted games are currently replay-only
|
||||
- Database history has no automatic retention policy
|
||||
- Curated-game metadata and replay controls are deferred to [Replay Implementation Tasks](./todo.md)
|
||||
- REST API only
|
||||
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
# Replay Implementation Tasks
|
||||
|
||||
This plan covers the remaining work needed for first-class replay in the web
|
||||
and CLI clients, durable player-game browsing, and a curated archive of famous
|
||||
games. The persistence/API foundation completed by the database audit is listed
|
||||
first so later work does not duplicate or bypass it.
|
||||
|
||||
## Foundation Available Now
|
||||
|
||||
- [x] Store terminal `result` and `end_time_utc` on each game.
|
||||
- [x] Persist `white_claimed_by` and `black_claimed_by`, including claims made
|
||||
on the first valid move after game creation.
|
||||
- [x] Commit each move, first-move claim, and move-caused terminal result in one
|
||||
SQLite transaction.
|
||||
- [x] Rewind moves and clear a stale terminal result/end time in one transaction.
|
||||
- [x] Return ordered UCI moves with `fenAfterMove` through
|
||||
`GET /api/v1/games/{gameId}/history`.
|
||||
- [x] Return bounded pages through authenticated
|
||||
`GET /api/v1/users/me/games?limit=&offset=`.
|
||||
- [x] Provide matching Go client DTOs and methods (`GetGameHistory`,
|
||||
`GetMyGames`) without prematurely adding CLI presentation.
|
||||
- [x] Evict terminal games from memory after a configurable TTL while retaining
|
||||
durable rows and moves.
|
||||
- [x] Add an async-write barrier and a single SQLite read snapshot for immediate,
|
||||
internally consistent replay reads.
|
||||
- [x] Configure browser API origin through `/config`, with `/chess` fallback for
|
||||
the static deployment at `lixen.com/projects/chess/`.
|
||||
- [x] Add debug-level persistence, cleanup, engine queue, and lifecycle logging.
|
||||
|
||||
## Decisions Required Before Replay UI Work
|
||||
|
||||
| Decision | Current behavior | Decision needed |
|
||||
|---|---|---|
|
||||
| History visibility | Public to anyone with a game UUID, like live game reads | Keep public, make games private by default, or add per-game visibility |
|
||||
| Live mutation authorization | Configure, undo, computer-trigger, and unload remain UUID-based; claimed slots protect human moves only | Define owner/opponent/spectator permissions before replay and archive UI expose more game discovery |
|
||||
| Database retention | Indefinite; only terminal in-memory state is evicted | Retention by account type, archive status, age, or explicit deletion |
|
||||
| Delete semantics | `DELETE /games/{id}` unloads memory but retains history | Name it “close/unload,” or add a separate authorized durable delete |
|
||||
| Durability guarantee | Gameplay continues after a write failure; health becomes degraded | Keep best-effort, acknowledge writes, retry with an outbox, or fail gameplay closed |
|
||||
| Result model | `white_wins`, `black_wins`, `draw`, `stalemate` | Keep compatibility or split outcome (`1-0`, `0-1`, `1/2-1/2`) from termination reason |
|
||||
| Archived-game owner | Not created | Protected system/demo user, separate archive owner table, or ownerless source records |
|
||||
| Replay notation | UCI plus authoritative FEN after each move | Add SAN and canonical PGN at import/write time or derive them on read |
|
||||
|
||||
Record these choices in an ADR before changing the v1 response contract. Until
|
||||
privacy is decided, do not add searchable public player-game indexes or expose
|
||||
usernames in public history.
|
||||
|
||||
## Phase 1 — Complete the Durable Game Model
|
||||
|
||||
### Results and termination
|
||||
|
||||
- [ ] Represent outcome separately from termination reason. Candidate fields:
|
||||
`outcome`, `termination`, and optional `result_detail`.
|
||||
- [ ] Detect and persist all supported draw paths, not only stalemate:
|
||||
insufficient material, repetition, fifty/seventy-five-move rule, and agreed
|
||||
draw if that interaction is added.
|
||||
- [ ] Define behavior for resignation, timeout, abandonment, engine failure,
|
||||
and administrative termination.
|
||||
- [ ] Add constraints covering valid combinations: an end time requires a
|
||||
terminal outcome; an ongoing game has neither.
|
||||
- [ ] Decide whether undoing a finished rated/player game is allowed. If yes,
|
||||
preserve an audit event rather than silently rewriting official history.
|
||||
|
||||
### Stable participant metadata
|
||||
|
||||
- [ ] Snapshot display names at game start/end so replay remains readable after
|
||||
a user rename or temporary-account deletion.
|
||||
- [ ] Separate historical participant identity from mutable controller config.
|
||||
Changing a human slot to a computer must never remove the user's game link.
|
||||
- [ ] Decide whether anonymous players receive a durable pseudonym, remain
|
||||
unnamed, or are excluded from archive browsing.
|
||||
- [ ] Add optional clocks/time-control metadata before timeout results are
|
||||
supported.
|
||||
|
||||
### Notation and integrity
|
||||
|
||||
- [ ] Add SAN per ply and canonical PGN, or add a deterministic backend
|
||||
converter from the stored initial FEN/UCI line.
|
||||
- [ ] Validate that `move_number`, `player_color`, FEN side-to-move, and the
|
||||
previous position form one legal continuous line.
|
||||
- [ ] Add a stored content hash for import idempotency and corruption checks.
|
||||
- [ ] Add a repair/audit CLI command that reports broken game rows without
|
||||
mutating them; make repair an explicit separate operation.
|
||||
- [ ] Define a schema-migration policy beyond v2, including forward-version
|
||||
rejection, backup instructions, and rollback limitations.
|
||||
|
||||
## Phase 2 — Replay and Library APIs
|
||||
|
||||
### Player games
|
||||
|
||||
- [ ] Add filters to the authenticated list: `status`, `result`, color, opponent
|
||||
type, and date range.
|
||||
- [ ] Replace offset pagination with a stable `(start_time_utc, game_id)` cursor
|
||||
before the table grows large; retain v1 offset parameters during migration.
|
||||
- [ ] Return a compact display label/opponent summary so clients do not recreate
|
||||
association logic.
|
||||
- [ ] Define whether an authenticated user may list a game merely created for
|
||||
their random player ID versus one explicitly claimed by them.
|
||||
- [ ] Add authorization tests for expired/deleted sessions and attempts to list
|
||||
another user's games.
|
||||
|
||||
### Replay payload
|
||||
|
||||
- [ ] Version the history payload before adding annotations, evaluations,
|
||||
comments, variations, clocks, or PGN tags.
|
||||
- [ ] Include a canonical final FEN and normalized outcome/termination fields.
|
||||
- [ ] Decide whether long games return one payload or paged/chunked moves.
|
||||
- [ ] Add `ETag`/`If-None-Match` for immutable finished histories.
|
||||
- [ ] Add a downloadable PGN response with correct `Content-Type` and filename.
|
||||
- [ ] Return an explicit “ongoing/incomplete” marker when history is requested
|
||||
before a terminal result.
|
||||
|
||||
### Live-game restoration
|
||||
|
||||
- [ ] Decide whether a server restart should make unfinished games playable or
|
||||
replay-only.
|
||||
- [ ] If play must resume, load the last persisted FEN, next turn, player config,
|
||||
claims, and move list into memory at startup.
|
||||
- [ ] Mark games interrupted in `pending` state as recoverable `stuck` or
|
||||
`ongoing`; never re-submit an engine task blindly.
|
||||
- [ ] Define reconciliation when the service previously entered degraded mode
|
||||
and memory contains moves absent from SQLite.
|
||||
|
||||
## Phase 3 — Curated Famous-Game Archive
|
||||
|
||||
### Schema and ownership
|
||||
|
||||
- [ ] Add a game origin such as `player`, `curated`, or `imported`.
|
||||
- [ ] Add searchable archive metadata: title, event, site, event date, round,
|
||||
white/black display names, Elo values, ECO/opening, source URL, source license,
|
||||
attribution text, and import timestamp.
|
||||
- [ ] Add publication state, featured flag, and explicit featured rank/order.
|
||||
- [ ] Create a protected demo/system identity only if ownership remains tied to
|
||||
users. It must not consume temporary-user capacity, expire, authenticate, or
|
||||
be evicted/deleted through normal user tools.
|
||||
- [ ] Prefer a separate protected archive owner over credentials embedded in
|
||||
seed scripts.
|
||||
- [ ] Add only indexes backed by actual archive queries; confirm each with
|
||||
`EXPLAIN QUERY PLAN` and a representative data volume.
|
||||
|
||||
### Import pipeline
|
||||
|
||||
- [ ] Add `chess-server db archive import` for one PGN or a directory.
|
||||
- [ ] Parse PGN tags, comments, NAGs, and variations deliberately; document
|
||||
which are preserved and which are discarded in the first version.
|
||||
- [ ] Validate every main-line move from its initial position and generate the
|
||||
authoritative FEN sequence before opening the transaction.
|
||||
- [ ] Import a game and all moves in one transaction.
|
||||
- [ ] Make repeated imports idempotent by source key/content hash.
|
||||
- [ ] Add dry-run, structured error output, per-file summary, and all-or-nothing
|
||||
versus continue-on-error modes.
|
||||
- [ ] Preserve source attribution and verify redistribution rights for every
|
||||
bundled collection.
|
||||
- [ ] Seed a small, reviewed fixture set in tests; keep large archives outside
|
||||
the executable and repository unless licensing and binary size are accepted.
|
||||
|
||||
### Archive API
|
||||
|
||||
- [ ] Add a public, bounded curated list endpoint with stable sorting.
|
||||
- [ ] Add exact filters required by the UI (featured, player name, event, year,
|
||||
ECO); do not expose an unconstrained database query API.
|
||||
- [ ] Reuse the same history representation for player and curated games.
|
||||
- [ ] Cache immutable curated list/history responses and invalidate only on
|
||||
archive administration.
|
||||
|
||||
## Phase 4 — CLI Replay Experience
|
||||
|
||||
- [ ] Add `games`/`games mine` to call `GetMyGames`, show pagination, result,
|
||||
colors, opponent/controller, date, and move count.
|
||||
- [ ] Add `games featured` after the curated endpoint exists.
|
||||
- [ ] Add `replay <gameId>` and allow selection from a prior list result.
|
||||
- [ ] Render the initial FEN before ply 1; never assume the standard start.
|
||||
- [ ] Add next/previous/start/end navigation, move-number jump, and optional
|
||||
autoplay speed.
|
||||
- [ ] Display UCI initially and SAN once the backend contract supplies it.
|
||||
- [ ] Clearly separate replay state from live session state: replay commands
|
||||
must not poll, move, undo, configure, or delete the live game.
|
||||
- [ ] Add `pgn save <path>` after the PGN endpoint is defined.
|
||||
- [ ] Cover empty lists, ongoing histories, custom FEN, malformed/incomplete
|
||||
history, expired auth, server restart, and deleted live-memory state.
|
||||
|
||||
## Phase 5 — Web Replay Experience
|
||||
|
||||
- [ ] Add “My games” for authenticated users and a separate “Classic games”
|
||||
collection available without login.
|
||||
- [ ] Build accessible loading, empty, pagination, and error states.
|
||||
- [ ] Add a replay route/deep link, for example `?replay=<gameId>`, that works
|
||||
beneath `/projects/chess/` and does not assume the API shares that path.
|
||||
- [ ] Initialize from `initialFen`; step by assigning the stored
|
||||
`fenAfterMove`, not by replaying moves through a browser chess engine.
|
||||
- [ ] Add previous/next/start/end buttons, move-list selection, keyboard
|
||||
controls, autoplay speed, pause, and current-ply announcement.
|
||||
- [ ] Disable move, computer-trigger, undo, and player-configuration actions in
|
||||
replay mode.
|
||||
- [ ] Stop live long-polling when replay mode begins and restore it only when a
|
||||
live game is explicitly reopened.
|
||||
- [ ] Show result, termination, players, date/event, source attribution, and
|
||||
custom-start notice.
|
||||
- [ ] Make browser back/forward restore list filters and replay ply.
|
||||
- [ ] Test both embedded `/config` and the deployed `/chess` fallback, including
|
||||
CORS and reverse-proxy headers.
|
||||
- [ ] Add responsive and accessibility checks for board orientation, focus,
|
||||
screen-reader labels, reduced motion, and high contrast.
|
||||
|
||||
## Phase 6 — Durability, Operations, and Scale
|
||||
|
||||
- [ ] Choose and implement the durability contract from the decision table.
|
||||
For acknowledged persistence, return success only after a writer receipt or
|
||||
use a durable outbox with retries and ordering.
|
||||
- [ ] Expose counters/metrics for queue depth, enqueue rejection, write latency,
|
||||
failed transaction, barrier latency, replay read latency, and terminal-memory
|
||||
eviction.
|
||||
- [ ] Add request/game correlation fields to logs without logging JWTs,
|
||||
passwords, or full private payloads.
|
||||
- [ ] Configure a stable production JWT signing key (prefer a secret file or
|
||||
deployment secret) so persisted sessions can survive a server restart;
|
||||
document rotation and invalidation procedures.
|
||||
- [ ] Add a bounded degraded-mode recovery procedure; current behavior requires
|
||||
operator intervention/restart and cannot reconstruct missing writes.
|
||||
- [ ] Benchmark list and history queries with realistic user/archive sizes and
|
||||
verify query plans in CI.
|
||||
- [ ] Set WAL checkpoint and database backup procedures; test online backup and
|
||||
restore with active reads/writes.
|
||||
- [ ] Define database retention separately for anonymous, temporary-user,
|
||||
permanent-user, and curated games.
|
||||
- [ ] Add authorized durable deletion/anonymization if required by the privacy
|
||||
policy, with archive records protected from accidental cascades.
|
||||
|
||||
## Required Test Matrix
|
||||
|
||||
- [ ] Upgrade a production-shaped legacy database to every new schema version
|
||||
and reopen it with foreign keys enabled on multiple pooled connections.
|
||||
- [ ] Read history immediately after create, move, terminal move, slot claim,
|
||||
player reconfiguration, and undo—without sleeps.
|
||||
- [ ] Run concurrent legal moves from one position; exactly one may commit and
|
||||
the loser must receive `GAME_CONFLICT`.
|
||||
- [ ] Submit duplicate computer triggers; only one engine task may run.
|
||||
- [ ] Fill the write queue/fault SQLite and assert degraded health, visible
|
||||
logging, and documented client behavior.
|
||||
- [ ] Shut down with queued writes and prove all accepted writes drain.
|
||||
- [ ] Restart after a finished game and replay the exact FEN sequence/result.
|
||||
- [ ] Evict a terminal game from memory and replay it from SQLite.
|
||||
- [ ] Change a claimed human slot to computer and verify “My games” association
|
||||
remains.
|
||||
- [ ] Verify registration duplicate/session failures roll back account creation
|
||||
and capacity eviction.
|
||||
- [ ] Exercise public/private history rules for anonymous, owner, opponent, and
|
||||
unrelated authenticated clients.
|
||||
- [ ] Validate imported PGNs with promotions, castling, en passant, custom FEN,
|
||||
comments, and every supported result.
|
||||
- [ ] Run Go unit/race tests, HTTP integration scripts, JavaScript syntax/tests,
|
||||
and browser end-to-end replay navigation in CI.
|
||||
|
||||
## Replay Definition of Done
|
||||
|
||||
- A finished player game survives restart, appears once in its owner's list,
|
||||
and replays deterministically from the stored initial FEN to the stored final
|
||||
FEN in both clients.
|
||||
- A curated game is imported idempotently with source attribution, appears in a
|
||||
stable public collection, and uses the same replay path as a player game.
|
||||
- Undo, player reconfiguration, terminal eviction, and concurrent requests
|
||||
cannot produce a stale result, missing claim, duplicate ply, or mixed history
|
||||
snapshot.
|
||||
- Privacy, retention, durable deletion, and degraded-write behavior are
|
||||
documented and enforced consistently by API, storage, web, and CLI layers.
|
||||
- Query plans and benchmarks show no redundant indexes or unbounded list scans
|
||||
at the agreed deployment size.
|
||||
Reference in New Issue
Block a user