# WebSocket Migration Plan ## Difficulty Assessment: **Medium** ⚙️ **Time estimate:** 3-5 days for full implementation **Complexity:** Moderate - FastAPI has excellent WebSocket support --- ## Architecture Changes ### Current (Polling) ``` Client → HTTP GET every 5s → Server → Query DB → Return JSON ← HTTP Response ← ``` ### Future (WebSocket) ``` Client ←→ WebSocket (persistent) ←→ Server ↓ DB Query only on changes ↓ Push to client immediately ``` --- ## Implementation Steps ### Phase 1: Backend WebSocket Setup (1-2 days) #### 1. Add WebSocket endpoint **File:** `api/main.py` ```python from fastapi import WebSocket, WebSocketDisconnect from typing import Dict import asyncio import json # Connection manager to track active WebSocket connections class ConnectionManager: def __init__(self): self.active_connections: Dict[int, WebSocket] = {} # player_id -> websocket async def connect(self, websocket: WebSocket, player_id: int): await websocket.accept() self.active_connections[player_id] = websocket print(f"Player {player_id} connected via WebSocket") def disconnect(self, player_id: int): if player_id in self.active_connections: del self.active_connections[player_id] print(f"Player {player_id} disconnected") async def send_personal_message(self, player_id: int, message: dict): """Send message to specific player""" if player_id in self.active_connections: try: await self.active_connections[player_id].send_json(message) except Exception as e: print(f"Error sending to player {player_id}: {e}") self.disconnect(player_id) async def broadcast(self, message: dict, exclude: int = None): """Send message to all connected players""" disconnected = [] for player_id, connection in self.active_connections.items(): if player_id == exclude: continue try: await connection.send_json(message) except Exception as e: print(f"Error broadcasting to player {player_id}: {e}") disconnected.append(player_id) # Clean up disconnected players for player_id in disconnected: self.disconnect(player_id) async def send_to_location(self, location_id: str, message: dict, exclude: int = None): """Send message to all players in a specific location""" # Get players in location from database players_in_location = await db.get_players_in_location(location_id) for player in players_in_location: if player['id'] != exclude and player['id'] in self.active_connections: await self.send_personal_message(player['id'], message) manager = ConnectionManager() @app.websocket("/ws/game/{token}") async def websocket_endpoint(websocket: WebSocket, token: str): """ Main WebSocket endpoint for real-time game updates Client connects with auth token, receives push updates """ # Verify token and get player try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) player_id = payload.get("user_id") if not player_id: await websocket.close(code=1008, reason="Invalid token") return except JWTError: await websocket.close(code=1008, reason="Invalid token") return player = await db.get_player_by_id(player_id) if not player: await websocket.close(code=1008, reason="Player not found") return # Connect player await manager.connect(websocket, player_id) # Send initial state initial_state = await get_full_game_state(player_id) await websocket.send_json({ "type": "initial_state", "data": initial_state }) try: # Keep connection alive and listen for messages while True: # Receive messages from client (heartbeat, actions, etc.) data = await websocket.receive_json() # Handle different message types if data.get("type") == "heartbeat": await websocket.send_json({"type": "pong"}) elif data.get("type") == "action": # Client performed action - process and broadcast updates await handle_websocket_action(player_id, data, websocket) except WebSocketDisconnect: manager.disconnect(player_id) print(f"Player {player_id} disconnected") async def handle_websocket_action(player_id: int, data: dict, websocket: WebSocket): """Handle actions received via WebSocket""" action = data.get("action") if action == "move": # Process movement result = await process_move(player_id, data.get("direction")) # Send update to player await manager.send_personal_message(player_id, { "type": "state_update", "data": result }) # Notify players in new location if result.get("success"): await manager.send_to_location( result["new_location_id"], { "type": "player_entered", "player": { "id": player_id, "username": result["username"] } }, exclude=player_id ) # Add more actions as needed... ``` #### 2. Push updates on state changes **Modify existing endpoints to push updates:** ```python @app.post("/api/game/pickup") async def pickup(req, current_user): # ... existing pickup logic ... # After successful pickup, push update via WebSocket await manager.send_personal_message(current_user['id'], { "type": "inventory_update", "data": { "inventory": new_inventory, "dropped_items": new_dropped_items } }) # Also notify other players in location await manager.send_to_location(player['location_id'], { "type": "dropped_items_update", "data": {"dropped_items": new_dropped_items} }, exclude=current_user['id']) return result ``` #### 3. Add database helper for location players **File:** `api/database.py` ```python async def get_players_in_location(location_id: str) -> List[Dict]: """Get all players currently in a location""" async with DatabaseSession() as session: stmt = select(players).where(players.c.location_id == location_id) result = await session.execute(stmt) return [dict(row) for row in result.fetchall()] ``` --- ### Phase 2: Frontend WebSocket Setup (1-2 days) #### 1. Create WebSocket hook **File:** `pwa/src/hooks/useGameWebSocket.ts` ```typescript import { useEffect, useRef, useState } from 'react' interface WebSocketMessage { type: string data: any } export const useGameWebSocket = (token: string, onMessage: (msg: WebSocketMessage) => void) => { const ws = useRef(null) const [isConnected, setIsConnected] = useState(false) const reconnectTimeout = useRef(null) const connect = () => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const host = window.location.host const wsUrl = `${protocol}//${host}/ws/game/${token}` ws.current = new WebSocket(wsUrl) ws.current.onopen = () => { console.log('WebSocket connected') setIsConnected(true) // Start heartbeat const heartbeat = setInterval(() => { if (ws.current?.readyState === WebSocket.OPEN) { ws.current.send(JSON.stringify({ type: 'heartbeat' })) } }, 30000) // Every 30 seconds // Store interval ID for cleanup ws.current.addEventListener('close', () => clearInterval(heartbeat)) } ws.current.onmessage = (event) => { try { const message = JSON.parse(event.data) onMessage(message) } catch (error) { console.error('Error parsing WebSocket message:', error) } } ws.current.onerror = (error) => { console.error('WebSocket error:', error) } ws.current.onclose = () => { console.log('WebSocket disconnected') setIsConnected(false) // Attempt to reconnect after 3 seconds reconnectTimeout.current = window.setTimeout(() => { console.log('Attempting to reconnect...') connect() }, 3000) } } useEffect(() => { connect() return () => { // Cleanup on unmount if (reconnectTimeout.current) { clearTimeout(reconnectTimeout.current) } if (ws.current) { ws.current.close() } } }, [token]) const sendMessage = (message: any) => { if (ws.current?.readyState === WebSocket.OPEN) { ws.current.send(JSON.stringify(message)) } } return { isConnected, sendMessage } } ``` #### 2. Update Game component **File:** `pwa/src/components/Game.tsx` ```typescript import { useGameWebSocket } from '../hooks/useGameWebSocket' const Game = () => { // ... existing state ... const handleWebSocketMessage = (message: WebSocketMessage) => { switch (message.type) { case 'initial_state': // Set full game state on connect setPlayerState(message.data.player) setLocation(message.data.location) setInventory(message.data.inventory) setEquipment(message.data.equipment) break case 'state_update': // Partial update to player state setPlayerState(prev => ({ ...prev, ...message.data.player })) break case 'inventory_update': // Update inventory setInventory(message.data.inventory) if (message.data.dropped_items) { setDroppedItems(message.data.dropped_items) } break case 'pvp_combat_start': // PvP combat initiated setCombatState(message.data.combat) break case 'pvp_combat_action': // Opponent performed action in PvP setCombatLog(prev => [...prev, message.data.action]) setCombatState(prev => ({ ...prev, ...message.data.combat })) break case 'player_entered': // Another player entered your location setMessage(`${message.data.player.username} entered the area`) break // Add more message types... } } const { isConnected, sendMessage } = useGameWebSocket( localStorage.getItem('token') || '', handleWebSocketMessage ) // Remove old polling useEffect, replace with WebSocket // Keep fallback polling for when WebSocket disconnects useEffect(() => { if (!isConnected) { // Fallback to polling if WebSocket disconnected const interval = setInterval(() => { fetchGameData(true) }, 10000) // Poll every 10s as fallback return () => clearInterval(interval) } }, [isConnected]) // ... rest of component } ``` --- ### Phase 3: Hybrid Approach (Recommended - Best of Both Worlds) **Use WebSockets for real-time updates + Polling as fallback** ```typescript const Game = () => { const [connectionMode, setConnectionMode] = useState<'websocket' | 'polling'>('websocket') const { isConnected, sendMessage } = useGameWebSocket(token, handleWebSocketMessage) // Monitor connection and switch to polling if WebSocket fails useEffect(() => { if (!isConnected) { console.warn('WebSocket unavailable, using polling fallback') setConnectionMode('polling') } else { setConnectionMode('websocket') } }, [isConnected]) // Fallback polling when WebSocket not available useEffect(() => { if (connectionMode === 'polling') { const interval = setInterval(fetchGameData, 5000) return () => clearInterval(interval) } }, [connectionMode]) } ``` **Benefits:** - ✅ Best UX when WebSocket works (99% of time) - ✅ Graceful fallback for problematic networks - ✅ Works behind corporate firewalls - ✅ No downtime during deployments --- ## Scaling Considerations ### Single Server (Current - Simple) ``` Client ←→ WebSocket ←→ FastAPI Server ←→ Database ``` **Works for:** Up to 1,000 concurrent connections ### Multi-Server (Future - When growing) ``` Client ←→ WebSocket ←→ FastAPI Server 1 ←→ Redis Pub/Sub ↓ ↓ Database ←→ FastAPI Server 2 ``` **Use Redis for message broadcasting between servers:** ```python import redis.asyncio as redis redis_client = redis.from_url("redis://localhost") class ConnectionManager: async def broadcast(self, message: dict): # Publish to Redis channel await redis_client.publish('game_events', json.dumps(message)) async def listen_to_broadcasts(self): # Subscribe to Redis channel pubsub = redis_client.pubsub() await pubsub.subscribe('game_events') async for message in pubsub.listen(): if message['type'] == 'message': data = json.loads(message['data']) # Forward to connected clients await self._send_to_local_connections(data) ``` **Works for:** Unlimited connections (horizontal scaling) --- ## Migration Strategy: Gradual Rollout ### Option 1: Big Bang (3-5 days downtime) - Implement everything at once - Test thoroughly - Deploy and switch ### Option 2: Gradual (Recommended - Zero downtime) **Week 1:** - ✅ Implement WebSocket endpoint - ✅ Keep polling working - ✅ Add feature flag: `USE_WEBSOCKET=false` **Week 2:** - ✅ Test WebSocket with beta users - ✅ Fix any issues - ✅ Enable for 10% of users **Week 3:** - ✅ Enable for 50% of users - ✅ Monitor performance - ✅ Fix edge cases **Week 4:** - ✅ Enable for 100% of users - ✅ Keep polling as fallback - ✅ Remove old polling code (optional) --- ## Expected Benefits After Migration ### Performance - **Latency:** 5000ms → **<100ms** (50x faster) - **Bandwidth:** 18KB/min → **~1KB/min** (95% reduction) - **Server Load:** 12 queries/poll → **1 query/change** (90% reduction) ### User Experience - ⚡ **Instant combat updates** (no 5s delay) - 🗺️ **Live player locations** on map - 💬 **Real-time chat** capability - 🎮 **Better PvP** feel (see actions immediately) - 📢 **Server announcements** (events, maintenance) ### New Features Enabled - 👥 Party/group system - 💬 In-game chat - 🗺️ Live world map with player positions - 📊 Real-time leaderboards - 🎪 Live events/raids - 🎁 Random spawns/drops broadcast --- ## My Recommendation ### For Your Game: **Implement WebSockets Now** 🚀 **Why:** 1. **You're already planning Steam release** - WebSocket quality expected 2. **PvP combat exists** - Real-time feel makes huge difference 3. **FastAPI has excellent WebSocket support** - Not that hard 4. **Your codebase is clean** - Easy to refactor 5. **Growing player base** - Better to do it now than later **Timeline:** - Day 1-2: Backend WebSocket setup - Day 3-4: Frontend integration - Day 5: Testing & polish - Week 2: Gradual rollout **Risk:** Low - Keep polling as fallback --- ## Code Complexity Comparison ### Polling (Current) ```typescript // Client - 20 lines useEffect(() => { const interval = setInterval(fetchGameData, 5000) return () => clearInterval(interval) }, []) ``` ```python # Server - 5 lines @app.get("/api/game/state") async def get_state(user): return await db.get_player_state(user.id) ``` **Total:** ~25 lines, very simple ### WebSocket (After migration) ```typescript // Client - 80 lines (hook + handler) const useGameWebSocket = (token, onMessage) => { // Connection management // Reconnection logic // Message handling // Heartbeat } ``` ```python # Server - 150 lines class ConnectionManager: # Connection tracking # Broadcasting # Message routing @app.websocket("/ws/game/{token}") async def websocket_endpoint(): # Auth # Connection handling # Message processing ``` **Total:** ~230 lines, moderate complexity **Verdict:** About 10x more code, but FastAPI does heavy lifting. Complexity is **manageable**. --- ## My Assessment **Difficulty:** 6/10 for me to implement - I know the codebase well - FastAPI WebSocket support is great - Your architecture is clean **Would take me:** 3-4 days to implement fully with testing **Worth it?** **Absolutely YES** 💯 - Long-term better performance - Better UX - Industry standard for real-time games - Enables future features - Required for serious Steam release Want me to start implementing it? I can do it in phases with zero downtime!