Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.env
config/config.prod.yml
config.prod.yml
113 changes: 107 additions & 6 deletions frontend/src/Pages/OnlineDebateRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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;
});
};
Comment on lines +1153 to +1172
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the OnlineDebateRoom.tsx file to understand the context
wc -l frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 107


🏁 Script executed:

# Look for where ReconnectingWebSocket is instantiated and how it's configured
rg "ReconnectingWebSocket" frontend/src/Pages/OnlineDebateRoom.tsx -A 5 -B 5

Repository: AOSSIE-Org/DebateAI

Length of output: 1710


🏁 Script executed:

# Check package.json to understand the exact version of ReconnectingWebSocket
rg "reconnecting-websocket|ReconnectingWebSocket" . --type json -A 2 -B 2

Repository: AOSSIE-Org/DebateAI

Length of output: 1740


🏁 Script executed:

# Look for wsRetryCount usage throughout the file to understand state management
rg "wsRetryCount|wsConnectionState" frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 333


🏁 Script executed:

# Let's look at the specific code around lines 1153-1172 where onclose is defined
sed -n '1140,1180p' frontend/src/Pages/OnlineDebateRoom.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 1584


🏁 Script executed:

# Check if there are other handlers (onopen, onerror, onmessage) to understand full behavior
rg "rws\.on(open|close|error|message)" frontend/src/Pages/OnlineDebateRoom.tsx -A 2

Repository: AOSSIE-Org/DebateAI

Length of output: 629


🏁 Script executed:

# Look for WS_MAX_RETRIES definition
rg "WS_MAX_RETRIES" frontend/src/Pages/OnlineDebateRoom.tsx -B 2 -A 2

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:

  • reconnecting-websocket (v4.4.0) README / npm (default options include maxRetries: Infinity). [1]
  • pladaria GitHub README (reconnect behavior, maxRetries semantics; docs/examples note emission of EHOSTDOWN when retries exhausted in earlier releases). [2]

Verify potential off-by-one error in retry count logic.

The onclose handler manually increments wsRetryCount to mirror ReconnectingWebSocket'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 of maxRetries: 5 in ReconnectingWebSocket v4.4.0 matters—it could mean either:

  • 5 total connection attempts (1 initial + 4 retries)
  • 5 retries after the initial attempt (1 initial + 5 retries = 6 total)

If the library interprets maxRetries as 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 wsRetryCount and the library's internal retry limit stay synchronized.

🤖 Prompt for AI Agents
In @frontend/src/Pages/OnlineDebateRoom.tsx around lines 1153 - 1172, The
onclose handler's manual retry counter (setWsRetryCount) can be off-by-one
relative to ReconnectingWebSocket's maxRetries semantics—inspect how
ReconnectingWebSocket v4.4.0 defines maxRetries (whether it's total attempts or
retries after the initial attempt) and adjust the check in rws.onclose
accordingly: either change the comparison from if (newCount >= WS_MAX_RETRIES)
to if (newCount > WS_MAX_RETRIES) or compare against WS_MAX_RETRIES + 1 so
wsRetryCount semantics match the library, or better yet read the library's
actual retry/attempt property (e.g., rws.retryCount or rws.reconnectAttempts if
available) and base the error transition (setting setWsConnectionState('error'))
on that instead of the manual counter to ensure synchronization.

rws.onmessage = async (event) => {
const data: WSMessage = JSON.parse(event.data);
switch (data.type) {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing cleanup of spectator peer connections.

When leaving the debate, spectatorPCsRef connections are not closed. This could leave orphaned connections.

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
In @frontend/src/Pages/OnlineDebateRoom.tsx around lines 1969 - 1987,
handleLeaveDebate fails to close spectator peer connections stored in
spectatorPCsRef; update the handleLeaveDebate callback to iterate
spectatorPCsRef.current (handle Map/object/array shape used in your code), call
.close() on each RTCPeerConnection, remove any event listeners if attached, and
then clear or reset spectatorPCsRef.current to an empty container; keep existing
cleanup for localStreamRef, audioStream, wsRef, and pcRef and ensure you
reference spectatorPCsRef inside the same callback so all spectator connections
are closed before navigate('/game').


useEffect(() => {
if (!manualRecordingRef.current) {
return;
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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>
Expand Down
148 changes: 148 additions & 0 deletions frontend/src/components/ConnectionStatus.tsx
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;