refactor: deduplicate persistence, add upsert queries, throttle snake saves
- Replace Create+Get+Update with UpsertGame/UpsertSnakeGame queries - Extract free functions (saveGame, loadGame, etc.) from duplicated receiver methods on Store and Instance types - Remove duplicate generateID from snake package, reuse game.GenerateID - Throttle snake game DB writes to every 2s instead of every tick - Fix double-lock in c4game chat handler - Update all code for sqlc pointer types (*string instead of sql.NullString)
This commit is contained in:
150
game/persist.go
150
game/persist.go
@@ -2,80 +2,43 @@ package game
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
)
|
||||
|
||||
// Persistence methods on GameStore (used during Get to hydrate from DB).
|
||||
|
||||
func (gs *GameStore) saveGame(g *Game) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := gs.queries.GetGame(ctx, g.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = gs.queries.CreateGame(ctx, repository.CreateGameParams{
|
||||
ID: g.ID,
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
// saveGame persists the game state via upsert.
|
||||
func saveGame(queries *repository.Queries, g *Game) error {
|
||||
var winnerUserID *string
|
||||
if g.Winner != nil && g.Winner.UserID != nil {
|
||||
winnerUserID = g.Winner.UserID
|
||||
}
|
||||
|
||||
return gs.queries.UpdateGame(ctx, updateGameParams(g))
|
||||
var winningCells *string
|
||||
if wc := g.WinningCellsToJSON(); wc != "" {
|
||||
winningCells = &wc
|
||||
}
|
||||
|
||||
return queries.UpsertGame(context.Background(), repository.UpsertGameParams{
|
||||
ID: g.ID,
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
WinningCells: winningCells,
|
||||
RematchGameID: g.RematchGameID,
|
||||
})
|
||||
}
|
||||
|
||||
func (gs *GameStore) loadGame(id string) (*Game, error) {
|
||||
row, err := gs.queries.GetGame(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gameFromRow(row)
|
||||
}
|
||||
|
||||
func (gs *GameStore) loadGamePlayers(id string) ([]*Player, error) {
|
||||
rows, err := gs.queries.GetGamePlayers(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return playersFromRows(rows), nil
|
||||
}
|
||||
|
||||
// Persistence methods on GameInstance (used during gameplay mutations).
|
||||
|
||||
func (gi *GameInstance) saveGame(g *Game) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := gi.queries.GetGame(ctx, g.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = gi.queries.CreateGame(ctx, repository.CreateGameParams{
|
||||
ID: g.ID,
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gi.queries.UpdateGame(ctx, updateGameParams(g))
|
||||
}
|
||||
|
||||
func (gi *GameInstance) saveGamePlayer(gameID string, player *Player, slot int) error {
|
||||
var userID, guestPlayerID sql.NullString
|
||||
func saveGamePlayer(queries *repository.Queries, gameID string, player *Player, slot int) error {
|
||||
var userID, guestPlayerID *string
|
||||
if player.UserID != nil {
|
||||
userID = sql.NullString{String: *player.UserID, Valid: true}
|
||||
userID = player.UserID
|
||||
} else {
|
||||
guestPlayerID = sql.NullString{String: string(player.ID), Valid: true}
|
||||
id := string(player.ID)
|
||||
guestPlayerID = &id
|
||||
}
|
||||
|
||||
return gi.queries.CreateGamePlayer(context.Background(), repository.CreateGamePlayerParams{
|
||||
return queries.CreateGamePlayer(context.Background(), repository.CreateGamePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
@@ -85,36 +48,25 @@ func (gi *GameInstance) saveGamePlayer(gameID string, player *Player, slot int)
|
||||
})
|
||||
}
|
||||
|
||||
// Shared helpers for domain ↔ DB mapping.
|
||||
|
||||
func updateGameParams(g *Game) repository.UpdateGameParams {
|
||||
var winnerUserID sql.NullString
|
||||
if g.Winner != nil && g.Winner.UserID != nil {
|
||||
winnerUserID = sql.NullString{String: *g.Winner.UserID, Valid: true}
|
||||
}
|
||||
|
||||
var winningCells sql.NullString
|
||||
if wc := g.WinningCellsToJSON(); wc != "" {
|
||||
winningCells = sql.NullString{String: wc, Valid: true}
|
||||
}
|
||||
|
||||
var rematchGameID sql.NullString
|
||||
if g.RematchGameID != nil {
|
||||
rematchGameID = sql.NullString{String: *g.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return repository.UpdateGameParams{
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
WinningCells: winningCells,
|
||||
RematchGameID: rematchGameID,
|
||||
ID: g.ID,
|
||||
func loadGame(queries *repository.Queries, id string) (*Game, error) {
|
||||
row, err := queries.GetGame(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gameFromRow(row)
|
||||
}
|
||||
|
||||
func gameFromRow(row repository.Game) (*Game, error) {
|
||||
func loadGamePlayers(queries *repository.Queries, id string) ([]*Player, error) {
|
||||
rows, err := queries.GetGamePlayers(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return playersFromRows(rows), nil
|
||||
}
|
||||
|
||||
// Domain ↔ DB mapping helpers.
|
||||
|
||||
func gameFromRow(row *repository.Game) (*Game, error) {
|
||||
g := &Game{
|
||||
ID: row.ID,
|
||||
CurrentTurn: int(row.CurrentTurn),
|
||||
@@ -125,18 +77,18 @@ func gameFromRow(row repository.Game) (*Game, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if row.WinningCells.Valid {
|
||||
_ = g.WinningCellsFromJSON(row.WinningCells.String)
|
||||
if row.WinningCells != nil {
|
||||
_ = g.WinningCellsFromJSON(*row.WinningCells)
|
||||
}
|
||||
|
||||
if row.RematchGameID.Valid {
|
||||
g.RematchGameID = &row.RematchGameID.String
|
||||
if row.RematchGameID != nil {
|
||||
g.RematchGameID = row.RematchGameID
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func playersFromRows(rows []repository.GamePlayer) []*Player {
|
||||
func playersFromRows(rows []*repository.GamePlayer) []*Player {
|
||||
players := make([]*Player, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
player := &Player{
|
||||
@@ -144,11 +96,11 @@ func playersFromRows(rows []repository.GamePlayer) []*Player {
|
||||
Color: int(row.Color),
|
||||
}
|
||||
|
||||
if row.UserID.Valid {
|
||||
player.UserID = &row.UserID.String
|
||||
player.ID = PlayerID(row.UserID.String)
|
||||
} else if row.GuestPlayerID.Valid {
|
||||
player.ID = PlayerID(row.GuestPlayerID.String)
|
||||
if row.UserID != nil {
|
||||
player.UserID = row.UserID
|
||||
player.ID = PlayerID(*row.UserID)
|
||||
} else if row.GuestPlayerID != nil {
|
||||
player.ID = PlayerID(*row.GuestPlayerID)
|
||||
}
|
||||
|
||||
players = append(players, player)
|
||||
|
||||
Reference in New Issue
Block a user