Compare commits

..

4 Commits

Author SHA1 Message Date
Grail Finder
817d69c425 Enha: sse update 2025-08-03 16:13:51 +03:00
Grail Finder
acc3f11ee3 Enha: remove extra connection 2025-07-17 23:02:51 +03:00
Grail Finder
9fc36eb7ea Merge branch 'master' into enha/sse-try 2025-07-15 16:13:59 +03:00
Grail Finder
8f9865db3f Enha: simplify sse (worsened event recieving) 2025-07-12 20:33:41 +03:00
11 changed files with 91 additions and 76 deletions

View File

@@ -2,13 +2,12 @@ body{
background-color: #0C1616FF; background-color: #0C1616FF;
color: #8896b2; color: #8896b2;
min-width: 0px; min-width: 0px;
margin: 2em 2em !important; margin: 2em auto !important;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
line-height: 1.5; line-height: 1.5;
font-size: 16px; font-size: 16px;
font-family: Open Sans,Arial; font-family: Open Sans,Arial;
font-weight: bold;
text-align: center; text-align: center;
display: block; display: block;
} }
@@ -23,6 +22,18 @@ tr{
#usertable{ #usertable{
display: block ruby; display: block ruby;
} }
.actiontable{
display: inline flow-root;
margin-inline: 10px;
}
.action_name{
border: none;
display: inline;
font-family: inherit;
font-size: inherit;
padding: none;
width: auto;
}
#errorbox{ #errorbox{
border: 1px solid black; border: 1px solid black;
background-color: darkorange; background-color: darkorange;

BIN
assets/style.css.gz Normal file

Binary file not shown.

View File

@@ -1,15 +1,17 @@
package broker package broker
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"os"
"time" "time"
) )
// the amount of time to wait when pushing a message to // the amount of time to wait when pushing a message to
// a slow client or a client that closed after `range clients` started. // a slow client or a client that closed after `range clients` started.
const patience time.Duration = time.Millisecond * 500 // const patience time.Duration = time.Second * 1
type ( type (
NotificationEvent struct { NotificationEvent struct {
@@ -19,23 +21,23 @@ type (
NotifierChan chan NotificationEvent NotifierChan chan NotificationEvent
Broker struct { Broker struct {
// Events are pushed to this channel by the main events-gathering routine // Events are pushed to this channel by the main events-gathering routine
Notifier NotifierChan Notifier NotifierChan
// New client connections log *slog.Logger
newClients chan NotifierChan addClient chan NotifierChan
// Closed client connections clients map[NotifierChan]struct{}
closingClients chan NotifierChan
// Client connections registry
clients map[NotifierChan]struct{}
} }
) )
func NewBroker() (broker *Broker) { func NewBroker() (broker *Broker) {
// Instantiate a broker // Instantiate a broker
return &Broker{ return &Broker{
Notifier: make(NotifierChan, 1), Notifier: make(NotifierChan, 100),
newClients: make(chan NotifierChan), log: slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
closingClients: make(chan NotifierChan), Level: slog.LevelDebug,
clients: make(map[NotifierChan]struct{}), AddSource: true,
})),
addClient: make(chan NotifierChan, 10),
clients: map[NotifierChan]struct{}{},
} }
} }
@@ -44,7 +46,6 @@ var Notifier *Broker
// for use in different packages // for use in different packages
func init() { func init() {
Notifier = NewBroker() Notifier = NewBroker()
go Notifier.Listen()
} }
func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -59,29 +60,38 @@ func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Credentials", "true")
messageChan := make(NotifierChan, 10) // Buffered channel
broker.newClients <- messageChan
defer func() { broker.closingClients <- messageChan }()
ctx := r.Context() ctx := r.Context()
msgChan := make(NotifierChan, 10)
broker.addClient <- msgChan
// browser can close sse on its own; ping every 2s to prevent // browser can close sse on its own; ping every 2s to prevent
heartbeat := time.NewTicker(2 * time.Second) heartbeat := time.NewTicker(8 * time.Second)
defer heartbeat.Stop() defer heartbeat.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
broker.log.Debug("broker: got ctx done")
// Client disconnected // Client disconnected
return return
case event := <-messageChan: case event := <-broker.Notifier:
_, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.EventName, event.Payload) broker.log.Debug("got event", "event", event)
if err != nil { for i := 0; i < 10; i++ { // Repeat 3 times
fmt.Println(err) _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.EventName, event.Payload)
// Client disconnected if err != nil {
return broker.log.Error("write failed", "error", err)
return
}
w.(http.Flusher).Flush()
// Short delay between sends (non-blocking)
select {
case <-time.After(20 * time.Millisecond): // Adjust delay as needed
case <-ctx.Done():
return
}
} }
w.(http.Flusher).Flush()
case <-heartbeat.C: case <-heartbeat.C:
// Send SSE heartbeat comment // Send SSE heartbeat comment
if _, err := fmt.Fprint(w, ":\n\n"); err != nil { if _, err := fmt.Fprint(w, ":\n\n"); err != nil {
broker.log.Error("failed to write heartbeat", "error", err)
return // Client disconnected return // Client disconnected
} }
w.(http.Flusher).Flush() w.(http.Flusher).Flush()
@@ -89,38 +99,14 @@ func (broker *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
// Listen for new notifications and redistribute them to clients func (broker *Broker) Listen(ctx context.Context) {
func (broker *Broker) Listen() {
slog.Info("Broker listener started")
for { for {
slog.Info("Broker waiting for event")
select { select {
case s := <-broker.newClients: case <-ctx.Done():
// A new client has connected. return
// Register their message channel case clientChan := <-broker.addClient:
broker.clients[s] = struct{}{} // mutex
slog.Info("Client added", "clients listening", len(broker.clients)) broker.clients[clientChan] = struct{}{}
case s := <-broker.closingClients:
// A client has dettached and we want to
// stop sending them messages.
delete(broker.clients, s)
slog.Info("Client removed", "clients listening", len(broker.clients))
case event := <-broker.Notifier:
slog.Info("Received new event", "event", event.EventName, "payload", event.Payload)
// We got a new event from the outside!
// Send event to all connected clients
slog.Info("Broadcasting event to clients", "client_count", len(broker.clients))
for clientMessageChan := range broker.clients {
slog.Info("Sending event to client", "client", clientMessageChan)
select {
case clientMessageChan <- event:
slog.Info("Successfully sent event to client", "client", clientMessageChan)
case <-time.After(patience):
delete(broker.clients, clientMessageChan)
slog.Warn("Client timed out, removed", "client", clientMessageChan, "clients listening", len(broker.clients))
}
}
slog.Info("Finished broadcasting event")
} }
} }
} }

