refactor: remove persister abstraction layer
Some checks failed
CI / Deploy / test (pull_request) Successful in 8s
CI / Deploy / lint (pull_request) Failing after 46s
CI / Deploy / deploy (pull_request) Has been skipped

Inline persistence logic directly into game stores and handlers:
- game/persist.go: DB mapping methods on GameStore and GameInstance
- snake/persist.go: DB mapping methods on SnakeStore and SnakeGameInstance
- Chat persistence inlined into c4game handlers
- Delete db/persister.go (GamePersister, SnakePersister, ChatPersister)
- Stores now take *repository.Queries directly instead of Persister interface
This commit is contained in:
Ryan Hamamura
2026-03-02 12:30:33 -10:00
parent 8c3b3fc6ea
commit 2aa026b1d5
10 changed files with 475 additions and 448 deletions

View File

@@ -61,16 +61,16 @@ func (si *SnakeGameInstance) countdownPhase() {
si.initGame()
si.game.Status = StatusInProgress
if si.persister != nil {
si.persister.SaveSnakeGame(si.game)
if si.queries != nil {
si.saveSnakeGame(si.game)
}
si.gameMu.Unlock()
si.notify()
return
}
if si.persister != nil {
si.persister.SaveSnakeGame(si.game)
if si.queries != nil {
si.saveSnakeGame(si.game)
}
si.gameMu.Unlock()
si.notify()
@@ -123,8 +123,8 @@ func (si *SnakeGameInstance) gamePhase() {
// Inactivity timeout
if time.Since(lastInput) > inactivityLimit {
si.game.Status = StatusFinished
if si.persister != nil {
si.persister.SaveSnakeGame(si.game)
if si.queries != nil {
si.saveSnakeGame(si.game)
}
si.gameMu.Unlock()
si.notify()
@@ -195,8 +195,8 @@ func (si *SnakeGameInstance) gamePhase() {
si.game.Status = StatusFinished
}
if si.persister != nil {
si.persister.SaveSnakeGame(si.game)
if si.queries != nil {
si.saveSnakeGame(si.game)
}
si.gameMu.Unlock()

186
snake/persist.go Normal file
View File

@@ -0,0 +1,186 @@
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()
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 := 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))
}
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
if player.UserID != nil {
userID = sql.NullString{String: *player.UserID, Valid: true}
} else {
guestPlayerID = sql.NullString{String: string(player.ID), Valid: true}
}
return si.queries.CreateSnakePlayer(context.Background(), repository.CreateSnakePlayerParams{
GameID: gameID,
UserID: userID,
GuestPlayerID: guestPlayerID,
Nickname: player.Nickname,
Color: int64(player.Slot + 1),
Slot: int64(player.Slot),
})
}
// 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 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.GridHeight.Valid {
state.Height = int(row.GridHeight.Int64)
}
sg := &SnakeGame{
ID: row.ID,
State: state,
Players: make([]*Player, 8),
Status: Status(row.Status),
Mode: GameMode(row.GameMode),
Score: int(row.Score),
Speed: int(row.SnakeSpeed),
}
if row.RematchGameID.Valid {
sg.RematchGameID = &row.RematchGameID.String
}
return sg, nil
}
func snakePlayersFromRows(rows []repository.GamePlayer) []*Player {
players := make([]*Player, 0, len(rows))
for _, row := range rows {
player := &Player{
Nickname: row.Nickname,
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)
}
players = append(players, player)
}
return players
}

View File

