@@ -32,6 +32,10 @@ const (
3232 // UI constants for logging.
3333 separatorLine = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
3434 msgTypeField = "type"
35+
36+ // Server ping timing constants.
37+ expectedPingInterval = 54 * time .Second
38+ pingDelayThreshold = 10 * time .Second
3539)
3640
3741// Event represents a webhook event received from the server.
@@ -63,14 +67,16 @@ type Config struct {
6367
6468// Client represents a WebSocket client with automatic reconnection.
6569type Client struct {
66- logger * slog.Logger
67- ws * websocket.Conn
68- stopCh chan struct {}
69- stoppedCh chan struct {}
70- config Config
71- eventCount int
72- retries int
73- mu sync.RWMutex
70+ logger * slog.Logger
71+ ws * websocket.Conn
72+ stopCh chan struct {}
73+ stoppedCh chan struct {}
74+ config Config
75+ lastPingTime time.Time // Time of last ping received
76+ noActivitySince time.Time // Track when we last received anything
77+ mu sync.RWMutex
78+ eventCount int
79+ retries int
7480}
7581
7682// New creates a new robust WebSocket client.
@@ -454,26 +460,62 @@ func (c *Client) readEvents(ctx context.Context, ws *websocket.Conn) error {
454460
455461 // Handle ping messages
456462 if responseType == "ping" {
463+ now := time .Now ()
464+
465+ // Check if pings are arriving on schedule
466+ c .mu .Lock ()
467+ lastPing := c .lastPingTime
468+ c .lastPingTime = now
469+ c .noActivitySince = now
470+ c .mu .Unlock ()
471+
472+ if ! lastPing .IsZero () {
473+ timeSinceLastPing := now .Sub (lastPing )
474+ if timeSinceLastPing > expectedPingInterval + pingDelayThreshold {
475+ c .logger .Warn ("[PING-PONG] Delayed ping from server" ,
476+ "expected_interval" , expectedPingInterval ,
477+ "actual_interval" , timeSinceLastPing ,
478+ "delay" , timeSinceLastPing - expectedPingInterval )
479+ }
480+ }
481+
457482 c .logger .Debug ("[PING-PONG] Received PING from server" )
458483 pong := map [string ]any {msgTypeField : "pong" }
459484 // Echo back any sequence number if present
460485 if seq , ok := response ["seq" ]; ok {
461486 pong ["seq" ] = seq
462487 }
488+ // Set write deadline to ensure timely pong response
489+ if err := ws .SetWriteDeadline (time .Now ().Add (5 * time .Second )); err != nil {
490+ c .logger .Error ("[PING-PONG] Failed to set write deadline" , "error" , err )
491+ return fmt .Errorf ("error setting write deadline: %w" , err )
492+ }
463493 if err := websocket .JSON .Send (ws , pong ); err != nil {
464494 c .logger .Error ("[PING-PONG] Failed to send PONG response" , "error" , err )
465495 return fmt .Errorf ("error sending pong response: %w" , err )
466496 }
497+ // Clear write deadline
498+ if err := ws .SetWriteDeadline (time.Time {}); err != nil {
499+ c .logger .Warn ("[PING-PONG] Failed to clear write deadline" , "error" , err )
500+ }
467501 c .logger .Debug ("[PING-PONG] Sent PONG response to server" , "seq" , pong ["seq" ])
468502 continue
469503 }
470504
471505 // Handle pong acknowledgments
472506 if responseType == "pong" {
507+ c .mu .Lock ()
508+ c .noActivitySince = time .Now ()
509+ c .mu .Unlock ()
473510 c .logger .Debug ("[PING-PONG] Received PONG acknowledgment from server" )
474511 continue
475512 }
476513
514+ // Update activity timestamp for any event
515+ c .mu .Lock ()
516+ c .noActivitySince = time .Now ()
517+ c .mu .Unlock ()
518+
477519 // Process the event inline
478520 event := Event {
479521 Type : responseType ,
0 commit comments