diff --git a/features/c4game/handlers.go b/features/c4game/handlers.go
index e6f5b52..8c37d01 100644
--- a/features/c4game/handlers.go
+++ b/features/c4game/handlers.go
@@ -118,11 +118,21 @@ func HandleGameEvents(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManag
return sse.PatchElementTempl(pages.GameContent(g, myColor, room.Messages(), chatCfg))
}
- // Send initial render
+ sendPing := func() error {
+ return sse.PatchSignals([]byte(fmt.Sprintf(`{"lastPing":%d}`, time.Now().UnixMilli())))
+ }
+
+ // Send initial render and ping
+ if err := sendPing(); err != nil {
+ return
+ }
if err := patchAll(); err != nil {
return
}
+ heartbeat := time.NewTicker(15 * time.Second)
+ defer heartbeat.Stop()
+
// Subscribe to game state updates
gameCh := make(chan *nats.Msg, 64)
gameSub, err := nc.ChanSubscribe(connect4.GameSubject(gameID), gameCh)
@@ -140,6 +150,10 @@ func HandleGameEvents(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManag
select {
case <-ctx.Done():
return
+ case <-heartbeat.C:
+ if err := sendPing(); err != nil {
+ return
+ }
case <-gameCh:
if err := patchAll(); err != nil {
return
diff --git a/features/c4game/pages/game.templ b/features/c4game/pages/game.templ
index c5d9f11..16204cf 100644
--- a/features/c4game/pages/game.templ
+++ b/features/c4game/pages/game.templ
@@ -15,9 +15,10 @@ templ GamePage(g *connect4.Game, myColor int, messages []chat.Message, chatCfg c
@layouts.Base("Connect 4") {
+ @sharedcomponents.ConnectionIndicator()
@GameContent(g, myColor, messages, chatCfg)
}
diff --git a/features/common/components/shared.templ b/features/common/components/shared.templ
index 5567f93..76d67b6 100644
--- a/features/common/components/shared.templ
+++ b/features/common/components/shared.templ
@@ -44,6 +44,21 @@ templ NicknamePrompt(returnPath string) {
}
+// ConnectionIndicator shows a small dot indicating SSE connection status.
+// It requires a `lastPing` signal (unix ms timestamp) to be set by the server.
+templ ConnectionIndicator() {
+
+
+
+}
+
templ GameJoinPrompt(loginURL string, registerURL string, gamePath string) {
Join Game
diff --git a/features/snakegame/handlers.go b/features/snakegame/handlers.go
index afb6f3a..d5aac7b 100644
--- a/features/snakegame/handlers.go
+++ b/features/snakegame/handlers.go
@@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"strconv"
+ "time"
"github.com/alexedwards/scs/v2"
"github.com/go-chi/chi/v5"
@@ -123,11 +124,21 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
return sse.PatchElementTempl(pages.GameContent(sg, mySlot, chatMessages(), chatCfg, gameID))
}
- // Send initial render
+ sendPing := func() error {
+ return sse.PatchSignals([]byte(fmt.Sprintf(`{"lastPing":%d}`, time.Now().UnixMilli())))
+ }
+
+ // Send initial render and ping
+ if err := sendPing(); err != nil {
+ return
+ }
if err := patchAll(); err != nil {
return
}
+ heartbeat := time.NewTicker(15 * time.Second)
+ defer heartbeat.Stop()
+
// Subscribe to game updates via NATS
gameCh := make(chan *nats.Msg, 64)
gameSub, err := nc.ChanSubscribe(snake.GameSubject(gameID), gameCh)
@@ -151,6 +162,11 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
case <-ctx.Done():
return
+ case <-heartbeat.C:
+ if err := sendPing(); err != nil {
+ return
+ }
+
case <-gameCh:
// Drain backed-up game updates
for {
diff --git a/features/snakegame/pages/game.templ b/features/snakegame/pages/game.templ
index 378690c..f8fe244 100644
--- a/features/snakegame/pages/game.templ
+++ b/features/snakegame/pages/game.templ
@@ -32,11 +32,12 @@ templ GamePage(sg *snake.SnakeGame, mySlot int, messages []chat.Message, chatCfg
@layouts.Base("Snake") {
+ @components.ConnectionIndicator()
@GameContent(sg, mySlot, messages, chatCfg, gameID)
}