@@ -1,36 +1,28 @@
package snake
import (
"context"
"crypto/rand"
"encoding/hex"
"sync"
"github.com/ryanhamamura/c4/db/repository"
)
type Persister interface {
SaveSnakeGame(sg *SnakeGame) error
LoadSnakeGame(id string) (*SnakeGame, error)
SaveSnakePlayer(gameID string, player *Player) error
LoadSnakePlayers(gameID string) ([]*Player, error)
DeleteSnakeGame(id string) error
}
type SnakeStore struct {
games map[string]*SnakeGameInstance
gamesMu sync.RWMutex
persister Persister
games map[string]*SnakeGameInstance
gamesMu sync.RWMutex
queries *repository.Queries
notifyFunc func(gameID string)
}
func NewSnakeStore() *SnakeStore {
func NewSnakeStore(queries *repository.Queries) *SnakeStore {
return &SnakeStore{
games: make(map[string]*SnakeGameInstance),
games: make(map[string]*SnakeGameInstance),
queries: queries,
}
}
func (ss *SnakeStore) SetPersister(p Persister) {
ss.persister = p
}
func (ss *SnakeStore) SetNotifyFunc(f func(gameID string)) {
ss.notifyFunc = f
}
@@ -60,18 +52,18 @@ func (ss *SnakeStore) Create(width, height int, mode GameMode, speed int) *Snake
Speed: speed,
}
si := &SnakeGameInstance{
game: sg,
notify: ss.makeNotify(id),
persister: ss.persister,
store: ss,
game: sg,
notify: ss.makeNotify(id),
queries: ss.queries,
store: ss,
}
ss.gamesMu.Lock()
ss.games[id] = si
ss.gamesMu.Unlock()
if ss.persister != nil {
ss.persister.SaveSnakeGame(sg)
if ss.queries != nil {
ss.saveSnakeGame(sg)
}
return si
@@ -86,16 +78,16 @@ func (ss *SnakeStore) Get(id string) (*SnakeGameInstance, bool) {
return si, true
}
if ss.persister == nil {
if ss.queries == nil {
return nil, false
}
sg, err := ss.persister.LoadSnakeGame(id)
sg, err := ss.loadSnakeGame(id)
if err != nil || sg == nil {
return nil, false
}
players, _ := ss.persister.LoadSnakePlayers(id)
players, _ := ss.loadSnakePlayers(id)
if sg.Players == nil {
sg.Players = make([]*Player, 8)
}
@@ -106,10 +98,10 @@ func (ss *SnakeStore) Get(id string) (*SnakeGameInstance, bool) {
}
si = &SnakeGameInstance{
game: sg,
notify: ss.makeNotify(id),
persister: ss.persister,
store: ss,
game: sg,
notify: ss.makeNotify(id),
queries: ss.queries,
store: ss,
}
ss.gamesMu.Lock()
@@ -129,8 +121,8 @@ func (ss *SnakeStore) Delete(id string) error {
si.Stop()
}
if ss.persister != nil {
return ss.persister.DeleteSnakeGame(id)
if ss.queries != nil {
return ss.queries.DeleteSnakeGame(context.Background(), id)
}
return nil
}
@@ -158,14 +150,14 @@ func (ss *SnakeStore) ActiveGames() []*SnakeGame {
}
type SnakeGameInstance struct {
game *SnakeGame
gameMu sync.RWMutex
game *SnakeGame
gameMu sync.RWMutex
pendingDirQueue [8][]Direction // queued directions per slot (max 3)
notify func()
persister Persister
store *SnakeStore
stopCh chan struct{}
loopOnce sync.Once
notify func()
queries *repository.Queries
store *SnakeStore
stopCh chan struct{}
loopOnce sync.Once
}
func (si *SnakeGameInstance) ID() string {
@@ -214,9 +206,9 @@ func (si *SnakeGameInstance) Join(player *Player) bool {
player.Slot = slot
si.game.Players[slot] = player
if si.persister != nil {
si.persister.SaveSnakePlayer(si.game.ID, player)
si.persister.SaveSnakeGame(si.game)
if si.queries != nil {
si.saveSnakePlayer(si.game.ID, player)
si.saveSnakeGame(si.game)
}
si.notify()
@@ -301,8 +293,8 @@ func (si *SnakeGameInstance) CreateRematch() *SnakeGameInstance {
}
si.game.RematchGameID = &newID
if si.persister != nil {
si.persister.SaveSnakeGame(si.game)
if si.queries != nil {
si.saveSnakeGame(si.game)
}
si.gameMu.Unlock()