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:
172
snake/persist.go
172
snake/persist.go
@@ -2,108 +2,49 @@ package snake
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
)
|
||||
|
||||
// Persistence methods on SnakeStore (used during Get to hydrate from DB).
|
||||
|
||||
func (ss *SnakeStore) saveSnakeGame(sg *SnakeGame) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// saveSnakeGame persists the snake game state via upsert.
|
||||
func saveSnakeGame(queries *repository.Queries, sg *SnakeGame) error {
|
||||
boardJSON := "{}"
|
||||
var gridWidth, gridHeight *int64
|
||||
if sg.State != nil {
|
||||
boardJSON = sg.State.ToJSON()
|
||||
w, h := int64(sg.State.Width), int64(sg.State.Height)
|
||||
gridWidth, gridHeight = &w, &h
|
||||
}
|
||||
|
||||
var gridWidth, gridHeight sql.NullInt64
|
||||
if sg.State != nil {
|
||||
gridWidth = sql.NullInt64{Int64: int64(sg.State.Width), Valid: true}
|
||||
gridHeight = sql.NullInt64{Int64: int64(sg.State.Height), Valid: true}
|
||||
var winnerUserID *string
|
||||
if sg.Winner != nil && sg.Winner.UserID != nil {
|
||||
winnerUserID = sg.Winner.UserID
|
||||
}
|
||||
|
||||
_, err := ss.queries.GetSnakeGame(ctx, sg.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = ss.queries.CreateSnakeGame(ctx, repository.CreateSnakeGameParams{
|
||||
ID: sg.ID,
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
GridWidth: gridWidth,
|
||||
GridHeight: gridHeight,
|
||||
GameMode: int64(sg.Mode),
|
||||
SnakeSpeed: int64(sg.Speed),
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ss.queries.UpdateSnakeGame(ctx, updateSnakeGameParams(sg, boardJSON))
|
||||
return queries.UpsertSnakeGame(context.Background(), repository.UpsertSnakeGameParams{
|
||||
ID: sg.ID,
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
GridWidth: gridWidth,
|
||||
GridHeight: gridHeight,
|
||||
GameMode: int64(sg.Mode),
|
||||
SnakeSpeed: int64(sg.Speed),
|
||||
WinnerUserID: winnerUserID,
|
||||
RematchGameID: sg.RematchGameID,
|
||||
Score: int64(sg.Score),
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *SnakeStore) loadSnakeGame(id string) (*SnakeGame, error) {
|
||||
row, err := ss.queries.GetSnakeGame(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snakeGameFromRow(row)
|
||||
}
|
||||
|
||||
func (ss *SnakeStore) loadSnakePlayers(id string) ([]*Player, error) {
|
||||
rows, err := ss.queries.GetSnakePlayers(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snakePlayersFromRows(rows), nil
|
||||
}
|
||||
|
||||
// Persistence methods on SnakeGameInstance (used during gameplay mutations).
|
||||
|
||||
func (si *SnakeGameInstance) saveSnakeGame(sg *SnakeGame) error {
|
||||
ctx := context.Background()
|
||||
|
||||
boardJSON := "{}"
|
||||
if sg.State != nil {
|
||||
boardJSON = sg.State.ToJSON()
|
||||
}
|
||||
|
||||
var gridWidth, gridHeight sql.NullInt64
|
||||
if sg.State != nil {
|
||||
gridWidth = sql.NullInt64{Int64: int64(sg.State.Width), Valid: true}
|
||||
gridHeight = sql.NullInt64{Int64: int64(sg.State.Height), Valid: true}
|
||||
}
|
||||
|
||||
_, err := si.queries.GetSnakeGame(ctx, sg.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = si.queries.CreateSnakeGame(ctx, repository.CreateSnakeGameParams{
|
||||
ID: sg.ID,
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
GridWidth: gridWidth,
|
||||
GridHeight: gridHeight,
|
||||
GameMode: int64(sg.Mode),
|
||||
SnakeSpeed: int64(sg.Speed),
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return si.queries.UpdateSnakeGame(ctx, updateSnakeGameParams(sg, boardJSON))
|
||||
}
|
||||
|
||||
func (si *SnakeGameInstance) saveSnakePlayer(gameID string, player *Player) error {
|
||||
var userID, guestPlayerID sql.NullString
|
||||
func saveSnakePlayer(queries *repository.Queries, gameID string, player *Player) 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 si.queries.CreateSnakePlayer(context.Background(), repository.CreateSnakePlayerParams{
|
||||
return queries.CreateSnakePlayer(context.Background(), repository.CreateSnakePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
@@ -113,39 +54,34 @@ func (si *SnakeGameInstance) saveSnakePlayer(gameID string, player *Player) erro
|
||||
})
|
||||
}
|
||||
|
||||
// Shared helpers for domain ↔ DB mapping.
|
||||
|
||||
func updateSnakeGameParams(sg *SnakeGame, boardJSON string) repository.UpdateSnakeGameParams {
|
||||
var winnerUserID sql.NullString
|
||||
if sg.Winner != nil && sg.Winner.UserID != nil {
|
||||
winnerUserID = sql.NullString{String: *sg.Winner.UserID, Valid: true}
|
||||
}
|
||||
|
||||
var rematchGameID sql.NullString
|
||||
if sg.RematchGameID != nil {
|
||||
rematchGameID = sql.NullString{String: *sg.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return repository.UpdateSnakeGameParams{
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
RematchGameID: rematchGameID,
|
||||
Score: int64(sg.Score),
|
||||
ID: sg.ID,
|
||||
func loadSnakeGame(queries *repository.Queries, id string) (*SnakeGame, error) {
|
||||
row, err := queries.GetSnakeGame(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snakeGameFromRow(row)
|
||||
}
|
||||
|
||||
func snakeGameFromRow(row repository.Game) (*SnakeGame, error) {
|
||||
func loadSnakePlayers(queries *repository.Queries, id string) ([]*Player, error) {
|
||||
rows, err := queries.GetSnakePlayers(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snakePlayersFromRows(rows), nil
|
||||
}
|
||||
|
||||
// Domain ↔ DB mapping helpers.
|
||||
|
||||
func snakeGameFromRow(row *repository.Game) (*SnakeGame, error) {
|
||||
state, err := GameStateFromJSON(row.Board)
|
||||
if err != nil {
|
||||
state = &GameState{}
|
||||
}
|
||||
if row.GridWidth.Valid {
|
||||
state.Width = int(row.GridWidth.Int64)
|
||||
if row.GridWidth != nil {
|
||||
state.Width = int(*row.GridWidth)
|
||||
}
|
||||
if row.GridHeight.Valid {
|
||||
state.Height = int(row.GridHeight.Int64)
|
||||
if row.GridHeight != nil {
|
||||
state.Height = int(*row.GridHeight)
|
||||
}
|
||||
|
||||
sg := &SnakeGame{
|
||||
@@ -158,14 +94,14 @@ func snakeGameFromRow(row repository.Game) (*SnakeGame, error) {
|
||||
Speed: int(row.SnakeSpeed),
|
||||
}
|
||||
|
||||
if row.RematchGameID.Valid {
|
||||
sg.RematchGameID = &row.RematchGameID.String
|
||||
if row.RematchGameID != nil {
|
||||
sg.RematchGameID = row.RematchGameID
|
||||
}
|
||||
|
||||
return sg, nil
|
||||
}
|
||||
|
||||
func snakePlayersFromRows(rows []repository.GamePlayer) []*Player {
|
||||
func snakePlayersFromRows(rows []*repository.GamePlayer) []*Player {
|
||||
players := make([]*Player, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
player := &Player{
|
||||
@@ -173,11 +109,11 @@ func snakePlayersFromRows(rows []repository.GamePlayer) []*Player {
|
||||
Slot: int(row.Slot),
|
||||
}
|
||||
|
||||
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