• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / aiforge / 21152598554

19 Jan 2026 10:00PM UTC coverage: 83.846% (+0.9%) from 82.966%
21152598554

push

github

apowers313
Merge branch 'master' of https://github.com/apowers313/aiforge

1604 of 1876 branches covered (85.5%)

Branch coverage included in aggregate %.

8242 of 9867 relevant lines covered (83.53%)

20.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.03
/src/client/hooks/useWebSocket.ts
1
/**
2
 * useWebSocket - React hook for WebSocket connections
3
 */
4
import { useState, useEffect, useCallback, useRef } from 'react';
1✔
5
import { ReconnectingWebSocket, type ReconnectingWebSocketOptions } from '@client/services/websocket.js';
1✔
6
import { waitForServerHealth } from '@client/services/api.js';
1✔
7
import { log } from '@client/services/logger';
1✔
8

9
const wsHookLog = log.websocket;
1✔
10

11
/**
12
 * WebSocket connection status
13
 */
14
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
15

16
/**
17
 * Options for useWebSocket hook
18
 */
19
export interface UseWebSocketOptions {
20
  /**
21
   * Auto-connect on mount
22
   * @default true
23
   */
24
  autoConnect?: boolean;
25

26
  /**
27
   * Wait for server health check before connecting
28
   * This prevents connection attempts while server is starting up
29
   * @default false
30
   */
31
  waitForHealth?: boolean;
32

33
  /**
34
   * Callback when message is received
35
   */
36
  onMessage?: (data: unknown) => void;
37

38
  /**
39
   * Callback when connection opens
40
   */
41
  onOpen?: () => void;
42

43
  /**
44
   * Callback when connection closes
45
   */
46
  onClose?: () => void;
47

48
  /**
49
   * Callback when error occurs
50
   */
51
  onError?: (event: Event) => void;
52

53
  /**
54
   * ReconnectingWebSocket options
55
   */
56
  reconnectOptions?: Omit<ReconnectingWebSocketOptions, 'onMessage' | 'onOpen' | 'onClose' | 'onError'>;
57
}
58

59
/**
60
 * Return type for useWebSocket hook
61
 */
62
export interface UseWebSocketReturn {
63
  status: WebSocketStatus;
64
  isConnected: boolean;
65
  send: (data: unknown) => void;
66
  connect: () => void;
67
  disconnect: () => void;
68
}
69

70
/**
71
 * React hook for managing WebSocket connections
72
 */
