11import { useCallback , useEffect , useMemo } from "react" ;
2+ import { Logger } from "tslog" ;
23
34import { useRTCStore } from "@hooks/stores" ;
45
@@ -25,6 +26,128 @@ interface sendMessageParams {
2526 requireOrdered ?: boolean ;
2627}
2728
29+ const HANDSHAKE_TIMEOUT = 30 * 1000 ; // 30 seconds
30+ const HANDSHAKE_MAX_ATTEMPTS = 10 ;
31+ const logger = new Logger ( { name : "hidrpc" } ) ;
32+
33+ export function doRpcHidHandshake ( rpcHidChannel : RTCDataChannel , setRpcHidProtocolVersion : ( version : number | null ) => void ) {
34+ let attempts = 0 ;
35+ let lastConnectedTime : Date | undefined ;
36+ let lastSendTime : Date | undefined ;
37+ let handshakeCompleted = false ;
38+ let handshakeInterval : ReturnType < typeof setInterval > | null = null ;
39+
40+ const shouldGiveUp = ( ) => {
41+ if ( attempts > HANDSHAKE_MAX_ATTEMPTS ) {
42+ logger . error ( `Failed to send handshake message after ${ HANDSHAKE_MAX_ATTEMPTS } attempts` ) ;
43+ return true ;
44+ }
45+
46+ const timeSinceConnected = lastConnectedTime ? Date . now ( ) - lastConnectedTime . getTime ( ) : 0 ;
47+ if ( timeSinceConnected > HANDSHAKE_TIMEOUT ) {
48+ logger . error ( `Handshake timed out after ${ timeSinceConnected } ms` ) ;
49+ return true ;
50+ }
51+
52+ return false ;
53+ }
54+
55+ const sendHandshake = ( initial : boolean ) => {
56+ if ( handshakeCompleted ) return ;
57+
58+ attempts ++ ;
59+ lastSendTime = new Date ( ) ;
60+
61+ if ( ! initial && shouldGiveUp ( ) ) {
62+ if ( handshakeInterval ) {
63+ clearInterval ( handshakeInterval ) ;
64+ handshakeInterval = null ;
65+ }
66+ return ;
67+ }
68+
69+ let data : Uint8Array | undefined ;
70+ try {
71+ const message = new HandshakeMessage ( HID_RPC_VERSION ) ;
72+ data = message . marshal ( ) ;
73+ } catch ( e ) {
74+ logger . error ( "Failed to marshal message" , e ) ;
75+ return ;
76+ }
77+ if ( ! data ) return ;
78+ rpcHidChannel . send ( data as unknown as ArrayBuffer ) ;
79+
80+ if ( initial ) {
81+ handshakeInterval = setInterval ( ( ) => {
82+ sendHandshake ( false ) ;
83+ } , 1000 ) ;
84+ }
85+ } ;
86+
87+ const onMessage = ( ev : MessageEvent ) => {
88+ const message = unmarshalHidRpcMessage ( new Uint8Array ( ev . data ) ) ;
89+ if ( ! message || ! ( message instanceof HandshakeMessage ) ) return ;
90+
91+ if ( ! message . version ) {
92+ logger . error ( "Received handshake message without version" , message ) ;
93+ return ;
94+ }
95+
96+ if ( message . version > HID_RPC_VERSION ) {
97+ // we assume that the UI is always using the latest version of the HID RPC protocol
98+ // so we can't support this
99+ // TODO: use capabilities to determine rather than version number
100+ logger . error ( "Server is using a newer version than the client" , message ) ;
101+ return ;
102+ }
103+
104+ setRpcHidProtocolVersion ( message . version ) ;
105+
106+ const timeUsed = lastSendTime ? Date . now ( ) - lastSendTime . getTime ( ) : 0 ;
107+ logger . info ( `Handshake completed in ${ timeUsed } ms after ${ attempts } attempts (Version: ${ message . version } / ${ HID_RPC_VERSION } )` ) ;
108+
109+ // clean up
110+ rpcHidChannel . removeEventListener ( "message" , onMessage ) ;
111+ resetHandshake ( { completed : true } ) ;
112+ } ;
113+
114+ const resetHandshake = ( { lastConnectedTime : newLastConnectedTime , completed } : { lastConnectedTime ?: Date | undefined , completed ?: boolean } ) => {
115+ if ( newLastConnectedTime ) lastConnectedTime = newLastConnectedTime ;
116+ lastSendTime = undefined ;
117+ attempts = 0 ;
118+ if ( completed !== undefined ) handshakeCompleted = completed ;
119+ if ( handshakeInterval ) {
120+ clearInterval ( handshakeInterval ) ;
121+ handshakeInterval = null ;
122+ }
123+ } ;
124+
125+ const onConnected = ( ) => {
126+ resetHandshake ( { lastConnectedTime : new Date ( ) } ) ;
127+ logger . info ( "Channel connected" ) ;
128+
129+ sendHandshake ( true ) ;
130+ rpcHidChannel . addEventListener ( "message" , onMessage ) ;
131+ } ;
132+
133+ const onClose = ( ) => {
134+ resetHandshake ( { lastConnectedTime : undefined , completed : false } ) ;
135+
136+ logger . info ( "Channel closed" ) ;
137+ setRpcHidProtocolVersion ( null ) ;
138+
139+ rpcHidChannel . removeEventListener ( "message" , onMessage ) ;
140+ } ;
141+
142+ rpcHidChannel . addEventListener ( "open" , onConnected ) ;
143+ rpcHidChannel . addEventListener ( "close" , onClose ) ;
144+
145+ // handle case where channel is already open when the hook is mounted
146+ if ( rpcHidChannel . readyState === "open" ) {
147+ onConnected ( ) ;
148+ }
149+ }
150+
28151export function useHidRpc ( onHidRpcMessage ?: ( payload : RpcMessage ) => void ) {
29152 const {
30153 rpcHidChannel,
@@ -78,7 +201,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
78201 try {
79202 data = message . marshal ( ) ;
80203 } catch ( e ) {
81- console . error ( "Failed to marshal HID RPC message" , e ) ;
204+ logger . error ( "Failed to marshal message" , e ) ;
82205 }
83206 if ( ! data ) return ;
84207
@@ -151,99 +274,46 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
151274 sendMessage ( KEEPALIVE_MESSAGE ) ;
152275 } , [ sendMessage ] ) ;
153276
154- const sendHandshake = useCallback ( ( ) => {
155- if ( hidRpcDisabled ) return ;
156- if ( rpcHidProtocolVersion ) return ;
157- if ( ! rpcHidChannel ) return ;
158-
159- sendMessage ( new HandshakeMessage ( HID_RPC_VERSION ) , { ignoreHandshakeState : true } ) ;
160- } , [ rpcHidChannel , rpcHidProtocolVersion , sendMessage , hidRpcDisabled ] ) ;
161-
162- const handleHandshake = useCallback (
163- ( message : HandshakeMessage ) => {
164- if ( hidRpcDisabled ) return ;
165-
166- if ( ! message . version ) {
167- console . error ( "Received handshake message without version" , message ) ;
168- return ;
169- }
170-
171- if ( message . version > HID_RPC_VERSION ) {
172- // we assume that the UI is always using the latest version of the HID RPC protocol
173- // so we can't support this
174- // TODO: use capabilities to determine rather than version number
175- console . error ( "Server is using a newer HID RPC version than the client" , message ) ;
176- return ;
177- }
178-
179- setRpcHidProtocolVersion ( message . version ) ;
180- } ,
181- [ setRpcHidProtocolVersion , hidRpcDisabled ] ,
182- ) ;
183-
184277 useEffect ( ( ) => {
185278 if ( ! rpcHidChannel ) return ;
186279 if ( hidRpcDisabled ) return ;
187280
188- // send handshake message
189- sendHandshake ( ) ;
190-
191281 const messageHandler = ( e : MessageEvent ) => {
192282 if ( typeof e . data === "string" ) {
193- console . warn ( "Received string data in HID RPC message handler" , e . data ) ;
283+ logger . warn ( "Received string data in message handler" , e . data ) ;
194284 return ;
195285 }
196286
197287 const message = unmarshalHidRpcMessage ( new Uint8Array ( e . data ) ) ;
198288 if ( ! message ) {
199- console . warn ( "Received invalid HID RPC message" , e . data ) ;
289+ logger . warn ( "Received invalid message" , e . data ) ;
200290 return ;
201291 }
202292
203- console . debug ( "Received HID RPC message" , message ) ;
204- switch ( message . constructor ) {
205- case HandshakeMessage :
206- handleHandshake ( message as HandshakeMessage ) ;
207- break ;
208- default :
209- // not all events are handled here, the rest are handled by the onHidRpcMessage callback
210- break ;
211- }
293+ if ( message instanceof HandshakeMessage ) return ; // handshake message is handled by the doRpcHidHandshake function
212294
213- onHidRpcMessage ?.( message ) ;
214- } ;
295+ // to remove it from the production build, we need to use the /* @__PURE__ */ comment here
296+ // setting `esbuild.pure` doesn't work
297+ /* @__PURE__ */ logger . debug ( "Received message" , message ) ;
215298
216- const openHandler = ( ) => {
217- console . info ( "HID RPC channel opened" ) ;
218- sendHandshake ( ) ;
219- } ;
220-
221- const closeHandler = ( ) => {
222- console . info ( "HID RPC channel closed" ) ;
223- setRpcHidProtocolVersion ( null ) ;
299+ onHidRpcMessage ?.( message ) ;
224300 } ;
225301
226302 const errorHandler = ( e : Event ) => {
227- console . error ( `Error on rpcHidChannel '${ rpcHidChannel . label } ': ${ e } ` )
303+ logger . error ( `Error on channel '${ rpcHidChannel . label } '` , e ) ;
228304 } ;
229305
230306 rpcHidChannel . addEventListener ( "message" , messageHandler ) ;
231- rpcHidChannel . addEventListener ( "close" , closeHandler ) ;
232307 rpcHidChannel . addEventListener ( "error" , errorHandler ) ;
233- rpcHidChannel . addEventListener ( "open" , openHandler ) ;
234308
235309 return ( ) => {
236310 rpcHidChannel . removeEventListener ( "message" , messageHandler ) ;
237- rpcHidChannel . removeEventListener ( "close" , closeHandler ) ;
238311 rpcHidChannel . removeEventListener ( "error" , errorHandler ) ;
239- rpcHidChannel . removeEventListener ( "open" , openHandler ) ;
240312 } ;
241313 } , [
242314 rpcHidChannel ,
243315 onHidRpcMessage ,
244316 setRpcHidProtocolVersion ,
245- sendHandshake ,
246- handleHandshake ,
247317 hidRpcDisabled ,
248318 ] ) ;
249319
0 commit comments