Refactor connection indicator to patch with timestamp #11

Merged
ryan merged 1 commits from refactor/patch-connection-indicator into main 2026-03-03 20:32:11 +00:00
5 changed files with 58 additions and 15 deletions
Showing only changes of commit 06b3839c3a - Show all commits

View File

@@ -16,6 +16,7 @@ import (
"github.com/ryanhamamura/games/connect4"
"github.com/ryanhamamura/games/db/repository"
"github.com/ryanhamamura/games/features/c4game/pages"
sharedcomponents "github.com/ryanhamamura/games/features/common/components"
"github.com/ryanhamamura/games/sessions"
)
@@ -119,7 +120,7 @@ func HandleGameEvents(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManag
}
sendPing := func() error {
return sse.PatchSignals([]byte(fmt.Sprintf(`{"lastPing":%d}`, time.Now().UnixMilli())))
return sse.PatchElementTempl(sharedcomponents.ConnectionIndicator(time.Now().UnixMilli()))
}
// Send initial render and ping

View File

@@ -15,10 +15,10 @@ templ GamePage(g *connect4.Game, myColor int, messages []chat.Message, chatCfg c
@layouts.Base("Connect 4") {
<main
class="flex flex-col items-center gap-4 p-4"
data-signals="{chatMsg: '', lastPing: 0}"
data-signals="{chatMsg: ''}"
data-init={ fmt.Sprintf("@get('/games/%s/events',{requestCancellation:'disabled'})", g.ID) }
>
@sharedcomponents.ConnectionIndicator()
@sharedcomponents.ConnectionIndicator(0)
@GameContent(g, myColor, messages, chatCfg)
</main>
}

View File

@@ -1,6 +1,10 @@
package components
import "github.com/starfederation/datastar-go/datastar"
import (
"fmt"
"github.com/starfederation/datastar-go/datastar"
)
templ BackToLobby() {
<a class="link text-sm opacity-70" href="/">&larr; Back</a>
@@ -44,19 +48,56 @@ templ NicknamePrompt(returnPath string) {
</main>
}
func isStale(lastPing int64) bool {
return lastPing == 0
}
// 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() {
// Server patches this with a timestamp; client JS detects staleness.
templ ConnectionIndicator(lastPing int64) {
<div
id="connection-indicator"
class="fixed top-2 right-2 flex items-center gap-1 text-xs text-gray-500"
title="Connection status"
class="fixed top-2 right-2"
data-last-ping={ fmt.Sprintf("%d", lastPing) }
>
<span
class="w-2 h-2 rounded-full transition-colors duration-300"
data-class="{'bg-green-500': Date.now() - $lastPing < 20000, 'bg-red-500': Date.now() - $lastPing >= 20000}"
></span>
<div class="inline-grid *:[grid-area:1/1]">
<div
id="connection-ping"
class={
"status status-sm",
templ.KV("status-success animate-ping", !isStale(lastPing)),
templ.KV("status-error", isStale(lastPing)),
}
></div>
<div
id="connection-dot"
class={
"status status-sm",
templ.KV("status-success", !isStale(lastPing)),
templ.KV("status-error", isStale(lastPing)),
}
></div>
</div>
</div>
@connectionWatcher()
}
script connectionWatcher() {
setInterval(function() {
var el = document.getElementById('connection-indicator');
var dot = document.getElementById('connection-dot');
var ping = document.getElementById('connection-ping');
if (!el || !dot || !ping) return;
var lastPing = parseInt(el.dataset.lastPing, 10) || 0;
var stale = Date.now() - lastPing > 20000;
dot.classList.toggle('status-success', !stale);
dot.classList.toggle('status-error', stale);
ping.classList.toggle('status-success', !stale);
ping.classList.toggle('status-error', stale);
ping.classList.toggle('animate-ping', !stale);
}, 1000);
}
templ GameJoinPrompt(loginURL string, registerURL string, gamePath string) {

View File

@@ -13,6 +13,7 @@ import (
"github.com/ryanhamamura/games/chat"
chatcomponents "github.com/ryanhamamura/games/chat/components"
sharedcomponents "github.com/ryanhamamura/games/features/common/components"
"github.com/ryanhamamura/games/features/snakegame/pages"
"github.com/ryanhamamura/games/sessions"
"github.com/ryanhamamura/games/snake"
@@ -125,7 +126,7 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess
}
sendPing := func() error {
return sse.PatchSignals([]byte(fmt.Sprintf(`{"lastPing":%d}`, time.Now().UnixMilli())))
return sse.PatchElementTempl(sharedcomponents.ConnectionIndicator(time.Now().UnixMilli()))
}
// Send initial render and ping

View File

@@ -32,12 +32,12 @@ templ GamePage(sg *snake.SnakeGame, mySlot int, messages []chat.Message, chatCfg
@layouts.Base("Snake") {
<main
class="snake-wrapper flex flex-col items-center gap-4 p-4"
data-signals={ `{"chatMsg":"","lastPing":0}` }
data-signals={ `{"chatMsg":""}` }
data-init={ fmt.Sprintf("@get('/snake/%s/events',{requestCancellation:'disabled'})", gameID) }
data-on:keydown__throttle.100ms={ keydownScript(gameID) }
tabindex="0"
>
@components.ConnectionIndicator()
@components.ConnectionIndicator(0)
@GameContent(sg, mySlot, messages, chatCfg, gameID)
</main>
}