73
export function useWebSocket(url: string, options: UseWebSocketOptions = {}): UseWebSocketReturn {
1✔
74
  const {
100✔
75
    autoConnect = true,
100✔
76
    waitForHealth = false,
100✔
77
    onMessage,
100✔
78
    onOpen,
100✔
79
    onClose,
100✔
80
    onError,
100✔
81
    reconnectOptions,
100✔
82
  } = options;
100✔
83

84
  const [status, setStatus] = useState<WebSocketStatus>('disconnected');
100✔
85
  const wsRef = useRef<ReconnectingWebSocket | null>(null);
100✔
86
  const healthCheckInProgressRef = useRef(false);
100✔
87

88
  // Store callbacks in refs to avoid reconnection on callback changes
89
  const onMessageRef = useRef(onMessage);
100✔
90
  const onOpenRef = useRef(onOpen);
100✔
91
  const onCloseRef = useRef(onClose);
100✔
92
  const onErrorRef = useRef(onError);
100✔
93

94
  useEffect(() => {
100✔
95
    onMessageRef.current = onMessage;
18✔
96
    onOpenRef.current = onOpen;
18✔
97
    onCloseRef.current = onClose;
18✔
98
    onErrorRef.current = onError;
18✔
99
  }, [onMessage, onOpen, onClose, onError]);
100✔
100

101
  const connect = useCallback(() => {
100✔
102
    wsHookLog.debug({ hasExistingConnection: !!wsRef.current }, 'useWebSocket.connect() called');
18✔
103
    if (wsRef.current) {
18!
104
      wsHookLog.debug('Already connected, returning');
×
105
      return;
×
106
    }
×
107

108
    // If health check is enabled and not already in progress
109
    if (waitForHealth && !healthCheckInProgressRef.current) {
18✔
110
      healthCheckInProgressRef.current = true;
18✔
111
      setStatus('connecting');
18✔
112
      wsHookLog.info('Starting health check before WebSocket connection');
18✔
113

114
      waitForServerHealth({ maxAttempts: 600, delayMs: 1000 })
18✔
115
        .then(() => {
18✔
116
          wsHookLog.info('Health check passed, proceeding with WebSocket connection');
18✔
117
          healthCheckInProgressRef.current = false;
18✔
118
          // Now actually connect
119
          if (!wsRef.current) {
18✔
120
            wsHookLog.debug({ url }, 'Creating ReconnectingWebSocket');
18✔
121
            wsRef.current = new ReconnectingWebSocket(url, {
18✔
122
              ...reconnectOptions,
18✔
123
              onMessage: (data): void => {
18✔
124
                onMessageRef.current?.(data);
17✔
125
              },
17✔
126
              onOpen: (): void => {
18✔
127
                wsHookLog.info('useWebSocket: connection opened');
18✔
128
                setStatus('connected');
18✔
129
                onOpenRef.current?.();
18✔
130
              },
18✔
131
              onClose: (): void => {
18✔
132
                wsHookLog.info('useWebSocket: connection closed');
22✔
133
                setStatus('disconnected');
22✔
134
                onCloseRef.current?.();
22✔
135
              },
22✔
136
              onError: (event): void => {
18✔
137
                wsHookLog.error({ type: event.type }, 'useWebSocket: connection error');
×
138
                setStatus('error');
×
139
                onErrorRef.current?.(event);
×
140
              },
×
141
            });
18✔
142
            wsHookLog.debug('Initiating WebSocket connection');
18✔
143
            wsRef.current.connect();
18✔
144
          }
18✔
145
        })
18✔
146
        .catch(() => {
18✔
147
          wsHookLog.error('Health check failed');
×
148
          healthCheckInProgressRef.current = false;
×
149
          setStatus('error');
×
150
          onErrorRef.current?.(new Event('health_check_failed'));
×
151
        });
18✔
152
      return;
18✔
153
    }
18!
154

155
    wsHookLog.debug({ url }, 'Connecting without health check');
×
156
    setStatus('connecting');
×
157

158
    wsRef.current = new ReconnectingWebSocket(url, {
×
159
      ...reconnectOptions,
×
160
      onMessage: (data): void => {
×
161
        onMessageRef.current?.(data);
×
162
      },
×
163
      onOpen: (): void => {
×
164
        wsHookLog.info('useWebSocket: connection opened');
×
165
        setStatus('connected');
×
166
        onOpenRef.current?.();
×
167
      },
×
168
      onClose: (): void => {
×
169
        wsHookLog.info('useWebSocket: connection closed');
×
170
        setStatus('disconnected');
×
171
        onCloseRef.current?.();
×
172
      },
×
173
      onError: (event): void => {
×
174
        wsHookLog.error({ type: event.type }, 'useWebSocket: connection error');
×
175
        setStatus('error');
×
176
        onErrorRef.current?.(event);
×
177
      },
×
178
    });
×
179

180
    wsRef.current.connect();
×
181
  }, [url, reconnectOptions, waitForHealth]);
100✔
182

183
  const disconnect = useCallback(() => {
100✔
184
    if (wsRef.current) {
×
185
      wsRef.current.close();
×
186
      wsRef.current = null;
×
187
      setStatus('disconnected');
×
188
    }
×
189
  }, []);
100✔
190

191
  const send = useCallback((data: unknown) => {
100✔
192
    wsRef.current?.send(data);
38✔
193
  }, []);
100✔
194

195
  // Auto-connect on mount
196
  useEffect(() => {
100✔
197
    if (autoConnect) {
18✔
198
      connect();
18✔
199
    }
18✔
200

201
    return (): void => {
18✔
202
      // Reset health check flag on cleanup to handle React StrictMode double-mount
203
      healthCheckInProgressRef.current = false;
18✔
204
      if (wsRef.current) {
18✔
205
        wsRef.current.close();
18✔
206
        wsRef.current = null;
18✔
207
      }
18✔
208
    };
18✔
209
  }, [autoConnect, connect]);
100✔
210

211
  return {
100✔
212
    status,
100✔
213
    isConnected: status === 'connected',
100✔
214
    send,
100✔
215
    connect,
100✔
216
    disconnect,
100✔
217
  };
100✔
218
}
100✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc