* increases timeout * refactor: reuse constants for redis db * avoid closing connection on pong error * fix: prevent electron from pausing timer executions Our pings could be paused when our app is backgrounded. * refactor: remove unnecessary refs * fix: presence unreliable within same pod It was broadcasting presence changes to other pods, but not notifying clients connected to the same pod
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
package pusher
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/flowy-live/llink/genproto/llink/pusher"
|
|
"github.com/flowy-live/llink/internal/auth"
|
|
"github.com/google/uuid"
|
|
"nhooyr.io/websocket"
|
|
)
|
|
|
|
// Server handles WebSocket upgrades and gRPC presence queries.
|
|
type Server struct {
|
|
pbpusher.UnimplementedPusherServiceServer
|
|
|
|
ctx context.Context // server-scoped context for graceful shutdown
|
|
hub *Hub
|
|
bridge *RedisBridge
|
|
authSvc auth.AuthService
|
|
}
|
|
|
|
// NewServer creates a new pusher server. The ctx controls the lifetime of all
|
|
// WebSocket connections — when cancelled, all connections are closed gracefully.
|
|
func NewServer(ctx context.Context, hub *Hub, bridge *RedisBridge, authSvc auth.AuthService) *Server {
|
|
return &Server{
|
|
ctx: ctx,
|
|
hub: hub,
|
|
bridge: bridge,
|
|
authSvc: authSvc,
|
|
}
|
|
}
|
|
|
|
// HandleWebSocket handles the WebSocket upgrade and connection lifecycle.
|
|
func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|
// Authenticate via query param (WebSocket upgrade can't use custom headers)
|
|
token := r.URL.Query().Get("token")
|
|
if token == "" {
|
|
http.Error(w, "token required", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
session, err := s.authSvc.GetSession(r.Context(), token)
|
|
if err != nil {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Accept WebSocket upgrade
|
|
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
|
// Allow all origins for now — CORS is handled at the gateway level
|
|
InsecureSkipVerify: true,
|
|
})
|
|
if err != nil {
|
|
slog.Error("websocket accept failed", "error", err)
|
|
return
|
|
}
|
|
|
|
connID := uuid.New().String()
|
|
conn := newConn(connID, session.HumanId, ws)
|
|
|
|
slog.Info("websocket connected", "connId", connID, "humanId", session.HumanId)
|
|
|
|
// Use server context, NOT r.Context(). After WebSocket upgrade, the HTTP
|
|
// request context can be cancelled by load balancers or Go's HTTP server,
|
|
// and nhooyr/websocket permanently closes the conn on any context error.
|
|
ctx, cancel := context.WithCancel(s.ctx)
|
|
defer cancel()
|
|
|
|
// Auto-subscribe to presence channel so this user appears online
|
|
s.hub.Subscribe(conn, "_presence:"+session.HumanId)
|
|
|
|
// WritePump runs in a separate goroutine
|
|
go conn.WritePump(ctx)
|
|
|
|
// ReadPump blocks until the connection closes
|
|
conn.ReadPump(ctx, s.hub)
|
|
|
|
slog.Info("websocket disconnected", "connId", connID, "humanId", session.HumanId)
|
|
}
|
|
|
|
// GetOnlineHumanIds returns all currently connected human IDs.
|
|
func (s *Server) GetOnlineHumanIds(ctx context.Context, _ *pbpusher.GetOnlineHumanIdsRequest) (*pbpusher.GetOnlineHumanIdsResponse, error) {
|
|
humanIDs, err := s.bridge.GetAllConnectedHumanIDs(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &pbpusher.GetOnlineHumanIdsResponse{HumanIds: humanIDs}, nil
|
|
}
|
|
|
|
// IsOnline checks whether specific humans are currently online.
|
|
func (s *Server) IsOnline(ctx context.Context, req *pbpusher.IsOnlineRequest) (*pbpusher.IsOnlineResponse, error) {
|
|
allOnline, err := s.bridge.GetAllConnectedHumanIDs(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
onlineSet := make(map[string]bool, len(allOnline))
|
|
for _, id := range allOnline {
|
|
onlineSet[id] = true
|
|
}
|
|
result := make(map[string]bool, len(req.HumanIds))
|
|
for _, id := range req.HumanIds {
|
|
result[id] = onlineSet[id]
|
|
}
|
|
return &pbpusher.IsOnlineResponse{Online: result}, nil
|
|
}
|
|
|
|
// GetChannelPresence returns presence (human IDs) for specific channels.
|
|
func (s *Server) GetChannelPresence(ctx context.Context, req *pbpusher.GetChannelPresenceRequest) (*pbpusher.GetChannelPresenceResponse, error) {
|
|
presence, err := s.bridge.GetPresence(ctx, req.ChannelIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := &pbpusher.GetChannelPresenceResponse{
|
|
Presences: make(map[string]*pbpusher.ChannelPresence, len(presence)),
|
|
}
|
|
for chID, humanIDs := range presence {
|
|
resp.Presences[chID] = &pbpusher.ChannelPresence{
|
|
HumanIds: humanIDs,
|
|
}
|
|
}
|
|
return resp, nil
|
|
}
|