@@ -59,14 +59,12 @@ func NewClient(id string, sub Subscription, conn *websocket.Conn, hub *Hub, user
5959// Run handles sending events to the client and periodic pings.
6060// Context should be passed from the caller for proper lifecycle management.
6161func (c * Client ) Run (ctx context.Context , pingInterval , writeTimeout time.Duration ) {
62- ticker := time .NewTicker (pingInterval )
63- defer func () {
64- ticker .Stop ()
65- // Don't close the websocket here - let the main handler do it
66- // This prevents double-close errors
67- c .Close ()
68- }()
62+ defer c .Close ()
63+
64+ // Start ping sender in separate goroutine to prevent event sends from blocking pings
65+ go c .sendPings (ctx , pingInterval , writeTimeout )
6966
67+ // Handle event sending in main goroutine
7068 for {
7169 select {
7270 case <- ctx .Done ():
@@ -104,6 +102,24 @@ func (c *Client) Run(ctx context.Context, pingInterval, writeTimeout time.Durati
104102
105103 log .Printf ("✓ Event sent to client %s" , c .ID )
106104
105+ case <- c .done :
106+ log .Printf ("client %s: done signal received" , c .ID )
107+ return
108+ }
109+ }
110+ }
111+
112+ // sendPings sends periodic pings in a separate goroutine to prevent blocking by event sends.
113+ func (c * Client ) sendPings (ctx context.Context , pingInterval , writeTimeout time.Duration ) {
114+ ticker := time .NewTicker (pingInterval )
115+ defer ticker .Stop ()
116+
117+ for {
118+ select {
119+ case <- ctx .Done ():
120+ return
121+ case <- c .done :
122+ return
107123 case <- ticker .C :
108124 // Check if we're missing pongs before sending next ping
109125 c .mu .RLock ()
@@ -139,11 +155,6 @@ func (c *Client) Run(ctx context.Context, pingInterval, writeTimeout time.Durati
139155 log .Printf ("error sending ping to client %s: %v" , c .ID , err )
140156 return
141157 }
142- // Ping sent to keep connection alive
143-
144- case <- c .done :
145- log .Printf ("client %s: done signal received" , c .ID )
146- return
147158 }
148159 }
149160}
0 commit comments