Merge pull request 'contextProvider' (#101) from Backend_Fix into main

Reviewed-on: #101
This commit was merged in pull request #101.
This commit is contained in:
2025-11-17 23:13:06 +00:00
7 changed files with 493 additions and 68 deletions
+4 -11
View File
@@ -14,6 +14,7 @@ import gameRouter from './routers/gameRouter';
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
import { WebSocketService } from '../Application/Services/WebSocketService';
import { GameWebSocketService } from '../Application/Services/GameWebSocketService';
import { container } from '../Application/Services/DIContainer';
import { GameRepository } from '../Infrastructure/Repository/GameRepository';
import { UserRepository } from '../Infrastructure/Repository/UserRepository';
import { RedisService } from '../Application/Services/RedisService';
@@ -183,17 +184,9 @@ AppDataSource.initialize()
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
});
// Initialize Game WebSocket service for /game namespace
const gameRepository = new GameRepository();
const userRepository = new UserRepository();
const redisService = RedisService.getInstance();
gameWebSocketService = new GameWebSocketService(
webSocketService['io'], // Access the io property directly
gameRepository,
userRepository,
redisService
);
// Initialize Game WebSocket service for /game namespace via DIContainer
container.setSocketIO(webSocketService['io']);
gameWebSocketService = container.gameWebSocketService;
logStartup('Game WebSocket service initialized for /game namespace');
})
.catch((error) => {
@@ -226,21 +226,43 @@ export class StartGamePlayCommandHandler {
private async notifyGameStart(game: GameAggregate): Promise<void> {
try {
// Note: WebSocket notifications will be handled when WebSocket service is available
// For now, just log the game start
logOther('Game start notifications prepared', {
// Get board data from Redis
const redisKey = `game_board_${game.id}`;
const boardDataStr = await this.redisService.get(redisKey);
if (!boardDataStr) {
logError('Board data not found in Redis during game start notification', new Error('Missing board data'));
return;
}
const boardData: BoardData = JSON.parse(boardDataStr);
// Get turn sequence from Redis
const gamePlayData = await this.getGamePlayFromRedis(game.id);
if (!gamePlayData) {
logError('Game play data not found in Redis', new Error('Missing game play data'));
return;
}
// Get WebSocket service from DIContainer and broadcast game start
const gameWebSocketService = DIContainer.getInstance().gameWebSocketService;
await gameWebSocketService.broadcastGameStart(
game.gamecode,
boardData,
gamePlayData.turnSequence,
game
);
logOther('Game start notifications sent via WebSocket', {
gameId: game.id,
gameCode: game.gamecode,
playerCount: game.players.length,
websocketRoom: `game_${game.gamecode}`
websocketRoom: `game_${game.gamecode}`,
firstPlayer: gamePlayData.turnSequence[0]
});
// TODO: Implement WebSocket notifications when service is properly integrated
// wsService.notifyGameStart(game.gamecode, game.players);
// wsService.broadcastGameStateUpdate(game.gamecode, gameStateData);
} catch (error) {
logError('Failed to prepare game start notifications', error instanceof Error ? error : new Error(String(error)));
logError('Failed to send game start notifications', error instanceof Error ? error : new Error(String(error)));
// Don't throw error here - notification failure shouldn't prevent game start
}
}
@@ -68,6 +68,8 @@ import { RedisService } from './RedisService';
import { GameService } from '../Game/GameService';
import { BoardGenerationService } from '../Game/BoardGenerationService';
import { GenerateBoardCommandHandler } from '../Game/commands/GenerateBoardCommandHandler';
import { GameWebSocketService } from './GameWebSocketService';
import type { Server as SocketIOServer } from 'socket.io';
/**
* Central Dependency Injection Container
@@ -96,6 +98,8 @@ export class DIContainer {
private _fieldEffectService: FieldEffectService | null = null;
private _gameService: GameService | null = null;
private _boardGenerationService: BoardGenerationService | null = null;
private _gameWebSocketService: GameWebSocketService | null = null;
private _socketIOInstance: SocketIOServer | null = null;
// Command Handlers
private _createUserCommandHandler: CreateUserCommandHandler | null = null;
@@ -272,6 +276,30 @@ export class DIContainer {
return this._boardGenerationService;
}
/**
* Set the Socket.IO instance (must be called before accessing gameWebSocketService)
*/
public setSocketIO(io: SocketIOServer): void {
this._socketIOInstance = io;
// Reset gameWebSocketService so it gets recreated with new IO instance
this._gameWebSocketService = null;
}
public get gameWebSocketService(): GameWebSocketService {
if (!this._gameWebSocketService) {
if (!this._socketIOInstance) {
throw new Error('Socket.IO instance must be set before accessing gameWebSocketService. Call setSocketIO() first.');
}
this._gameWebSocketService = new GameWebSocketService(
this._socketIOInstance,
this.gameRepository as any, // Cast to concrete type
this.userRepository as any, // Cast to concrete type
RedisService.getInstance()
);
}
return this._gameWebSocketService;
}
// Command Handler getters
public get createUserCommandHandler(): CreateUserCommandHandler {
if (!this._createUserCommandHandler) {
+28 -25
View File
@@ -24,6 +24,7 @@ import VerifyEmailPage from "./pages/Auth/VerifyEmailPage"
import ChooseDeck from "./pages/Game/ChooseDeck"
import PlayerSetup from "./pages/Game/PlayerSetup"
import GameModalsDemo from "./pages/Game/GameModalsDemo"
import { GameWebSocketProvider } from "./contexts/GameWebSocketContext"
function App() {
const [isMobile, setIsMobile] = useState(false)
@@ -53,31 +54,33 @@ function App() {
return (
<>
<Router>
<Routes>
<Route path={ROUTES.VERIFY_EMAIL} element={<VerifyEmailPage />} />
<Route path={ROUTES.ABOUT} element={<About />} />
<Route path={ROUTES.LOBBY} element={<Lobby />} />
<Route path={ROUTES.REGISTER} element={<AuthRegister />} />
<Route path={ROUTES.LOGIN} element={<AuthLogin />} />
<Route path={ROUTES.FORGOT_PASSWORD} element={<ForgotPassword />} />
<Route path={ROUTES.RESET_PASSWORD} element={<ResetPassword />} />
<Route path={ROUTES.PROFILE} element={<ProfileCard />} />
<Route path={ROUTES.TEST} element={<Test />} />
<Route path={ROUTES.ROOT} element={<Landingpage />} />
<Route path={ROUTES.HOME} element={<Home />} />
<Route path={ROUTES.DECKS} element={<DeckManagerPage />} />
<Route path={ROUTES.DECK_DETAILS} element={<Card_display />} />
<Route path={ROUTES.DECK_CREATOR} element={<DeckCreator />} />
<Route path={ROUTES.DECK_CREATOR_EDIT} element={<DeckCreator />} />
<Route path={ROUTES.GAME} element={<GameScreen />} />
<Route path={ROUTES.GAME_TEST} element={<GameTest />} />
{/* <Route path={ROUTES.CONTACTS} element={<CompanyHub />} /> */}
<Route path={ROUTES.REPORTS} element={<Reports />} />
<Route path={ROUTES.CHOOSE_DECK} element={<ChooseDeck />} />
<Route path={ROUTES.PLAYER_SETUP} element={<PlayerSetup />} />
</Routes>
</Router>
<GameWebSocketProvider>
<Router>
<Routes>
<Route path={ROUTES.VERIFY_EMAIL} element={<VerifyEmailPage />} />
<Route path={ROUTES.ABOUT} element={<About />} />
<Route path={ROUTES.LOBBY} element={<Lobby />} />
<Route path={ROUTES.REGISTER} element={<AuthRegister />} />
<Route path={ROUTES.LOGIN} element={<AuthLogin />} />
<Route path={ROUTES.FORGOT_PASSWORD} element={<ForgotPassword />} />
<Route path={ROUTES.RESET_PASSWORD} element={<ResetPassword />} />
<Route path={ROUTES.PROFILE} element={<ProfileCard />} />
<Route path={ROUTES.TEST} element={<Test />} />
<Route path={ROUTES.ROOT} element={<Landingpage />} />
<Route path={ROUTES.HOME} element={<Home />} />
<Route path={ROUTES.DECKS} element={<DeckManagerPage />} />
<Route path={ROUTES.DECK_DETAILS} element={<Card_display />} />
<Route path={ROUTES.DECK_CREATOR} element={<DeckCreator />} />
<Route path={ROUTES.DECK_CREATOR_EDIT} element={<DeckCreator />} />
<Route path={ROUTES.GAME} element={<GameScreen />} />
<Route path={ROUTES.GAME_TEST} element={<GameTest />} />
{/* <Route path={ROUTES.CONTACTS} element={<CompanyHub />} /> */}
<Route path={ROUTES.REPORTS} element={<Reports />} />
<Route path={ROUTES.CHOOSE_DECK} element={<ChooseDeck />} />
<Route path={ROUTES.PLAYER_SETUP} element={<PlayerSetup />} />
</Routes>
</Router>
</GameWebSocketProvider>
{/* ✅ Toastify Container */}
<ToastConfig />
@@ -0,0 +1,371 @@
import React, { createContext, useContext, useRef, useState, useCallback, useMemo, useEffect } from 'react';
import { io } from 'socket.io-client';
import { API_CONFIG } from '../api/userApi';
const GameWebSocketContext = createContext(null);
const isDev = import.meta.env.DEV;
const log = (...args) => isDev && console.log(...args);
const warn = (...args) => isDev && console.warn(...args);
const logError = (...args) => console.error(...args);
/**
* Provider that maintains WebSocket connection across page navigation
*/
export const GameWebSocketProvider = ({ children }) => {
const socketRef = useRef(null);
const gameTokenRef = useRef(null);
const [isConnected, setIsConnected] = useState(false);
const [gameState, setGameState] = useState(null);
const [boardData, setBoardData] = useState(null);
const [error, setError] = useState(null);
const [isGamemaster, setIsGamemaster] = useState(false);
const [gameStarted, setGameStarted] = useState(false);
const [pendingPlayers, setPendingPlayers] = useState([]);
const [approvalStatus, setApprovalStatus] = useState(null);
// Memoized derived values
const players = useMemo(() => {
const connectedPlayers = gameState?.connectedPlayers || [];
const currentPlayers = gameState?.currentPlayers || [];
if (currentPlayers.length > 0) {
return currentPlayers;
}
if (connectedPlayers.length > 0) {
return connectedPlayers.map((nameOrObj, index) => {
const playerName = typeof nameOrObj === 'string'
? nameOrObj
: (nameOrObj.playerName || nameOrObj.name || `Player ${index + 1}`);
return {
id: `player-${index}`,
name: playerName,
isOnline: true,
isReady: gameState?.readyPlayers?.includes(playerName) || false,
};
});
}
return [];
}, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]);
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
/**
* Connect to game WebSocket with a game token
* This maintains the connection even when navigating between pages
*/
const connect = useCallback((gameToken) => {
if (!gameToken) {
warn('⚠️ Cannot connect without game token');
return;
}
// If already connected with same token, don't reconnect
if (socketRef.current?.connected && gameTokenRef.current === gameToken) {
log('✅ Already connected with same token');
return;
}
// Disconnect old socket if exists
if (socketRef.current) {
log('🔌 Disconnecting old socket');
socketRef.current.removeAllListeners();
socketRef.current.disconnect();
}
log('🔌 Connecting to game WebSocket...');
gameTokenRef.current = gameToken;
// Connect to /game namespace
socketRef.current = io(`${API_CONFIG.wsURL}/game`, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
timeout: 5000,
});
const socket = socketRef.current;
// Connection handlers
socket.on('connect', () => {
log('✅ Connected to game WebSocket');
setIsConnected(true);
setError(null);
socket.emit('game:join', { gameToken });
});
socket.on('connect_error', (err) => {
logError('❌ Connection error:', err);
setIsConnected(false);
setError(err.message);
});
socket.on('disconnect', (reason) => {
log('🔌 Disconnected:', reason);
setIsConnected(false);
});
// Game state handlers
socket.on('game:state', (state) => {
log('📊 Game state received:', state);
if (state?.isGamemaster !== undefined) {
setIsGamemaster(state.isGamemaster);
}
setGameState(state);
});
socket.on('game:state-update', (state) => {
log('📊 Game state update:', state);
if (state?.isGamemaster !== undefined) {
setIsGamemaster(state.isGamemaster);
}
setGameState(state);
});
socket.on('game:joined', (data) => {
log('✅ Joined game:', data);
if (data.isGamemaster !== undefined) {
setIsGamemaster(data.isGamemaster);
}
});
socket.on('game:player-joined', (data) => {
log('👤 Player joined:', data.playerName);
setGameState(prev => {
if (!prev) return prev;
const currentConnected = prev.connectedPlayers || [];
if (!currentConnected.includes(data.playerName)) {
return {
...prev,
connectedPlayers: [...currentConnected, data.playerName]
};
}
return prev;
});
});
socket.on('game:start', (data) => {
log('🎮 Game started:', data);
setGameStarted(true);
// Store board data if provided
if (data.boardData) {
setBoardData(data.boardData);
log('✅ Board data stored from game:start event');
}
// Update game state with turn info
if (data.playerOrder) {
setGameState(prev => ({
...prev,
playerOrder: data.playerOrder,
currentPlayer: data.currentPlayer,
turnSequence: data.playerOrder
}));
}
});
socket.on('game:started', (data) => {
log('🎮 Game started (legacy event):', data);
setGameStarted(true);
});
socket.on('game:player-moved', (moveData) => {
log('🏃 Player moved:', moveData.playerName);
setGameState(prev => {
if (!prev?.currentPlayers) return prev;
return {
...prev,
currentPlayers: prev.currentPlayers.map(p =>
p.playerId === moveData.playerId
? { ...p, boardPosition: moveData.newPosition }
: p
),
};
});
});
socket.on('game:turn-changed', (data) => {
log('🔄 Turn changed to:', data.currentPlayerName);
setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev);
});
socket.on('game:error', (err) => {
logError('❌ Game error:', err);
setError(err.message);
});
// Approval system handlers
socket.on('game:pending-approval', (data) => {
log('⏳ Pending gamemaster approval:', data);
setApprovalStatus('pending');
setError('Waiting for gamemaster approval...');
});
socket.on('game:player-requesting-join', (data) => {
log('🔔 Player requesting to join:', data.playerName);
setPendingPlayers(prev => {
if (prev.some(p => p.playerName === data.playerName)) return prev;
return [...prev, {
playerName: data.playerName,
isAuthenticated: data.isAuthenticated,
timestamp: data.timestamp
}];
});
});
socket.on('game:approval-granted', (data) => {
log('✅ Join request approved:', data);
setApprovalStatus('approved');
setError(null);
socket.emit('game:join-approved', { gameToken });
});
socket.on('game:approval-denied', (data) => {
logError('❌ Join request denied:', data.reason || data.message);
setApprovalStatus('denied');
setError(data.reason || 'Your request to join was denied');
});
socket.on('game:player-approved', (data) => {
log('✅ Player approved by gamemaster:', data.playerName);
setPendingPlayers(prev => prev.filter(p => p.playerName !== data.playerName));
setGameState(prev => {
if (!prev) return prev;
const currentConnected = prev.connectedPlayers || [];
if (!currentConnected.includes(data.playerName)) {
return {
...prev,
connectedPlayers: [...currentConnected, data.playerName]
};
}
return prev;
});
});
}, []);
/**
* Disconnect from WebSocket
*/
const disconnect = useCallback(() => {
if (socketRef.current) {
log('🔌 Disconnecting from game WebSocket');
socketRef.current.removeAllListeners();
socketRef.current.disconnect();
socketRef.current = null;
gameTokenRef.current = null;
setIsConnected(false);
setGameState(null);
setBoardData(null);
setError(null);
setIsGamemaster(false);
setGameStarted(false);
setPendingPlayers([]);
setApprovalStatus(null);
}
}, []);
// Action methods
const rollDice = useCallback((diceValue) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot roll dice: not connected');
return false;
}
log('🎲 Rolling dice:', diceValue);
socket.emit('game:dice-roll', { gameCode: gameState?.gameCode, diceValue });
return true;
}, [isConnected, gameState?.gameCode]);
const sendMessage = useCallback((message) => {
const socket = socketRef.current;
if (!socket || !isConnected) return false;
socket.emit('game:chat', { gameCode: gameState?.gameCode, message });
return true;
}, [isConnected, gameState?.gameCode]);
const setReady = useCallback((ready = true) => {
const socket = socketRef.current;
if (!socket || !isConnected) return false;
socket.emit('game:ready', { gameCode: gameState?.gameCode, ready });
return true;
}, [isConnected, gameState?.gameCode]);
const leaveGame = useCallback(() => {
const socket = socketRef.current;
if (!socket || !isConnected) return false;
socket.emit('game:leave', { gameCode: gameState?.gameCode });
return true;
}, [isConnected, gameState?.gameCode]);
const approvePlayer = useCallback((playerName) => {
const socket = socketRef.current;
if (!socket || !isConnected || !isGamemaster) return false;
socket.emit('game:approve-player', { gameCode: gameState?.gameCode, playerName });
return true;
}, [isConnected, isGamemaster, gameState?.gameCode]);
const rejectPlayer = useCallback((playerName, reason = 'Join request denied') => {
const socket = socketRef.current;
if (!socket || !isConnected || !isGamemaster) return false;
socket.emit('game:reject-player', { gameCode: gameState?.gameCode, playerName, reason });
return true;
}, [isConnected, isGamemaster, gameState?.gameCode]);
const addEventListener = useCallback((event, handler) => {
const socket = socketRef.current;
if (socket) socket.on(event, handler);
}, []);
const removeEventListener = useCallback((event, handler) => {
const socket = socketRef.current;
if (socket) socket.off(event, handler);
}, []);
const value = {
socket: socketRef.current,
isConnected,
gameState,
players,
boardData,
currentTurn,
error,
isGamemaster,
gameStarted,
pendingPlayers,
approvalStatus,
// Connection management
connect,
disconnect,
// Methods
rollDice,
sendMessage,
setReady,
leaveGame,
approvePlayer,
rejectPlayer,
addEventListener,
removeEventListener,
};
return (
<GameWebSocketContext.Provider value={value}>
{children}
</GameWebSocketContext.Provider>
);
};
/**
* Hook to access the shared WebSocket connection
*/
export const useGameWebSocketContext = () => {
const context = useContext(GameWebSocketContext);
if (!context) {
throw new Error('useGameWebSocketContext must be used within GameWebSocketProvider');
}
return context;
};
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useMemo, useCallback } from "react"
import { getVerticalOffset } from "../../utils/randomUtils"
import Dice from "../../utils/dice/Dice"
import { useGameWebSocket } from "../../hooks/useGameWebSocket"
import { useGameWebSocketContext } from "../../contexts/GameWebSocketContext"
import JokerApprovalModal from "./JokerApprovalModal"
import CardDisplayModal from "./CardDisplayModal"
import ConsequenceModal from "./ConsequenceModal"
@@ -45,8 +45,7 @@ const getDefaultFieldType = (count) => {
}
const GameScreen = () => {
// WebSocket connection
const gameToken = localStorage.getItem('gameToken')
// WebSocket connection from context (maintains connection across navigation)
const {
isConnected,
gameState,
@@ -61,7 +60,7 @@ const GameScreen = () => {
submitPositionGuess,
addEventListener,
removeEventListener
} = useGameWebSocket(gameToken)
} = useGameWebSocketContext()
// Try to get boardData from WebSocket, fallback to localStorage
const boardData = useMemo(() => {
@@ -626,7 +625,7 @@ const GameScreen = () => {
<div>👥 Players: {backendPlayers?.length || 0}</div>
<div>🎲 Board Fields: {boardData?.fields?.length || 0}</div>
<div>🏁 Current Turn: {currentTurn || 'N/A'}</div>
<div>🔑 Token: {gameToken ? '✅' : '❌'}</div>
{/* <div>🔑 Token: {gameToken ? '✅' : '❌'}</div> */}
</div>
</div>
)}
+27 -18
View File
@@ -6,7 +6,7 @@ import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate"
import Navbar from "../../components/Navbar/Navbar.jsx"
import Background from "../../assets/backgrounds/Background.jsx"
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
import { useGameWebSocket } from "../../hooks/useGameWebSocket.js"
import { useGameWebSocketContext } from "../../contexts/GameWebSocketContext"
import { startGame } from "../../api/gameApi.js"
const Lobby = () => {
@@ -21,11 +21,13 @@ const Lobby = () => {
// Get game code from location state or WebSocket
const gameCodeFromState = location.state?.gameCode
const gameToken = localStorage.getItem("gameToken")
const {
isConnected,
gameState,
const gameToken = localStorage.getItem('gameToken')
// Use the shared WebSocket context
const {
connect,
isConnected,
gameState,
players,
isGamemaster,
gameStarted,
@@ -33,7 +35,14 @@ const Lobby = () => {
approvalStatus,
approvePlayer,
rejectPlayer,
} = useGameWebSocket(gameToken)
} = useGameWebSocketContext()
// Connect to WebSocket when component mounts
useEffect(() => {
if (gameToken) {
connect(gameToken)
}
}, [gameToken, connect])
const gameCode = gameCodeFromState || gameState?.gameCode || "Loading..."
@@ -70,19 +79,18 @@ const Lobby = () => {
console.log("🎮 Game started, navigating to /game")
goGame()
}
}, [gameStarted, navigate])
}, [gameStarted, goGame])
// Handle approval status changes
useEffect(() => {
if (approvalStatus === "denied") {
notifyError("A gamemaster elutasította a csatlakozási kérelmedet.")
localStorage.removeItem("gameToken")
if (approvalStatus === 'denied') {
alert('A gamemaster elutasította a csatlakozási kérelmedet.')
localStorage.removeItem('gameToken')
navigate("/home")
} else if (approvalStatus === "approved") {
console.log("✅ Join approved, you can now see the lobby")
notifySuccess("Csatlakozás jóváhagyva!")
} else if (approvalStatus === 'approved') {
console.log('✅ Join approved, you can now see the lobby')
}
}, [approvalStatus, navigate])
}, [approvalStatus, goHome])
const handleExit = () => {
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
@@ -120,8 +128,9 @@ const Lobby = () => {
}
// Navigate immediately after successful start (don't wait for WebSocket)
console.log("🎮 Navigating to /game...")
navigate("/game")
console.log('🎮 Navigating to /game...')
navigate('/game')
} catch (error) {
console.error("Failed to start game:", error)
@@ -129,7 +138,7 @@ const Lobby = () => {
if (error.response?.status === 409) {
console.log("Game already started, navigating to /game...")
// Navigate anyway - game is already running
navigate("/game")
navigate('/game')
} else {
notifyError(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
}