All files / web/src/hooks useGameBreakRoom.ts

0% Statements 0/174
0% Branches 0/1
0% Functions 0/1
0% Lines 0/174

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175                                                                                                                                                                                                                                                                                                                                                             
'use client'

import { useCallback, useEffect, useRef, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { roomKeys, useCreateRoom, useLeaveRoom, useSetRoomGame, type RoomData } from './useRoomData'

export interface UseGameBreakRoomOptions {
  studentName: string
  enabled: boolean
  /**
   * Pre-configured game settings to apply when selecting a game.
   * Nested by game name: { 'memory-quiz': { selectedCount: 5 } }
   */
  gameConfig?: Record<string, Record<string, unknown>>
  onRoomReady?: (room: RoomData) => void
  onError?: (error: Error) => void
}

export interface UseGameBreakRoomResult {
  room: RoomData | null
  isCreating: boolean
  isSettingGame: boolean
  error: Error | null
  /**
   * Select a game for this room.
   * If gameConfig was provided in options and contains settings for this game,
   * those settings will be applied.
   */
  selectGame: (gameName: string, configOverride?: Record<string, unknown>) => Promise<void>
  cleanup: () => Promise<void>
}

export function useGameBreakRoom({
  studentName,
  enabled,
  gameConfig,
  onRoomReady,
  onError,
}: UseGameBreakRoomOptions): UseGameBreakRoomResult {
  const queryClient = useQueryClient()
  const [room, setRoom] = useState<RoomData | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const roomIdRef = useRef<string | null>(null)
  const isCleaningUpRef = useRef(false)
  const hasStartedRef = useRef(false)

  const onRoomReadyRef = useRef(onRoomReady)
  const onErrorRef = useRef(onError)
  onRoomReadyRef.current = onRoomReady
  onErrorRef.current = onError

  const createRoom = useCreateRoom()
  const leaveRoom = useLeaveRoom()
  const setRoomGame = useSetRoomGame()

  const createRoomRef = useRef(createRoom)
  const leaveRoomRef = useRef(leaveRoom)
  createRoomRef.current = createRoom
  leaveRoomRef.current = leaveRoom

  // Track if room creation is in progress to prevent duplicates
  const isCreatingRef = useRef(false)

  useEffect(() => {
    if (!enabled) {
      hasStartedRef.current = false
      return
    }

    // Skip if already started or already have a room
    if (
      hasStartedRef.current ||
      roomIdRef.current ||
      isCleaningUpRef.current ||
      isCreatingRef.current
    ) {
      return
    }
    hasStartedRef.current = true
    isCreatingRef.current = true

    async function initRoom() {
      try {
        const result = await createRoomRef.current.mutateAsync({
          name: `${studentName}'s Game Break`,
          gameName: null,
          accessMode: 'open',
        })
        const newRoom = result.room

        // Always set the room - don't check mounted flag during Strict Mode
        // The cleanup() function handles proper teardown when truly unmounting
        if (!roomIdRef.current) {
          roomIdRef.current = newRoom.id
          setRoom(newRoom)
          onRoomReadyRef.current?.(newRoom)
        } else {
          // Another room was already set (shouldn't happen with guards), leave this one
          console.log('[useGameBreakRoom] Room already exists, leaving duplicate')
          await leaveRoomRef.current.mutateAsync(newRoom.id).catch(() => {})
        }
      } catch (err) {
        const error = err instanceof Error ? err : new Error(String(err))
        setError(error)
        onErrorRef.current?.(error)
      } finally {
        isCreatingRef.current = false
      }
    }

    initRoom()

    // No cleanup needed here - cleanup() handles room teardown
  }, [enabled, studentName])

  const selectGame = useCallback(
    async (gameName: string, configOverride?: Record<string, unknown>) => {
      if (!room) {
        throw new Error('No room available')
      }

      // Merge config sources: gameConfig[gameName] from options + configOverride
      const baseConfig = gameConfig?.[gameName]
      const mergedConfig =
        baseConfig || configOverride ? { ...baseConfig, ...configOverride } : undefined

      await setRoomGame.mutateAsync({
        roomId: room.id,
        gameName,
        gameConfig: mergedConfig ? { [gameName]: mergedConfig } : undefined,
      })

      setRoom((prev) =>
        prev
          ? {
              ...prev,
              gameName,
              gameConfig: mergedConfig ? { [gameName]: mergedConfig } : prev.gameConfig,
            }
          : null
      )
    },
    [room, setRoomGame, gameConfig]
  )

  const cleanup = useCallback(async () => {
    if (isCleaningUpRef.current) return
    isCleaningUpRef.current = true

    const roomId = roomIdRef.current
    if (roomId) {
      try {
        await leaveRoomRef.current.mutateAsync(roomId)
      } catch {
        // Intentionally swallow cleanup errors
      }
      roomIdRef.current = null
      queryClient.setQueryData(roomKeys.current(), null)
    }

    setRoom(null)
    hasStartedRef.current = false
    isCleaningUpRef.current = false
  }, [queryClient])

  return {
    room,
    isCreating: createRoom.isPending,
    isSettingGame: setRoomGame.isPending,
    error,
    selectGame,
    cleanup,
  }
}