Add !votes command, fix vote tally timing, and improve Kosmi stability
- Add !votes command (IRC + Kosmi) showing per-session and all-time vote
breakdowns for the current game via new Jackbox API endpoints
(GET sessions/{id}/games, sessions/{id}/votes, games/{id})
- Fix vote tally broadcasting: remove debounce timer, announce tallies
only at game transitions or session end instead of after every vote
- Add !kreconnect IRC command to manually trigger Kosmi reconnection
- Add WebSocket ping/pong keepalive and write mutex to Kosmi client
for connection stability
- Add watchConnection() auto-reconnect on unexpected Kosmi disconnects
- Remove old 2025-10-31 chat summaries; add votes command design doc
Made-with: Cursor
This commit is contained in:
@@ -14,10 +14,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
kosmiWSURL = "wss://engine.kosmi.io/gql-ws"
|
||||
kosmiWSURL = "wss://engine.kosmi.io/gql-ws"
|
||||
kosmiHTTPURL = "https://engine.kosmi.io/"
|
||||
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
appVersion = "4364"
|
||||
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
appVersion = "4364"
|
||||
|
||||
pingInterval = 30 * time.Second
|
||||
pongTimeout = 90 * time.Second
|
||||
writeWait = 10 * time.Second
|
||||
)
|
||||
|
||||
// GraphQL-WS Protocol message types
|
||||
@@ -40,6 +44,7 @@ type GraphQLWSClient struct {
|
||||
messageCallback func(*NewMessagePayload)
|
||||
connected bool
|
||||
mu sync.RWMutex
|
||||
writeMu sync.Mutex
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
@@ -208,9 +213,17 @@ func (c *GraphQLWSClient) Connect() error {
|
||||
c.connected = true
|
||||
c.mu.Unlock()
|
||||
|
||||
// Set up ping/pong keepalive
|
||||
conn.SetReadDeadline(time.Now().Add(pongTimeout))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(pongTimeout))
|
||||
return nil
|
||||
})
|
||||
|
||||
c.log.Info("Native WebSocket client connected and ready")
|
||||
|
||||
// Start message listener
|
||||
// Start keepalive pinger and message listener
|
||||
go c.startPing()
|
||||
go c.listenForMessages()
|
||||
|
||||
return nil
|
||||
@@ -359,7 +372,10 @@ func (c *GraphQLWSClient) SendMessage(text string) error {
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.conn.WriteJSON(msg); err != nil {
|
||||
c.writeMu.Lock()
|
||||
err := c.conn.WriteJSON(msg)
|
||||
c.writeMu.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send message: %w", err)
|
||||
}
|
||||
|
||||
@@ -396,3 +412,33 @@ func (c *GraphQLWSClient) IsConnected() bool {
|
||||
return c.connected
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the client disconnects
|
||||
func (c *GraphQLWSClient) Done() <-chan struct{} {
|
||||
return c.done
|
||||
}
|
||||
|
||||
// startPing sends WebSocket ping frames at a regular interval to keep the
|
||||
// connection alive and detect stale connections early.
|
||||
func (c *GraphQLWSClient) startPing() {
|
||||
ticker := time.NewTicker(pingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.writeMu.Lock()
|
||||
err := c.conn.WriteControl(
|
||||
websocket.PingMessage, nil, time.Now().Add(writeWait),
|
||||
)
|
||||
c.writeMu.Unlock()
|
||||
if err != nil {
|
||||
c.log.Warnf("Ping failed, connection likely dead: %v", err)
|
||||
c.conn.Close()
|
||||
return
|
||||
}
|
||||
case <-c.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user