v0.11.0 harden persistence and prepare game replays
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"chess/internal/server/core"
|
||||
"chess/internal/server/storage"
|
||||
)
|
||||
|
||||
// GetGameHistory returns a durable replay even after the live game has been
|
||||
// evicted from memory or the server has restarted.
|
||||
func (s *Service) GetGameHistory(gameID string) (*core.GameHistoryResponse, error) {
|
||||
if s.store == nil {
|
||||
return nil, ErrStorageDisabled
|
||||
}
|
||||
record, moves, err := s.store.GetGameHistory(gameID)
|
||||
if err != nil {
|
||||
if storage.IsGameNotFound(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrGameNotFound, gameID)
|
||||
}
|
||||
if isStorageUnavailable(err) {
|
||||
return nil, fmt.Errorf("%w: %v", ErrStorageUnavailable, err)
|
||||
}
|
||||
return nil, fmt.Errorf("get game history: %w", err)
|
||||
}
|
||||
|
||||
history := &core.GameHistoryResponse{
|
||||
GameID: record.GameID,
|
||||
InitialFEN: record.InitialFEN,
|
||||
Result: record.Result,
|
||||
StartTimeUTC: record.StartTimeUTC,
|
||||
EndTimeUTC: record.EndTimeUTC,
|
||||
Players: playersResponse(*record),
|
||||
Moves: make([]core.HistoryMove, 0, len(moves)),
|
||||
}
|
||||
for _, move := range moves {
|
||||
history.Moves = append(history.Moves, core.HistoryMove{
|
||||
MoveNumber: move.MoveNumber,
|
||||
MoveUCI: move.MoveUCI,
|
||||
FENAfterMove: move.FENAfterMove,
|
||||
PlayerColor: move.PlayerColor,
|
||||
MoveTimeUTC: move.MoveTimeUTC,
|
||||
})
|
||||
}
|
||||
return history, nil
|
||||
}
|
||||
|
||||
// GetUserGames returns a bounded page of games associated either at creation
|
||||
// or by a later slot claim.
|
||||
func (s *Service) GetUserGames(userID string, limit, offset int) (*core.GameListResponse, error) {
|
||||
if s.store == nil {
|
||||
return nil, ErrStorageDisabled
|
||||
}
|
||||
records, err := s.store.QueryGamesForUser(userID, limit+1, offset)
|
||||
if err != nil {
|
||||
if isStorageUnavailable(err) {
|
||||
return nil, fmt.Errorf("%w: %v", ErrStorageUnavailable, err)
|
||||
}
|
||||
return nil, fmt.Errorf("get user games: %w", err)
|
||||
}
|
||||
|
||||
hasNext := len(records) > limit
|
||||
if hasNext {
|
||||
records = records[:limit]
|
||||
}
|
||||
response := &core.GameListResponse{
|
||||
Games: make([]core.GameSummary, 0, len(records)),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
if hasNext {
|
||||
next := offset + limit
|
||||
response.NextOffset = &next
|
||||
}
|
||||
for _, record := range records {
|
||||
response.Games = append(response.Games, core.GameSummary{
|
||||
GameID: record.GameID,
|
||||
InitialFEN: record.InitialFEN,
|
||||
Result: record.Result,
|
||||
StartTimeUTC: record.StartTimeUTC,
|
||||
EndTimeUTC: record.EndTimeUTC,
|
||||
MoveCount: record.MoveCount,
|
||||
Players: playersResponse(record.GameRecord),
|
||||
})
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func isStorageUnavailable(err error) bool {
|
||||
return errors.Is(err, storage.ErrStorageDegraded) ||
|
||||
errors.Is(err, storage.ErrStoreClosed) ||
|
||||
errors.Is(err, storage.ErrWriteQueueFull) ||
|
||||
errors.Is(err, context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
func playersResponse(record storage.GameRecord) core.PlayersResponse {
|
||||
return core.PlayersResponse{
|
||||
White: &core.Player{
|
||||
ID: record.WhitePlayerID, Color: core.ColorWhite, Type: core.PlayerType(record.WhiteType),
|
||||
Level: record.WhiteLevel, SearchTime: record.WhiteSearchTime, ClaimedBy: record.WhiteClaimedBy,
|
||||
},
|
||||
Black: &core.Player{
|
||||
ID: record.BlackPlayerID, Color: core.ColorBlack, Type: core.PlayerType(record.BlackType),
|
||||
Level: record.BlackLevel, SearchTime: record.BlackSearchTime, ClaimedBy: record.BlackClaimedBy,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user