refactor: patch entire game content for snake SSE handler
Some checks failed
CI / Deploy / test (pull_request) Successful in 16s
CI / Deploy / lint (pull_request) Failing after 20s
CI / Deploy / deploy (pull_request) Has been skipped

Same approach as connect4 — extract GameContent component and patch it
as a single element, letting DOM morphing handle the diff.
This commit is contained in:
Ryan Hamamura
2026-03-02 22:34:20 -10:00
parent 0808c4d972
commit 4faf4f73b0
2 changed files with 53 additions and 48 deletions

View File

@@ -12,7 +12,6 @@ import (
"github.com/ryanhamamura/games/chat"
chatcomponents "github.com/ryanhamamura/games/chat/components"
"github.com/ryanhamamura/games/features/snakegame/components"
"github.com/ryanhamamura/games/features/snakegame/pages"
"github.com/ryanhamamura/games/sessions"
"github.com/ryanhamamura/games/snake"
@@ -100,16 +99,33 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
chatCfg := snakeChatConfig(gameID)
// Send initial render
// Chat room (multiplayer only)
var room *chat.Room
sg := si.GetGame()
sse.PatchElementTempl(components.Board(sg)) //nolint:errcheck
sse.PatchElementTempl(components.StatusBanner(sg, mySlot, gameID)) //nolint:errcheck
sse.PatchElementTempl(components.PlayerList(sg, mySlot)) //nolint:errcheck
if sg.Mode == snake.ModeMultiplayer {
sse.PatchElementTempl(chatcomponents.Chat(nil, chatCfg)) //nolint:errcheck
if sg.Status == snake.StatusWaitingForPlayers || sg.Status == snake.StatusCountdown {
sse.PatchElementTempl(components.InviteLink(gameID)) //nolint:errcheck
room = chat.NewRoom(nc, snake.ChatSubject(gameID))
}
chatMessages := func() []chat.Message {
if room == nil {
return nil
}
return room.Messages()
}
patchAll := func() error {
si, ok = snakeStore.Get(gameID)
if !ok {
return fmt.Errorf("game not found")
}
mySlot = si.GetPlayerSlot(playerID)
sg = si.GetGame()
return sse.PatchElementTempl(pages.GameContent(sg, mySlot, chatMessages(), chatCfg, gameID))
}
// Send initial render
if err := patchAll(); err != nil {
return
}
// Subscribe to game updates via NATS
@@ -123,10 +139,8 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
// Chat subscription (multiplayer only)
var chatCh chan *nats.Msg
var chatSub *nats.Subscription
var room *chat.Room
if sg.Mode == snake.ModeMultiplayer {
room = chat.NewRoom(nc, snake.ChatSubject(gameID))
if room != nil {
chatCh, chatSub, err = room.Subscribe()
if err != nil {
return
@@ -150,19 +164,7 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
}
}
drained:
si, ok = snakeStore.Get(gameID)
if !ok {
return
}
mySlot = si.GetPlayerSlot(playerID)
sg = si.GetGame()
if err := sse.PatchElementTempl(components.Board(sg)); err != nil {
return
}
if err := sse.PatchElementTempl(components.StatusBanner(sg, mySlot, gameID)); err != nil {
return
}
if err := sse.PatchElementTempl(components.PlayerList(sg, mySlot)); err != nil {
if err := patchAll(); err != nil {
return
}
@@ -170,11 +172,8 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
if msg == nil {
continue
}
_, snapshot := room.Receive(msg.Data)
if snapshot == nil {
continue
}
if err := sse.PatchElementTempl(chatcomponents.Chat(snapshot, chatCfg)); err != nil {
room.Receive(msg.Data)
if err := patchAll(); err != nil {
return
}
}

View File

@@ -37,29 +37,35 @@ templ GamePage(sg *snake.SnakeGame, mySlot int, messages []chat.Message, chatCfg
data-on:keydown.throttle_100ms={ keydownScript(gameID) }
tabindex="0"
>
@components.BackToLobby()
<h1 class="text-3xl font-bold">~~~~</h1>
@snakecomponents.PlayerList(sg, mySlot)
@snakecomponents.StatusBanner(sg, mySlot, gameID)
if sg.Status == snake.StatusInProgress || sg.Status == snake.StatusFinished {
if sg.Mode == snake.ModeMultiplayer {
<div class="snake-game-area">
@snakecomponents.Board(sg)
@chatcomponents.Chat(messages, chatCfg)
</div>
} else {
@snakecomponents.Board(sg)
}
} else if sg.Mode == snake.ModeMultiplayer {
@chatcomponents.Chat(messages, chatCfg)
}
if sg.Mode == snake.ModeMultiplayer && (sg.Status == snake.StatusWaitingForPlayers || sg.Status == snake.StatusCountdown) {
@snakecomponents.InviteLink(gameID)
}
@GameContent(sg, mySlot, messages, chatCfg, gameID)
</main>
}
}
templ GameContent(sg *snake.SnakeGame, mySlot int, messages []chat.Message, chatCfg chatcomponents.Config, gameID string) {
<div id="game-content">
@components.BackToLobby()
<h1 class="text-3xl font-bold">~~~~</h1>
@snakecomponents.PlayerList(sg, mySlot)
@snakecomponents.StatusBanner(sg, mySlot, gameID)
if sg.Status == snake.StatusInProgress || sg.Status == snake.StatusFinished {
if sg.Mode == snake.ModeMultiplayer {
<div class="snake-game-area">
@snakecomponents.Board(sg)
@chatcomponents.Chat(messages, chatCfg)
</div>
} else {
@snakecomponents.Board(sg)
}
} else if sg.Mode == snake.ModeMultiplayer {
@chatcomponents.Chat(messages, chatCfg)
}
if sg.Mode == snake.ModeMultiplayer && (sg.Status == snake.StatusWaitingForPlayers || sg.Status == snake.StatusCountdown) {
@snakecomponents.InviteLink(gameID)
}
</div>
}
templ JoinPage(gameID string) {
@layouts.Base("Snake - Join") {
@components.GameJoinPrompt(