View File

@@ -13,6 +13,12 @@
</head> </head>
<body> <body>
<div id="ancestor" hx-ext="sse" sse-connect="/sub/sse"> <div id="ancestor" hx-ext="sse" sse-connect="/sub/sse">
<script type="text/javascript">
document.body.addEventListener('htmx:sseError', function (e) {
// do something before the event data is swapped in
console.log(e)
})
</script>
<div id="main-content"> <div id="main-content">
{{template "main" .}} {{template "main" .}}
</div> </div>

View File

@@ -1,6 +1,6 @@
{{define "error"}} {{define "error"}}
<a href="/"> <a href="/">
<div id=errorbox class="bg-orange-100 border-l-4 border-black text-black p-4" role="alert"> <div id=errorbox class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert">
<p class="font-bold">An error from server</p> <p class="font-bold">An error from server</p>
<p>{{.}}</p> <p>{{.}}</p>
<p>Click this banner to return to main page.</p> <p>Click this banner to return to main page.</p>

View File

@@ -22,6 +22,7 @@
{{template "roomlist" .List}} {{template "roomlist" .List}}
</div> </div>
{{else}} {{else}}
<div id="sse-listener" sse-connect="/sub/sse" hx-trigger="sse:roomupdate_{{.State.RoomID}}" hx-get="/room" hx-target="#room-interier" hx-swap="none" style="display:none;"></div>
<div id="room"> <div id="room">
{{template "room" .}} {{template "room" .}}
</div> </div>

View File

@@ -1,11 +0,0 @@
{{define "journal"}}
<div id="systembox" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2">
bot journal: <br>
<ul>
{{range .LogJournal}}
<li>{{.Username}}: {{.Entry}}</li>
{{end}}
</ul>
</div>
{{end}}

View File

