-
Notifications
You must be signed in to change notification settings - Fork 136
[Bug]:WebSocket Connection Stability & Reconnection Logic #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Isha-upadhyay
wants to merge
3
commits into
AOSSIE-Org:main
Choose a base branch
from
Isha-upadhyay:bug-websocket-stability
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+256
−6
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| .env | ||
| config/config.prod.yml | ||
| config.prod.yml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,12 @@ import React, { | |
| useRef, | ||
| useState, | ||
| } from "react"; | ||
| import { useParams } from "react-router-dom"; | ||
| import { useParams, useNavigate } from "react-router-dom"; | ||
| import { Button } from "../components/ui/button"; | ||
|
|
||
| import JudgmentPopup from "@/components/JudgementPopup"; | ||
| import SpeechTranscripts from "@/components/SpeechTranscripts"; | ||
| import ConnectionStatus from "@/components/ConnectionStatus"; | ||
| import { useUser } from "@/hooks/useUser"; | ||
| import { getAuthToken } from "@/utils/auth"; | ||
| import ReconnectingWebSocket from "reconnecting-websocket"; | ||
|
|
@@ -140,12 +141,13 @@ const BASE_URL = import.meta.env.VITE_BASE_URL || window.location.origin; | |
|
|
||
| const WS_BASE_URL = BASE_URL.replace( | ||
| /^https?/, | ||
| (match) => (match === "https" ? "wss" : "ws") | ||
| (match: string) => (match === "https" ? "wss" : "ws") | ||
| ); | ||
|
|
||
|
|
||
| const OnlineDebateRoom = (): JSX.Element => { | ||
| const { roomId } = useParams<{ roomId: string }>(); | ||
| const navigate = useNavigate(); | ||
| const { user: currentUser } = useUser(); | ||
| const currentUserId = currentUser?.id ?? null; | ||
| useDebateWS(roomId ?? null); | ||
|
|
@@ -243,6 +245,13 @@ const OnlineDebateRoom = (): JSX.Element => { | |
| const retryCountRef = useRef<number>(0); | ||
| const manualRecordingRef = useRef(false); | ||
|
|
||
| // Connection status state | ||
| const [wsConnectionState, setWsConnectionState] = useState<'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error'>('connecting'); | ||
| const [wsRetryCount, setWsRetryCount] = useState(0); | ||
| const [wsLastError, setWsLastError] = useState<string | null>(null); | ||
| const [rtcConnectionState, setRtcConnectionState] = useState<'connected' | 'disconnected' | 'failed'>('disconnected'); | ||
| const WS_MAX_RETRIES = 5; | ||
|
|
||
| const cleanupSpectatorConnection = useCallback((connectionId: string) => { | ||
| const pc = spectatorPCsRef.current.get(connectionId); | ||
| if (pc) { | ||
|
|
@@ -1115,14 +1124,18 @@ const OnlineDebateRoom = (): JSX.Element => { | |
|
|
||
| const rws = new ReconnectingWebSocket(wsUrl, [], { | ||
| connectionTimeout: 4000, | ||
| maxRetries: Infinity, | ||
| maxRetries: WS_MAX_RETRIES, | ||
| maxReconnectionDelay: 10000, | ||
| minReconnectionDelay: 1000, | ||
| reconnectionDelayGrowFactor: 1.3, | ||
| }); | ||
| wsRef.current = rws; | ||
|
|
||
| rws.onopen = () => { | ||
| console.log('[WS] Connected to debate room'); | ||
| setWsConnectionState('connected'); | ||
| setWsRetryCount(0); | ||
| setWsLastError(null); | ||
| rws.send(JSON.stringify({ type: "join", room: roomId })); | ||
| // Wait a bit before fetching participants to ensure room is fully created | ||
| setTimeout(() => { | ||
|
|
@@ -1132,6 +1145,31 @@ const OnlineDebateRoom = (): JSX.Element => { | |
| flushSpectatorOfferQueue(); | ||
| }; | ||
|
|
||
| rws.onerror = (error: any) => { | ||
| console.error('[WS] WebSocket error:', error); | ||
| setWsLastError('WebSocket connection error'); | ||
| }; | ||
|
|
||
| rws.onclose = (event: any) => { | ||
| console.log('[WS] WebSocket closed:', event.code, event.reason); | ||
| const errorMessage = event.code === 1006 ? 'Abnormal closure' : | ||
| event.code === 1008 ? 'Policy violation' : | ||
| event.code === 1011 ? 'Server error' : | ||
| `Connection closed (code: ${event.code})`; | ||
| setWsLastError(errorMessage); | ||
|
|
||
| setWsRetryCount(prev => { | ||
| const newCount = prev + 1; | ||
| if (newCount >= WS_MAX_RETRIES) { | ||
| setWsConnectionState('error'); | ||
| console.log('[Connection] Max retries exceeded'); | ||
| } else { | ||
| setWsConnectionState('reconnecting'); | ||
| console.log(`[Connection] Retrying (${newCount}/${WS_MAX_RETRIES})...`); | ||
| } | ||
| return newCount; | ||
| }); | ||
| }; | ||
| rws.onmessage = async (event) => { | ||
| const data: WSMessage = JSON.parse(event.data); | ||
| switch (data.type) { | ||
|
|
@@ -1385,6 +1423,28 @@ const OnlineDebateRoom = (): JSX.Element => { | |
| setRemoteStream(event.streams[0]); | ||
| }; | ||
|
|
||
| pc.onconnectionstatechange = () => { | ||
| console.log('[RTC] Connection state changed:', pc.connectionState); | ||
| switch (pc.connectionState) { | ||
| case 'connected': | ||
| setRtcConnectionState('connected'); | ||
| break; | ||
| case 'failed': | ||
| setRtcConnectionState('failed'); | ||
| break; | ||
| case 'disconnected': | ||
| setRtcConnectionState('disconnected'); | ||
| break; | ||
| default: | ||
| setRtcConnectionState('disconnected'); | ||
| } | ||
| }; | ||
|
|
||
| pc.oniceconnectionstatechange = () => { | ||
| console.log('[RTC] ICE connection state changed:', pc.iceConnectionState); | ||
| // Additional logging for ICE state changes | ||
| }; | ||
|
|
||
| const getMedia = async () => { | ||
| try { | ||
| const stream = await navigator.mediaDevices.getUserMedia({ | ||
|
|
@@ -1894,6 +1954,38 @@ const OnlineDebateRoom = (): JSX.Element => { | |
| setIsManualRecording(false); | ||
| }, [isManualRecording, stopAudioRecording, stopSpeechRecognition]); | ||
|
|
||
| // Connection management handlers | ||
| const handleReconnect = useCallback(() => { | ||
| console.log('[Connection] Manual reconnect initiated'); | ||
| setWsConnectionState('connecting'); | ||
| setWsRetryCount(0); | ||
| setWsLastError(null); | ||
| // The ReconnectingWebSocket will automatically attempt to reconnect | ||
| if (wsRef.current) { | ||
| wsRef.current.reconnect(); | ||
| } | ||
| }, []); | ||
|
|
||
| const handleLeaveDebate = useCallback(() => { | ||
| console.log('[Connection] Leaving debate room'); | ||
| // Stop local media tracks | ||
| if (localStreamRef.current) { | ||
| localStreamRef.current.getTracks().forEach(track => track.stop()); | ||
| } | ||
| if (audioStream) { | ||
| audioStream.getTracks().forEach(track => track.stop()); | ||
| } | ||
| // Clean up connections | ||
| if (wsRef.current) { | ||
| wsRef.current.close(); | ||
| } | ||
| if (pcRef.current) { | ||
| pcRef.current.close(); | ||
| } | ||
| // Navigate away | ||
| navigate('/game'); | ||
| }, [navigate, audioStream]); | ||
|
Comment on lines
+1969
to
+1987
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing cleanup of spectator peer connections. When leaving the debate, Suggested fix const handleLeaveDebate = useCallback(() => {
console.log('[Connection] Leaving debate room');
// Stop local media tracks
if (localStreamRef.current) {
localStreamRef.current.getTracks().forEach(track => track.stop());
}
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
}
+ // Clean up spectator connections
+ spectatorPCsRef.current.forEach((spectatorPc) => {
+ try {
+ spectatorPc.close();
+ } catch {
+ // Ignore close errors
+ }
+ });
+ spectatorPCsRef.current.clear();
// Clean up connections
if (wsRef.current) {
wsRef.current.close();
}
if (pcRef.current) {
pcRef.current.close();
}
// Navigate away
navigate('/game');
}, [navigate, audioStream]);🤖 Prompt for AI Agents |
||
|
|
||
| useEffect(() => { | ||
| if (!manualRecordingRef.current) { | ||
| return; | ||
|
|
@@ -2092,6 +2184,17 @@ const OnlineDebateRoom = (): JSX.Element => { | |
| // Render UI | ||
| return ( | ||
| <div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-200 p-4"> | ||
| {/* Connection Status Component */} | ||
| <ConnectionStatus | ||
| state={wsConnectionState} | ||
| retryCount={wsRetryCount} | ||
| maxRetries={WS_MAX_RETRIES} | ||
| rtcState={rtcConnectionState} | ||
| lastErrorMessage={wsLastError} | ||
| onReconnect={handleReconnect} | ||
| onLeave={handleLeaveDebate} | ||
| /> | ||
|
|
||
| <div className="w-full max-w-5xl mx-auto py-2"> | ||
| <div className="bg-gradient-to-r from-orange-100 via-white to-orange-100 rounded-xl p-4 text-center"> | ||
| <h1 className="text-3xl font-bold text-gray-900"> | ||
|
|
@@ -2424,10 +2527,8 @@ const OnlineDebateRoom = (): JSX.Element => { | |
| onClick={ | ||
| isManualRecording ? handleStopSpeaking : handleStartSpeaking | ||
| } | ||
| variant={isManualRecording ? "destructive" : "default"} | ||
| size="sm" | ||
| className={`h-8 rounded-md px-3 text-xs ${isManualRecording ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : ''}`} | ||
| disabled={!isManualRecording && !canStartSpeaking} | ||
| className="px-4" | ||
| > | ||
| {isManualRecording ? "Stop Speaking" : "Start Speaking"} | ||
| </Button> | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import React from 'react'; | ||
| import { Button } from './ui/button'; | ||
|
|
||
| type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error'; | ||
| type RTCState = 'connected' | 'disconnected' | 'failed'; | ||
|
|
||
| interface ConnectionStatusProps { | ||
| state: ConnectionState; | ||
| retryCount: number; | ||
| maxRetries: number; | ||
| rtcState: RTCState; | ||
| lastErrorMessage: string | null; | ||
| onReconnect: () => void; | ||
| onLeave: () => void; | ||
| } | ||
|
|
||
| const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ | ||
| state, | ||
| retryCount, | ||
| maxRetries, | ||
| rtcState, | ||
| lastErrorMessage, | ||
| onReconnect, | ||
| onLeave, | ||
| }) => { | ||
| const getStatusColor = () => { | ||
| switch (state) { | ||
| case 'connected': | ||
| return 'text-green-600 bg-green-100 border-green-200'; | ||
| case 'connecting': | ||
| return 'text-blue-600 bg-blue-100 border-blue-200'; | ||
| case 'reconnecting': | ||
| return 'text-amber-600 bg-amber-100 border-amber-200'; | ||
| case 'disconnected': | ||
| case 'error': | ||
| return 'text-red-600 bg-red-100 border-red-200'; | ||
| default: | ||
| return 'text-gray-600 bg-gray-100 border-gray-200'; | ||
| } | ||
| }; | ||
|
|
||
| const getStatusIcon = () => { | ||
| switch (state) { | ||
| case 'connected': | ||
| return '●'; | ||
| case 'connecting': | ||
| return '↻'; | ||
| case 'reconnecting': | ||
| return '↻'; | ||
| case 'disconnected': | ||
| return '⚠'; | ||
| case 'error': | ||
| return '⚠'; | ||
| default: | ||
| return '○'; | ||
| } | ||
| }; | ||
|
|
||
| const getStatusText = () => { | ||
| switch (state) { | ||
| case 'connected': | ||
| return 'Connected'; | ||
| case 'connecting': | ||
| return 'Connecting...'; | ||
| case 'reconnecting': | ||
| return `Reconnecting (${retryCount}/${maxRetries})...`; | ||
| case 'disconnected': | ||
| return 'Disconnected'; | ||
| case 'error': | ||
| return `Connection Error`; | ||
| default: | ||
| return 'Unknown'; | ||
| } | ||
| }; | ||
|
|
||
| const getRTCStatusText = () => { | ||
| switch (rtcState) { | ||
| case 'connected': | ||
| return 'Video/Audio: OK'; | ||
| case 'disconnected': | ||
| return 'Video/Audio: Disconnected'; | ||
| case 'failed': | ||
| return 'Video/Audio: Failed'; | ||
| default: | ||
| return ''; | ||
| } | ||
| }; | ||
|
|
||
| const shouldShowComponent = () => { | ||
| // Show component when not connected or when there's an issue | ||
| return state !== 'connected' || rtcState !== 'connected'; | ||
| }; | ||
|
|
||
| if (!shouldShowComponent()) { | ||
| // Minimal connected indicator | ||
| return ( | ||
| <div className="fixed top-4 right-4 z-50"> | ||
| <div className="flex items-center space-x-2 bg-green-100 border border-green-200 rounded-lg px-3 py-2 shadow-sm"> | ||
| <span className="text-green-600 text-sm font-medium">● Connected</span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="fixed top-4 right-4 z-50 max-w-sm"> | ||
| <div className={`border rounded-lg p-4 shadow-lg ${getStatusColor()}`}> | ||
| <div className="flex items-center space-x-2 mb-2"> | ||
| <span className={`text-lg ${state === 'reconnecting' ? 'animate-spin' : ''}`}> | ||
| {getStatusIcon()} | ||
| </span> | ||
| <span className="font-semibold text-sm">{getStatusText()}</span> | ||
| </div> | ||
|
|
||
| {rtcState !== 'connected' && ( | ||
| <div className="text-xs mb-2 text-gray-700"> | ||
| {getRTCStatusText()} | ||
| </div> | ||
| )} | ||
|
|
||
| {lastErrorMessage && ( | ||
| <div className="text-xs mb-3 text-gray-800 bg-white bg-opacity-50 rounded p-2"> | ||
| <strong>Error:</strong> {lastErrorMessage} | ||
| </div> | ||
| )} | ||
|
|
||
| {(state === 'disconnected' || state === 'error') && ( | ||
| <div className="flex space-x-2"> | ||
| <Button | ||
| onClick={onReconnect} | ||
| className="text-xs px-3 py-1 h-8 rounded-md" | ||
| > | ||
| Reconnect | ||
| </Button> | ||
| <Button | ||
| onClick={onLeave} | ||
| className="text-xs px-3 py-1 h-8 rounded-md bg-red-600 text-white hover:bg-red-700" | ||
| > | ||
| Leave Debate | ||
| </Button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ConnectionStatus; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the OnlineDebateRoom.tsx file to understand the context wc -l frontend/src/Pages/OnlineDebateRoom.tsxRepository: AOSSIE-Org/DebateAI
Length of output: 107
🏁 Script executed:
Repository: AOSSIE-Org/DebateAI
Length of output: 1710
🏁 Script executed:
Repository: AOSSIE-Org/DebateAI
Length of output: 1740
🏁 Script executed:
Repository: AOSSIE-Org/DebateAI
Length of output: 333
🏁 Script executed:
Repository: AOSSIE-Org/DebateAI
Length of output: 1584
🏁 Script executed:
Repository: AOSSIE-Org/DebateAI
Length of output: 629
🏁 Script executed:
Repository: AOSSIE-Org/DebateAI
Length of output: 1142
🌐 Web query:
reconnecting-websocket 4.4.0 npm onclose retry behavior maxRetries💡 Result:
Short answer: on an onclose the library will automatically schedule reconnection attempts until options.maxRetries is reached (default: Infinity). retryCount increments on each attempt; if you set maxRetries to a finite number and it is exhausted the library stops retrying and emits an error indicating the host is down (historical docs show an EHOSTDOWN error). [1][2]
Sources:
Verify potential off-by-one error in retry count logic.
The
onclosehandler manually incrementswsRetryCountto mirrorReconnectingWebSocket's internal retry behavior. While the manual counter is tied to the library's events (not a true "double counting" issue), there's a potential synchronization concern:The condition
if (newCount >= WS_MAX_RETRIES)sets state to 'error' when the counter reaches 5. However, the exact semantics ofmaxRetries: 5in ReconnectingWebSocket v4.4.0 matters—it could mean either:If the library interprets
maxRetriesas the second case, the manual counter will trigger the 'error' state one attempt early, creating a state mismatch.Clarify the retry counting logic to ensure
wsRetryCountand the library's internal retry limit stay synchronized.🤖 Prompt for AI Agents