FOUNDRY
C8 Platform
← 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