← Tasks
[HK-01] useVoiceRoom — room connection state machine (IDLE→CONNECTING→CONNECTED→...)
completedcode_genP0
Description
## HK-01: useVoiceRoom — room connection state machine (IDLE→CONNECTING→CONNECTED→...)
Section: Hooks | Milestone: M1 | Owner: AG Lu
### Failing Tests
- **hook_file** (file_exists): File `c8-audio/hooks/useVoiceRoom.ts` must exist
- **state_machine** (grep): Pattern `IDLE|CONNECTING|CONNECTED|RECONNECTING|DISCONNECTED` in `c8-audio/hooks/useVoiceRoom.ts` (need 3+ matches)
### Acceptance Criteria
- File `c8-audio/hooks/useVoiceRoom.ts` must exist
- File matching `c8-audio/hooks/useVoiceRoom.ts` must contain pattern `IDLE|CONNECTING|CONNECTED|RECONNECTING|DISCONNECTED`
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- LIB-03: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
22d ago
Completed
19d ago
Quality
Overall
79%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 34,
"estimated_usd": 0.064677,
"output_tokens": 4305
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 45265,
"response_text": "I'll help you design a room connection state machine for the `useVoiceRoom` hook. This will manage the various states of connecting to and participating in a voice room.\n\n```typescript\n// types/voiceRoom.ts\nexport enum VoiceRoomState {\n IDLE = 'idle',\n CONNECTING = 'connecting',\n CONNECTED = 'connected',\n RECONNECTING = 'reconnecting',\n DISCONNECTING = 'disconnecting',\n DISCONNECTED = 'disconnected',\n ERROR = 'error'\n}\n\nexport enum VoiceRoomAction {\n CONNECT = 'connect',\n CONNECT_SUCCESS = 'connect_success',\n CONNECT_FAILURE = 'connect_failure',\n DISCONNECT = 'disconnect',\n DISCONNECT_SUCCESS = 'disconnect_success',\n CONNECTION_LOST = 'connection_lost',\n RECONNECT_ATTEMPT = 'reconnect_attempt',\n RECONNECT_SUCCESS = 'reconnect_success',\n RECONNECT_FAILURE = 'reconnect_failure',\n RESET = 'reset'\n}\n\nexport interface VoiceRoomContext {\n roomId?: string;\n error?: Error;\n retryCount: number;\n maxRetries: number;\n lastConnectedAt?: Date;\n participants: Participant[];\n isHost: boolean;\n}\n\nexport interface Participant {\n id: string;\n name: string;\n isMuted: boolean;\n isSpeaking: boolean;\n joinedAt: Date;\n}\n\nexport interface VoiceRoomEvent {\n type: VoiceRoomAction;\n payload?: {\n roomId?: string;\n error?: Error;\n participants?: Participant[];\n [key: string]: any;\n };\n}\n```\n\n```typescript\n// hooks/useVoiceRoom.ts\nimport { useReducer, useCallback, useEffect, useRef } from 'react';\nimport { VoiceRoomState, VoiceRoomAction, VoiceRoomContext, VoiceRoomEvent } from '../types/voiceRoom';\n\nconst initialContext: VoiceRoomContext = {\n retryCount: 0,\n maxRetries: 3,\n participants: [],\n isHost: false\n};\n\n// State machine reducer\nfunction voiceRoomReducer(\n state: { current: VoiceRoomState; context: VoiceRoomContext },\n event: VoiceRoomEvent\n): { current: VoiceRoomState; context: VoiceRoomContext } {\n const { type, payload } = event;\n const { current, context } = state;\n\n switch (current) {\n case VoiceRoomState.IDLE:\n switch (type) {\n case VoiceRoomAction.CONNECT:\n return {\n current: VoiceRoomState.CONNECTING,\n context: {\n ...context,\n roomId: payload?.roomId,\n error: undefined,\n retryCount: 0\n }\n };\n default:\n return state;\n }\n\n case VoiceRoomState.CONNECTING:\n switch (type) {\n case VoiceRoomAction.CONNECT_SUCCESS:\n return {\n current: VoiceRoomState.CONNECTED,\n context: {\n ...context,\n error: undefined,\n retryCount: 0,\n lastConnectedAt: new Date(),\n participants: payload?.participants || [],\n isHost: payload?.isHost || false\n }\n };\n case VoiceRoomAction.CONNECT_FAILURE:\n return {\n current: VoiceRoomState.ERROR,\n context: {\n ...context,\n error: payload?.error\n }\n };\n case VoiceRoomAction.DISCONNECT:\n return {\n current: VoiceRoomState.DISCONNECTING,\n context\n };\n default:\n return state;\n }\n\n case VoiceRoomState.CONNECTED:\n switch (type) {\n case VoiceRoomAction.DISCONNECT:\n return {\n current: VoiceRoomState.DISCONNECTING,\n context\n };\n case VoiceRoomAction.CONNECTION_LOST:\n return {\n current: VoiceRoomState.RECONNECTING,\n context: {\n ...context,\n retryCount: 0\n }\n };\n default:\n return state;\n }\n\n case VoiceRoomState.RECONNECTING:\n switch (type) {\n case VoiceRoomAction.RECONNECT_SUCCESS:\n return {\n current: VoiceRoomState.CONNECTED,\n context: {\n ...context,\n error: undefined,\n retryCount: 0,\n lastConnectedAt: new Date()\n }\n };\n case VoiceRoomAction.RECONNECT_FAILURE:\n if (context.retryCount >= context.maxRetries) {\n return {\n current: VoiceRoomState.ERROR,\n context: {\n ...context,\n error: payload?.error || new Error('Max reconnection attempts exceeded')\n }\n };\n }\n return {\n current: VoiceRoomState.RECONNECTING,\n context: {\n ...context,\n retryCount: context.retryCount + 1,\n error: payload?.error\n }\n };\n case VoiceRoomAction.DISCONNECT:\n return {\n current: VoiceRoomState.DISCONNECTING,\n context\n };\n default:\n return state;\n }\n\n case VoiceRoomState.DISCONNECTING:\n switch (type) {\n case VoiceRoomAction.DISCONNECT_SUCCESS:\n return {\n current: VoiceRoomState.DISCONNECTED,\n context: {\n ...initialContext,\n maxRetries: context.maxRetries\n }\n };\n case VoiceRoomAction.CONNECT_FAILURE:\n return {\n current: VoiceRoomState.DISCONNECTED,\n context: {\n ...initialContext,\n maxRetries: context.maxRetries,\n error: payload?.error\n }\n };\n default:\n return state;\n }\n\n case VoiceRoomState.DISCONNECTED:\n switch (type) {\n case VoiceRoomAction.CONNECT:\n return {\n current: VoiceRoomState.CONNECTING,\n context: {\n ...initialContext,\n roomId: payload?.roomId,\n maxRetries: context.maxRetries\n }\n };\n case VoiceRoomAction.RESET:\n return {\n current: VoiceRoomState.IDLE,\n context: {\n ...initialContext,\n maxRetries: context.maxRetries\n }\n };\n default:\n return state;\n }\n\n case VoiceRoomState.ERROR:\n switch (type) {\n case VoiceRoomAction.CONNECT:\n return {\n current: VoiceRoomState.CONNECTING,\n context: {\n ...context,\n roomId: payload?.roomId,\n error: undefined,\n retryCount: 0\n }\n };\n case VoiceRoomAction.RESET:\n return {\n current: VoiceRoomState.IDLE,\n context: {\n ...initialContext,\n maxRetries: context.maxRetries\n }\n };\n default:\n return state;\n }\n\n default:\n return state;\n }\n}\n\nexport function useVoiceRoom() {\n const [state, dispatch] = useReducer(voiceRoomReducer, {\n current: VoiceRoomState.IDLE,\n context: initialContext\n });\n\n const reconnectTimeoutRef = useRef<NodeJS.Timeout>();\n const connectionRef = useRef<WebSocket | null>(null);\n\n // Clean up timeouts on unmount\n useEffect(() => {\n return () => {\n if (reconnectTimeoutRef.current) {\n clearTimeout(reconnectTimeoutRef.current);\n }\n };\n }, []);\n\n // Auto-reconnect logic\n useEffect(() => {\n if (state.current === VoiceRoomState.RECONNECTING) {\n const delay = Math.min(1000 * Math.pow(2, state.context.retryCount), 10000); // Exponential backoff\n \n reconnectTimeoutRef.current = setTimeout(() => {\n if (state.context.roomId) {\n attemptReconnection(state.context.roomId);\n }\n }, delay);\n }\n\n return () => {\n if (reconnectTimeoutRef.current) {\n clearTimeout(reconnectTimeoutRef.current);\n }\n };\n }, [state.current, state.context.retryCount]);\n\n const connect = useCallback(async (roomId: string) => {\n dispatch({ type: VoiceRoomAction.CONNECT, payload: { roomId } });\n \n try {\n // Simulate connection logic - replace with actual implementation\n const ws = new WebSocket(`ws://your-voice-server.com/rooms/${roomId}`);\n \n ws.onopen = () => {\n connectionRef.current = ws;\n dispatch({ \n type: VoiceRoomAction.CONNECT_SUCCESS, \n payload: { \n participants: [],\n isHost: false \n } \n });\n };\n\n ws.onclose = () => {\n if (state.current === VoiceRoomState.CONNECTED) {\n dispatch({ type: VoiceRoomAction.CONNECTION_LOST });\n }\n };\n\n ws.onerror = (error) => {\n dispatch({ \n type: VoiceRoomAction.CONNECT_FAILURE, \n payload: { error: new Error('Connection failed') } \n });\n };\n\n } catch (error) {\n dispatch({ \n type: VoiceRoomAction.CONNECT_FAILURE, \n payload: { error: error as Error } \n });\n }\n }, [state.current]);\n\n const disconnect = useCallback(async () => {\n dispatch({ type: VoiceRoomAction.DISCONNECT });\n \n try {\n if (connectionRef.current) {\n connectionRef.current.close();\n connectionRef.current = null;\n }\n \n // Cleanup logic here\n dispatch({ type: VoiceRoomAction.DISCONNECT_SUCCESS });\n } catch (error) {\n dispatch({ \n type: VoiceRoomAction.CONNECT_FAILURE, \n payload: { error: error as Error } \n });\n }\n }, []);\n\n const attemptReconnection = useCallback(async (roomId: string) => {\n try {\n // Attempt to reconnect\n const ws = new WebSocket(`ws://your-voice-server.com/rooms/${roomId}`);\n \n ws.onopen = () => {\n connectionRef.current = ws;\n dispatch({ type: VoiceRoomAction.RECONNECT_SUCCESS });\n };\n\n ws.onerror = () => {\n dispatch({ \n type: VoiceRoomAction.RECONNECT_FAILURE,\n payload: { error: new Error('Reconnection failed') }\n });\n };\n\n } catch (error) {\n dispatch({ \n type: VoiceRoomAction.RECONNECT_FAILURE,\n payload: { error: error as Error }\n });\n }\n }, []);\n\n const reset = useCallback(() => {\n dispatch({ type: VoiceRoomAction.RESET });\n }, []);\n\n // Derived state helpers\n const isConnecting = state.current === VoiceRoomState.CONNECTING;\n const isConnected = state.current === VoiceRoomState.CONNECTED;\n const isReconnecting = state.current === VoiceRoomState.RECONNECTING;\n const isDisconnecting = state.current === VoiceRoomState.DISCONNECTING;\n const hasError = state.current === VoiceRoomState.ERROR;\n const canConnect = [VoiceRoomState.IDLE, VoiceRoomState.DISCONNECTED, VoiceRoomState.ERROR].includes(state.current);\n\n return {\n // State\n state: state.current,\n context: state.context,\n \n // Derived state\n isConnecting,\n isConnected,\n isReconnecting,\n isDisconnecting,\n hasError,\n canConnect,\n \n // Actions\n connect,\n disconnect,\n reset,\n \n // Data\n participants: state.context.participants,\n roomId: state.context.roomId,\n error: state.context.error,\n isHost: state.context.isHost,\n retryCount: state.context.retryCount\n };\n}\n```\n\n```typescript\n// Example usage in a component\nimport React from 'react';\nimport { useVoiceRoom } from '../hooks/useVoiceRoom';\nimport { VoiceRoomState } from '../types/voiceRoom';\n\nexport function VoiceRoomComponent() {\n const {\n state,\n isConnecting,\n isConnected,\n isReconnecting,\n hasError,\n canConnect,\n connect,\n disconnect,\n reset,\n participants,\n error,\n retryCount\n } = useVoiceRoom();\n\n const handleConnect = () => {\n connect('room-123');\n };\n\n const renderConnectionStatus = () => {\n switch (state) {\n case VoiceRoomState.IDLE:\n return <span className=\"text-gray-500\">Ready to connect</span>;\n case VoiceRoomState.CONNECTING:\n return <span className=\"text-blue-500\">Connecting...</span>;\n case VoiceRoomState.CONNECTED:\n return <span className=\"text-green-500\">Connected</span>;\n case VoiceRoomState.RECONNECTING:\n return (\n <span className=\"text-yellow-500\">\n Reconnecting... (attempt {retryCount + 1})\n </span>\n );\n case VoiceRoomState.DISCONNECTING:\n return <span className=\"text-gray-500\">Disconnecting...</span>;\n case VoiceRoomState.DISCONNECTED:\n return <span className=\"text-gray-500\">Disconnected</span>;\n case VoiceRoomState.ERROR:\n return <span className=\"text-red-500\">Connection error</span>;\n default:\n return null;\n }\n };\n\n return (\n <div className=\"voice-room-panel\">\n <div className=\"status-bar\">\n {renderConnectionStatus()}\n {hasError && error && (\n <div className=\"error-message text-red-500\">\n {error.message}\n </div>\n )}\n </div>\n\n <div className=\"controls\">\n {canConnect && (\n <button \n onClick={handleConnect}\n disabled={isConnecting}\n className=\"btn-primary\"\n >\n {isConnecting ? 'Connecting...' : 'Join Room'}\n </button>\n )}\n \n {isConnected && (\n <button \n onClick={disconnect}\n className=\"btn-secondary\"\n >\n Leave Room\n </button>\n )}\n \n {hasError && (\n <button \n onClick={reset}\n className=\"btn-secondary\"\n >\n Reset\n </button>\n )}\n </div>\n\n {isConnected && (\n <div className=\"participants\">\n <h3>Participants ({participants.length})</h3>\n {participants.map(participant => (\n <div key={participant.id} className=\"participant\">\n <span>{participant.name}</span>\n {participant.isMuted && <span className=\"muted\">🔇</span>}\n {participant.isSpeaking && <span className=\"speaking\">🗣️</span>}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n```\n\nThis state machine implementation provides:\n\n## Key Features:\n\n1. **Clear State Transitions**: IDLE → CONNECTING → CONNECTED → DISCONNECTING → DISCONNECTED\n2. **Error Handling**: Dedicated ERROR state with recovery options\n3. **Auto-Reconnection**: Exponential backoff strategy for connection recovery\n4. **Type Safety**: Full TypeScript support with proper typing\n5. **Cleanup**: Proper resource cleanup and timeout management\n\n## State Flow:\n\n- **IDLE**: Initial state, ready to connect\n- **CONNECTING**: Attempting initial connection\n- **CONNECTED**: Successfully connected and active\n- **RECONNECTING**: Attempting to restore lost connection\n- **DISCONNECTING**: Gracefully closing connection\n- **DISCONNECTED**: Clean disconnection state\n- **ERROR**: Error state with recovery options\n\n## Benefits:\n\n- Predictable state management\n- Automatic retry logic\n- Clean error handling\n- Easy to test and debug\n- Extensible for additional features\n\nThe hook provides a clean API for components while managing all the complex connection logic internally."
}Task ID: db8a1915-8cff-4c46-b5e6-3985979c2ea0