Merge feature/socket-reconnect to main, redis implementation done till game waiting room#23
Merge feature/socket-reconnect to main, redis implementation done till game waiting room#23vishalpokuri wants to merge 34 commits intomainfrom
Conversation
* Initial plan * Add Redis pub-sub support for scalable backend architecture Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com> * Remove logs from git tracking and update .gitignore Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com> * Fix async API consistency in gameStateManager functions Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com> * fix the user management bug * Fix WebSocket idle connection timeout on /play page Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com> * Refactor /play page to use global socketManager instead of creating its own socket connection Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com> * remove uneccesary long lines * remove unnecessary docs * refactor * fix the user management bug * fix the import error of abi --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com> Co-authored-by: ayushk4543 <sanusingh335@gmail.com>
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
…n/uno-game into copilot/migrate-backend-to-typescript-again
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
Co-authored-by: ayush4345 <99096397+ayush4345@users.noreply.github.com>
…ased user management
- Add Redis-based user storage with UUID generation for global user accounts
- Implement registerOrLoginUser system: creates new account on first connect, returns existing on subsequent connections
- Refactor storage layer into modular architecture:
- BaseRedisStorage: shared Redis client and operations
- UserStorage: user account and room management with Redis + in-memory fallback
- GameStorage: game state persistence with Redis + disk backup
- Add smart logging utility (log.ts): console.log in dev, winston in production
- Update User interface to support optional room (users exist before joining rooms)
- Add socket event handlers for user registration when wallet connects
- Integrate wallet connection with user registration in frontend
- Add Docker support with Dockerfile and docker-compose.yml
- Install uuid package for unique user ID generation
Redis Keys:
- user:${userId}: User object with wallet, name, status, timestamps
- users:all: Set of all registered user IDs
- room:users:${roomId}: Array of user IDs in seat order
Breaking Changes:
- Removed legacy users.ts, gameStateManager.ts, redisStorage.ts
- User.room is now optional (users can exist without being in a room)
- All storage methods now use dependency injection pattern
…andler - Set socketId when user registers/logs in via registerOrLoginUser - Update user's room field in database when joining game room via joinRoom - Ensure socketId is properly tracked across socket connections - Fix user lookup to use getUserBySocketId for current connection state
There was a problem hiding this comment.
Pull request overview
This PR implements comprehensive socket reconnection functionality with Redis-based state persistence for the Zunno multiplayer UNO game. The changes include a complete backend migration to TypeScript, enhanced socket management with automatic reconnection, wallet-based user persistence, and UI improvements for connection status visibility.
Key changes:
- Socket reconnection with exponential backoff and action buffering
- Redis integration for persistent game state and user management
- Backend migration from JavaScript to TypeScript with modular architecture
- Wallet-based user identification for cross-session continuity
Reviewed changes
Copilot reviewed 67 out of 73 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/services/socketManager.ts | New socket manager with reconnection, heartbeat, and buffering |
| frontend/src/services/socket.ts | Socket proxy wrapper for stale reference prevention |
| frontend/src/utils/walletStorage.ts | LocalStorage utilities for wallet persistence |
| frontend/src/context/SocketConnectionContext.tsx | React context for global connection state |
| frontend/src/components/ConnectionStatusIndicator.tsx | UI component showing connection status |
| frontend/src/components/gameroom/Room.tsx | Enhanced with reconnection and state sync logic |
| frontend/src/components/gameroom/Game.js | Refactored game logic with action buffering |
| frontend/src/constants/gameConstants.ts | Shared game constants (MAX_PLAYERS = 3) |
| backend/index.ts | New TypeScript entry point with modular structure |
| backend/services/storage/* | Redis-based storage layer for users and games |
| backend/socket/* | Modular socket event handlers |
| backend/config/* | Configuration for Redis and Socket.IO |
| backend/types/users.ts | TypeScript type definitions for users |
| frontend/src/app/provider.jsx | Added SocketConnectionProvider |
| contracts/script/deploy.s.sol | Commented out console.log |
| Multiple UI files | Height changed from 100vh to 100svh, positioning adjustments |
Files not reviewed (1)
- backend/pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const CONNECTION = | ||
| process.env.NEXT_PUBLIC_WEBSOCKET_URL || | ||
| "https://zkuno-669372856670.us-central1.run.app"; | ||
| import socket, { socketManager } from "@/services/socket"; |
There was a problem hiding this comment.
Unused import socket.
|
|
||
| import React from 'react'; | ||
| import { useSocketConnection } from '@/context/SocketConnectionContext'; | ||
| import { AlertCircle, Wifi, WifiOff, RefreshCw } from 'lucide-react'; |
There was a problem hiding this comment.
Unused import Wifi.
|
|
||
| const isWildCard = (card) => card === 'W' || card === 'D4W'; | ||
| const isSkipCard = (card) => card.startsWith('skip'); | ||
| const isReverseCard = (card) => card.startsWith('_'); |
There was a problem hiding this comment.
Unused variable isReverseCard.
| const [playerHand, setPlayerHand] = useState<string[]>([]) | ||
| const [offChainGameState, setOffChainGameState] = | ||
| useState<OffChainGameState | null>(null); | ||
| const [error, setError] = useState<string | null>(null); |
There was a problem hiding this comment.
Unused variable error.
| const [offChainGameState, setOffChainGameState] = | ||
| useState<OffChainGameState | null>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [playerHand, setPlayerHand] = useState<string[]>([]); |
There was a problem hiding this comment.
Unused variable playerHand.
| */ | ||
|
|
||
| const WALLET_ADDRESS_KEY = 'zunno_wallet_address'; | ||
| const USER_ID_KEY = 'zunno_user_id'; |
There was a problem hiding this comment.
Unused variable USER_ID_KEY.
| // Get current player's deck | ||
| const playerDeck = allPlayerDecks[currentUser] || []; | ||
|
|
||
| console.log(currentUser, "currentUser") |
There was a problem hiding this comment.
Avoid automated semicolon insertion (95% of all statements in the enclosing function have an explicit semicolon).
|
|
||
| # Set the working directory in the container | ||
| WORKDIR /usr/src/app | ||
| WORKDIR /app |
There was a problem hiding this comment.
This Dockerfile no longer sets NODE_ENV=production inside the container, which means the Express app will run in development mode by default if the environment variable is not provided externally. In Express, non‑production mode enables verbose error responses and stack traces to clients, which can leak internal file paths, implementation details, and other sensitive information that an attacker can trigger by causing server errors. To avoid this, ensure the container always runs with NODE_ENV set to production (either in the Dockerfile or via your deployment environment) so that production-safe error handling is enforced.
| WORKDIR /app | |
| WORKDIR /app | |
| ENV NODE_ENV=production |
| socket.on('requestGameStateSync', ({ roomId, gameId }) => { | ||
| try { | ||
| // Fetch current game state from your game state storage | ||
| const gameState = getGameState(roomId, gameId); | ||
| const cardHashMap = getCardHashMap(gameId); | ||
|
|
||
| if (gameState) { | ||
| // Send state back to the requesting client | ||
| socket.emit(`gameStateSync-${roomId}`, { | ||
| newState: gameState, | ||
| cardHashMap: cardHashMap | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error syncing game state:', error); | ||
| } | ||
| }); |
There was a problem hiding this comment.
This requestGameStateSync handler returns the full game state to any client that supplies a roomId or gameId, without checking whether the requesting socket is actually a participant in that game. An attacker who can guess or learn valid room or game IDs can open a socket connection, call requestGameStateSync, and obtain complete game state (including other players’ hands) for games they are not authorized to view. Add an authorization check that ties the requesting socket to a user record and verifies that user is a member of the specified room/game before sending any state back.
| socket.on('rejoinRoom', ({ room, gameId }, callback) => { | ||
| try { | ||
| // Check if room exists | ||
| const roomExists = rooms.has(room); | ||
|
|
||
| if (roomExists) { | ||
| // Add socket back to room | ||
| socket.join(room); | ||
|
|
||
| // Restore user's session if needed | ||
| const user = findUserBySocketId(socket.id); | ||
| if (user) { | ||
| user.connected = true; | ||
| } | ||
|
|
||
| callback({ success: true, room, gameId }); | ||
|
|
||
| // Notify other players | ||
| socket.to(room).emit('playerReconnected', { | ||
| userId: socket.id, | ||
| room | ||
| }); |
There was a problem hiding this comment.
This rejoinRoom handler lets any connected client join an existing room by passing a room value, as long as rooms.has(room) is true, without verifying that the socket corresponds to the same user who was previously in that room. An attacker can guess or brute‑force room IDs and call this event to join arbitrary active rooms, observe game events, and potentially interfere with games they were never authorized to join. You should tie reconnection to a previously stored user identity (e.g., wallet or session ID) for that room and refuse reconnection when the socket/user does not match an existing room participant.
|
@ayushk-45 Can merge now safely |
No description provided.