33import { useCallback , useRef , useEffect } from 'react'
44import { useMissionControl } from '@/store'
55import { normalizeModel } from '@/lib/utils'
6+ import { buildGatewayWebSocketUrl } from '@/lib/gateway-url'
67import {
78 getOrCreateDeviceIdentity ,
89 signPayload ,
@@ -21,6 +22,7 @@ const DEFAULT_GATEWAY_CLIENT_ID = process.env.NEXT_PUBLIC_GATEWAY_CLIENT_ID || '
2122// Heartbeat configuration
2223const PING_INTERVAL_MS = 30_000
2324const MAX_MISSED_PONGS = 3
25+ const ERROR_LOG_DEDUPE_MS = 5_000
2426
2527// Gateway message types
2628interface GatewayFrame {
@@ -54,6 +56,7 @@ export function useWebSocket() {
5456 const manualDisconnectRef = useRef < boolean > ( false )
5557 const nonRetryableErrorRef = useRef < string | null > ( null )
5658 const connectRef = useRef < ( url : string , token ?: string ) => void > ( ( ) => { } )
59+ const lastWebSocketErrorRef = useRef < { message : string ; at : number } | null > ( null )
5760
5861 // Heartbeat tracking
5962 const pingCounterRef = useRef < number > ( 0 )
@@ -504,34 +507,61 @@ export function useWebSocket() {
504507 getGatewayErrorHelp ,
505508 ] )
506509
510+ const normalizeWebSocketUrl = useCallback ( ( rawUrl : string ) : string => {
511+ const built = buildGatewayWebSocketUrl ( {
512+ host : rawUrl ,
513+ port : Number ( process . env . NEXT_PUBLIC_GATEWAY_PORT || '18789' ) ,
514+ browserProtocol : window . location . protocol ,
515+ } )
516+
517+ const parsed = new URL ( built , window . location . origin )
518+ parsed . protocol = parsed . protocol === 'https:' ? 'wss:' : parsed . protocol === 'http:' ? 'ws:' : parsed . protocol
519+ parsed . pathname = '/'
520+ parsed . search = ''
521+ parsed . hash = ''
522+ return parsed . toString ( ) . replace ( / \/ $ / , '' )
523+ } , [ ] )
524+
525+ const shouldSuppressWebSocketError = useCallback ( ( message : string ) : boolean => {
526+ const now = Date . now ( )
527+ const previous = lastWebSocketErrorRef . current
528+ if ( previous && previous . message === message && now - previous . at < ERROR_LOG_DEDUPE_MS ) {
529+ return true
530+ }
531+ lastWebSocketErrorRef . current = { message, at : now }
532+ return false
533+ } , [ ] )
534+
507535 const connect = useCallback ( ( url : string , token ?: string ) => {
508536 const state = wsRef . current ?. readyState
509537 if ( state === WebSocket . OPEN || state === WebSocket . CONNECTING ) {
510538 return // Already connected or connecting
511539 }
512540
513- // Extract token from URL if present
514- const urlObj = new URL ( url , window . location . origin )
515- const urlToken = urlObj . searchParams . get ( 'token' )
541+ let urlToken = ''
542+ try {
543+ const parsedInput = new URL ( url , window . location . origin )
544+ urlToken = parsedInput . searchParams . get ( 'token' ) || ''
545+ } catch {
546+ urlToken = ''
547+ }
516548 authTokenRef . current = token || urlToken || ''
517549
518- // Remove token from URL (we'll send it in handshake)
519- urlObj . searchParams . delete ( 'token' )
520-
521- reconnectUrl . current = url
550+ const normalizedUrl = normalizeWebSocketUrl ( url )
551+ reconnectUrl . current = normalizedUrl
522552 handshakeCompleteRef . current = false
523553 manualDisconnectRef . current = false
524554 nonRetryableErrorRef . current = null
525555
526556 try {
527- const ws = new WebSocket ( url . split ( '?' ) [ 0 ] ) // Connect without query params
557+ const ws = new WebSocket ( normalizedUrl )
528558 wsRef . current = ws
529559
530560 ws . onopen = ( ) => {
531- log . info ( `Connected to ${ url . split ( '?' ) [ 0 ] } ` )
561+ log . info ( `Connected to ${ normalizedUrl } ` )
532562 // Don't set isConnected yet - wait for handshake
533563 setConnection ( {
534- url : url . split ( '?' ) [ 0 ] ,
564+ url : normalizedUrl ,
535565 reconnectAttempts : 0
536566 } )
537567 // Wait for connect.challenge from server
@@ -594,20 +624,33 @@ export function useWebSocket() {
594624
595625 ws . onerror = ( error ) => {
596626 log . error ( 'WebSocket error:' , error )
627+ const errorMessage = 'WebSocket error occurred'
628+ if ( ! shouldSuppressWebSocketError ( errorMessage ) ) {
629+ addLog ( {
630+ id : `error-${ Date . now ( ) } ` ,
631+ timestamp : Date . now ( ) ,
632+ level : 'error' ,
633+ source : 'websocket' ,
634+ message : errorMessage
635+ } )
636+ }
637+ }
638+
639+ } catch ( error ) {
640+ log . error ( 'Failed to connect to WebSocket:' , error )
641+ const errorMessage = 'Failed to initialize WebSocket connection'
642+ if ( ! shouldSuppressWebSocketError ( errorMessage ) ) {
597643 addLog ( {
598644 id : `error-${ Date . now ( ) } ` ,
599645 timestamp : Date . now ( ) ,
600646 level : 'error' ,
601647 source : 'websocket' ,
602- message : `WebSocket error occurred`
648+ message : errorMessage
603649 } )
604650 }
605-
606- } catch ( error ) {
607- log . error ( 'Failed to connect to WebSocket:' , error )
608651 setConnection ( { isConnected : false } )
609652 }
610- } , [ setConnection , handleGatewayFrame , addLog , stopHeartbeat ] )
653+ } , [ setConnection , handleGatewayFrame , addLog , stopHeartbeat , normalizeWebSocketUrl , shouldSuppressWebSocketError ] )
611654
612655 // Keep ref in sync so onclose always calls the latest version of connect
613656 useEffect ( ( ) => {
0 commit comments