@@ -9,7 +9,7 @@ import crypto from 'crypto';
9
9
import { toast } from 'sonner' ;
10
10
import { useSearchParams } from 'next/navigation' ;
11
11
import { getSuggestions } from '@/lib/actions' ;
12
- import Error from 'next/error' ;
12
+ import NextError from 'next/error' ;
13
13
14
14
export type Message = {
15
15
messageId : string ;
@@ -32,11 +32,24 @@ const useSocket = (
32
32
setIsWSReady : ( ready : boolean ) => void ,
33
33
setError : ( error : boolean ) => void ,
34
34
) => {
35
- const [ ws , setWs ] = useState < WebSocket | null > ( null ) ;
35
+ const wsRef = useRef < WebSocket | null > ( null ) ;
36
+ const reconnectTimeoutRef = useRef < NodeJS . Timeout > ( ) ;
37
+ const retryCountRef = useRef ( 0 ) ;
38
+ const isCleaningUpRef = useRef ( false ) ;
39
+ const MAX_RETRIES = 3 ;
40
+ const INITIAL_BACKOFF = 1000 ; // 1 second
41
+
42
+ const getBackoffDelay = ( retryCount : number ) => {
43
+ return Math . min ( INITIAL_BACKOFF * Math . pow ( 2 , retryCount ) , 10000 ) ; // Cap at 10 seconds
44
+ } ;
36
45
37
46
useEffect ( ( ) => {
38
- if ( ! ws ) {
39
- const connectWs = async ( ) => {
47
+ const connectWs = async ( ) => {
48
+ if ( wsRef . current ?. readyState === WebSocket . OPEN ) {
49
+ wsRef . current . close ( ) ;
50
+ }
51
+
52
+ try {
40
53
let chatModel = localStorage . getItem ( 'chatModel' ) ;
41
54
let chatModelProvider = localStorage . getItem ( 'chatModelProvider' ) ;
42
55
let embeddingModel = localStorage . getItem ( 'embeddingModel' ) ;
@@ -59,7 +72,13 @@ const useSocket = (
59
72
'Content-Type' : 'application/json' ,
60
73
} ,
61
74
} ,
62
- ) . then ( async ( res ) => await res . json ( ) ) ;
75
+ ) . then ( async ( res ) => {
76
+ if ( ! res . ok )
77
+ throw new Error (
78
+ `Failed to fetch models: ${ res . status } ${ res . statusText } ` ,
79
+ ) ;
80
+ return res . json ( ) ;
81
+ } ) ;
63
82
64
83
if (
65
84
! chatModel ||
@@ -202,6 +221,7 @@ const useSocket = (
202
221
wsURL . search = searchParams . toString ( ) ;
203
222
204
223
const ws = new WebSocket ( wsURL . toString ( ) ) ;
224
+ wsRef . current = ws ;
205
225
206
226
const timeoutId = setTimeout ( ( ) => {
207
227
if ( ws . readyState !== 1 ) {
@@ -217,11 +237,16 @@ const useSocket = (
217
237
const interval = setInterval ( ( ) => {
218
238
if ( ws . readyState === 1 ) {
219
239
setIsWSReady ( true ) ;
240
+ setError ( false ) ;
241
+ if ( retryCountRef . current > 0 ) {
242
+ toast . success ( 'Connection restored.' ) ;
243
+ }
244
+ retryCountRef . current = 0 ;
220
245
clearInterval ( interval ) ;
221
246
}
222
247
} , 5 ) ;
223
248
clearTimeout ( timeoutId ) ;
224
- console . log ( '[DEBUG] opened ') ;
249
+ console . debug ( new Date ( ) , 'ws:connected ') ;
225
250
}
226
251
if ( data . type === 'error' ) {
227
252
toast . error ( data . data ) ;
@@ -230,24 +255,68 @@ const useSocket = (
230
255
231
256
ws . onerror = ( ) => {
232
257
clearTimeout ( timeoutId ) ;
233
- setError ( true ) ;
258
+ setIsWSReady ( false ) ;
234
259
toast . error ( 'WebSocket connection error.' ) ;
235
260
} ;
236
261
237
262
ws . onclose = ( ) => {
238
263
clearTimeout ( timeoutId ) ;
239
- setError ( true ) ;
240
- console . log ( '[DEBUG] closed' ) ;
264
+ setIsWSReady ( false ) ;
265
+ console . debug ( new Date ( ) , 'ws:disconnected' ) ;
266
+ if ( ! isCleaningUpRef . current ) {
267
+ toast . error ( 'Connection lost. Attempting to reconnect...' ) ;
268
+ attemptReconnect ( ) ;
269
+ }
241
270
} ;
271
+ } catch ( error ) {
272
+ console . debug ( new Date ( ) , 'ws:error' , error ) ;
273
+ setIsWSReady ( false ) ;
274
+ attemptReconnect ( ) ;
275
+ }
276
+ } ;
242
277
243
- setWs ( ws ) ;
244
- } ;
278
+ const attemptReconnect = ( ) => {
279
+ retryCountRef . current += 1 ;
245
280
246
- connectWs ( ) ;
247
- }
248
- } , [ ws , url , setIsWSReady , setError ] ) ;
281
+ if ( retryCountRef . current > MAX_RETRIES ) {
282
+ console . debug ( new Date ( ) , 'ws:max_retries' ) ;
283
+ setError ( true ) ;
284
+ toast . error (
285
+ 'Unable to connect to server after multiple attempts. Please refresh the page to try again.' ,
286
+ ) ;
287
+ return ;
288
+ }
249
289
250
- return ws ;
290
+ const backoffDelay = getBackoffDelay ( retryCountRef . current ) ;
291
+ console . debug (
292
+ new Date ( ) ,
293
+ `ws:retry attempt=${ retryCountRef . current } /${ MAX_RETRIES } delay=${ backoffDelay } ms` ,
294
+ ) ;
295
+
296
+ if ( reconnectTimeoutRef . current ) {
297
+ clearTimeout ( reconnectTimeoutRef . current ) ;
298
+ }
299
+
300
+ reconnectTimeoutRef . current = setTimeout ( ( ) => {
301
+ connectWs ( ) ;
302
+ } , backoffDelay ) ;
303
+ } ;
304
+
305
+ connectWs ( ) ;
306
+
307
+ return ( ) => {
308
+ if ( reconnectTimeoutRef . current ) {
309
+ clearTimeout ( reconnectTimeoutRef . current ) ;
310
+ }
311
+ if ( wsRef . current ?. readyState === WebSocket . OPEN ) {
312
+ wsRef . current . close ( ) ;
313
+ isCleaningUpRef . current = true ;
314
+ console . debug ( new Date ( ) , 'ws:cleanup' ) ;
315
+ }
316
+ } ;
317
+ } , [ url , setIsWSReady , setError ] ) ;
318
+
319
+ return wsRef . current ;
251
320
} ;
252
321
253
322
const loadMessages = async (
@@ -291,7 +360,7 @@ const loadMessages = async (
291
360
return [ msg . role , msg . content ] ;
292
361
} ) as [ string , string ] [ ] ;
293
362
294
- console . log ( '[DEBUG] messages loaded ') ;
363
+ console . debug ( new Date ( ) , 'app:messages_loaded ') ;
295
364
296
365
document . title = messages [ 0 ] . content ;
297
366
@@ -373,7 +442,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
373
442
return ( ) => {
374
443
if ( ws ?. readyState === 1 ) {
375
444
ws . close ( ) ;
376
- console . log ( '[DEBUG] closed ') ;
445
+ console . debug ( new Date ( ) , 'ws:cleanup ') ;
377
446
}
378
447
} ;
379
448
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -388,12 +457,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
388
457
useEffect ( ( ) => {
389
458
if ( isMessagesLoaded && isWSReady ) {
390
459
setIsReady ( true ) ;
391
- console . log ( '[DEBUG] ready' ) ;
460
+ console . debug ( new Date ( ) , 'app:ready' ) ;
461
+ } else {
462
+ setIsReady ( false ) ;
392
463
}
393
464
} , [ isMessagesLoaded , isWSReady ] ) ;
394
465
395
466
const sendMessage = async ( message : string , messageId ?: string ) => {
396
467
if ( loading ) return ;
468
+ if ( ! ws || ws . readyState !== WebSocket . OPEN ) {
469
+ toast . error ( 'Cannot send message while disconnected' ) ;
470
+ return ;
471
+ }
397
472
398
473
setLoading ( true ) ;
399
474
setMessageAppeared ( false ) ;
@@ -404,7 +479,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
404
479
405
480
messageId = messageId ?? crypto . randomBytes ( 7 ) . toString ( 'hex' ) ;
406
481
407
- ws ? .send (
482
+ ws . send (
408
483
JSON . stringify ( {
409
484
type : 'message' ,
410
485
message : {
@@ -558,7 +633,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
558
633
559
634
return isReady ? (
560
635
notFound ? (
561
- < Error statusCode = { 404 } />
636
+ < NextError statusCode = { 404 } />
562
637
) : (
563
638
< div >
564
639
{ messages . length > 0 ? (
0 commit comments