@@ -1,5 +1,5 @@
{{define "room"}} {{define "room"}}
<div id="interier" hx-get="/" hx-trigger="sse:roomupdate_{{.State.RoomID}}" class=space-y-2> <div id="room-interier" class=space-y-2>
<div id="headwrapper" class="grid grid-cols-1 md:grid-cols-5 md:gap-4"> <div id="headwrapper" class="grid grid-cols-1 md:grid-cols-5 md:gap-4">
<div id="meta" class="md:col-span-1 border-2 rounded-lg text-center space-y-2"> <div id="meta" class="md:col-span-1 border-2 rounded-lg text-center space-y-2">
<p>Hello {{.State.Username}};</p> <p>Hello {{.State.Username}};</p>
@@ -53,16 +53,22 @@
</div> </div>
<hr/> <hr/>
<div class="grid grid-cols-1 md:grid-cols-5 md:gap-4"> <div class="grid grid-cols-1 md:grid-cols-5 md:gap-4">
<div class="md:col-span-1"> <div hx-get="/actionhistory" class="md:col-span-1">
{{template "actionhistory" .Room.ActionHistory}} {{template "actionhistory" .Room.ActionHistory}}
</div> </div>
<div id="cardtable" class="md:col-span-3"> <div id="cardtable" class="md:col-span-3">
{{template "cardtable" .Room}} {{template "cardtable" .Room}}
</div> </div>
<div class="hidden md:block md:col-span-1"> <div class="hidden md:block md:col-span-1"></div> <!-- Spacer -->
{{template "journal" .Room}} </div>
</div> <!-- Spacer --> <div id="systembox" class="overflow-y-auto max-h-96 border-2 border-gray-300 p-4 rounded-lg space-y-2">
</div> bot thought: <br>
<ul>
{{range .Room.LogJournal}}
<li>{{.Username}}: {{.Entry}}</li>
{{end}}
</ul>
</div>
<div> <div>
{{if .Room.IsRunning}} {{if .Room.IsRunning}}
{{if and (eq .State.Role "guesser") (eq .State.Team .Room.TeamTurn)}} {{if and (eq .State.Role "guesser") (eq .State.Team .Room.TeamTurn)}}

View File

@@ -334,3 +334,19 @@ func HandleRemoveBot(w http.ResponseWriter, r *http.Request) {
} }
notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "") notify(models.NotifyRoomUpdatePrefix+fi.Room.ID, "")
} }
func HandleGetRoom(w http.ResponseWriter, r *http.Request) {
fi, err := getFullInfoByCtx(r.Context())
if err != nil {
abortWithError(w, err.Error())
return
}
tmpl, err := template.ParseGlob("components/*.html")
if err != nil {
abortWithError(w, err.Error())
return
}
if err := tmpl.ExecuteTemplate(w, "room", fi); err != nil {
log.Error("failed to execute template", "error", err)
}
}

View File

@@ -106,9 +106,8 @@ func HandleExit(w http.ResponseWriter, r *http.Request) {
if err := repo.RoomDeleteByID(r.Context(), fi.Room.ID); err != nil { if err := repo.RoomDeleteByID(r.Context(), fi.Room.ID); err != nil {
log.Error("failed to remove room", "error", err) log.Error("failed to remove room", "error", err)
} }
notify(models.NotifyRoomListUpdate, "") // why is it needed? notify(models.NotifyRoomListUpdate, "")
} else { } else {
notify(models.NotifyRoomUpdatePrefix, "")
// if regular player leaves, just exit room // if regular player leaves, just exit room
if err := repo.PlayerExitRoom(r.Context(), fi.State.Username); err != nil { if err := repo.PlayerExitRoom(r.Context(), fi.State.Username); err != nil {
log.Error("failed to exit room", "error", err) log.Error("failed to exit room", "error", err)

View File

@@ -92,6 +92,7 @@ func ListenToRequests(port string) *http.Server {
mux.HandleFunc("GET /add-bot", handlers.HandleAddBot) mux.HandleFunc("GET /add-bot", handlers.HandleAddBot)
mux.HandleFunc("GET /remove-bot", handlers.HandleRemoveBot) mux.HandleFunc("GET /remove-bot", handlers.HandleRemoveBot)
mux.HandleFunc("GET /mark-card", handlers.HandleMarkCard) mux.HandleFunc("GET /mark-card", handlers.HandleMarkCard)
mux.HandleFunc("GET /room", handlers.HandleGetRoom)
// special // special
mux.HandleFunc("GET /renotify-bot", handlers.HandleRenotifyBot) mux.HandleFunc("GET /renotify-bot", handlers.HandleRenotifyBot)
// sse // sse