11import { useState , useEffect , useCallback , useRef } from 'react' ;
2+ import type { RemoteMessage } from '@/types' ;
23
34export const useRemoteConnection = ( ) => {
45 const wsRef = useRef < WebSocket | null > ( null ) ;
56 const [ status , setStatus ] = useState < 'connecting' | 'connected' | 'disconnected' > ( 'disconnected' ) ;
7+ const [ latency , setLatency ] = useState < number | null > ( null ) ;
68
79 useEffect ( ( ) => {
810 let isMounted = true ;
@@ -26,6 +28,9 @@ export const useRemoteConnection = () => {
2628 }
2729
2830 let reconnectTimer : NodeJS . Timeout ;
31+ let heartbeatTimer : NodeJS . Timeout ;
32+ let reconnectDelay = 1000 ;
33+ const MAX_RECONNECT_DELAY = 30000 ;
2934
3035 const connect = ( ) => {
3136 if ( ! isMounted ) return ;
@@ -35,27 +40,64 @@ export const useRemoteConnection = () => {
3540 wsRef . current . onopen = null ;
3641 wsRef . current . onclose = null ;
3742 wsRef . current . onerror = null ;
43+ wsRef . current . onmessage = null ;
3844 wsRef . current . close ( ) ;
3945 wsRef . current = null ;
4046 }
4147
4248 setStatus ( 'connecting' ) ;
4349 const socket = new WebSocket ( wsUrl ) ;
50+ wsRef . current = socket ;
4451
4552 socket . onopen = ( ) => {
46- if ( isMounted ) setStatus ( 'connected' ) ;
53+ if ( ! isMounted ) return ;
54+ setStatus ( 'connected' ) ;
55+ reconnectDelay = 1000 ;
56+
57+ // Fire first ping right away so the UI updates instantly
58+ if ( socket . readyState === WebSocket . OPEN ) {
59+ socket . send ( JSON . stringify ( { type : 'ping' , timestamp : Date . now ( ) } ) ) ;
60+ }
61+
62+ clearInterval ( heartbeatTimer ) ;
63+ heartbeatTimer = setInterval ( ( ) => {
64+ if ( socket . readyState === WebSocket . OPEN ) {
65+ socket . send ( JSON . stringify ( { type : 'ping' , timestamp : Date . now ( ) } ) ) ;
66+ }
67+ } , 3000 ) ;
4768 } ;
48- socket . onclose = ( ) => {
49- if ( isMounted ) {
50- setStatus ( 'disconnected' ) ;
51- reconnectTimer = setTimeout ( connect , 3000 ) ;
69+
70+ socket . onmessage = ( event ) => {
71+ try {
72+ const msg = JSON . parse ( event . data ) ;
73+ if ( msg . type === 'pong' ) {
74+ const ts = Number ( msg . timestamp ) ;
75+ const rtt = Date . now ( ) - ts ;
76+ if ( Number . isFinite ( ts ) && Number . isFinite ( rtt ) && rtt >= 0 && rtt < 60000 ) {
77+ setLatency ( rtt ) ;
78+ }
79+ }
80+ } catch {
81+ // ignore non-JSON or malformed server messages
5282 }
5383 } ;
54- socket . onerror = ( ) => {
55- socket . close ( ) ;
84+
85+ socket . onclose = ( ) => {
86+ if ( ! isMounted ) return ;
87+ setStatus ( 'disconnected' ) ;
88+ setLatency ( null ) ;
89+ wsRef . current = null ;
90+ clearInterval ( heartbeatTimer ) ;
91+
92+ const delay = reconnectDelay ;
93+ reconnectDelay = Math . min ( reconnectDelay * 2 , MAX_RECONNECT_DELAY ) ;
94+ reconnectTimer = setTimeout ( connect , delay ) ;
5695 } ;
5796
58- wsRef . current = socket ;
97+ socket . onerror = ( e ) => {
98+ console . error ( 'WS Error' , e ) ;
99+ socket . close ( ) ;
100+ } ;
59101 } ;
60102
61103 // Defer to next tick so React Strict Mode's immediate unmount
@@ -66,31 +108,33 @@ export const useRemoteConnection = () => {
66108 isMounted = false ;
67109 clearTimeout ( initialTimer ) ;
68110 clearTimeout ( reconnectTimer ) ;
111+ clearInterval ( heartbeatTimer ) ;
112+
69113 if ( wsRef . current ) {
70- // Nullify handlers to prevent cascading error/close events
71114 wsRef . current . onopen = null ;
72115 wsRef . current . onclose = null ;
73116 wsRef . current . onerror = null ;
117+ wsRef . current . onmessage = null ;
74118 wsRef . current . close ( ) ;
75119 wsRef . current = null ;
76120 }
77121 } ;
78122 } , [ ] ) ;
79123
80- const send = useCallback ( ( msg : any ) => {
124+ const send = useCallback ( ( msg : RemoteMessage ) => {
81125 if ( wsRef . current ?. readyState === WebSocket . OPEN ) {
82126 wsRef . current . send ( JSON . stringify ( msg ) ) ;
83127 }
84128 } , [ ] ) ;
85129
86- const sendCombo = useCallback ( ( msg : string [ ] ) => {
130+ const sendCombo = useCallback ( ( keys : string [ ] ) => {
87131 if ( wsRef . current ?. readyState === WebSocket . OPEN ) {
88132 wsRef . current . send ( JSON . stringify ( {
89- type : " combo" ,
90- keys : msg ,
133+ type : ' combo' ,
134+ keys,
91135 } ) ) ;
92136 }
93137 } , [ ] ) ;
94138
95- return { status, send, sendCombo } ;
139+ return { status, latency , send, sendCombo } ;
96140} ;
0 commit comments