# WebSocket Integration Guide This guide explains how to use the WebSocket system in TurboTrades for real-time communication. ## Table of Contents 1. [Overview](#overview) 2. [Client-Side Connection](#client-side-connection) 3. [Authentication](#authentication) 4. [Message Types](#message-types) 5. [Server-Side Broadcasting](#server-side-broadcasting) 6. [WebSocket Manager API](#websocket-manager-api) 7. [Best Practices](#best-practices) ## Overview The TurboTrades WebSocket system provides: - **User Mapping**: Automatically maps authenticated users to their WebSocket connections - **Public Broadcasting**: Send updates to all connected clients (authenticated or not) - **Targeted Messaging**: Send messages to specific users - **Heartbeat System**: Automatic detection and cleanup of dead connections - **Flexible Authentication**: Supports both authenticated and anonymous connections ## Client-Side Connection ### Basic Connection ```javascript // Connect to WebSocket server const ws = new WebSocket('ws://localhost:3000/ws'); // Listen for connection open ws.addEventListener('open', (event) => { console.log('Connected to WebSocket server'); }); // Listen for messages ws.addEventListener('message', (event) => { const message = JSON.parse(event.data); console.log('Received:', message); // Handle different message types switch(message.type) { case 'connected': console.log('Connection confirmed:', message.data); break; case 'new_listing': handleNewListing(message.data); break; case 'price_update': handlePriceUpdate(message.data); break; // ... handle other message types } }); // Listen for errors ws.addEventListener('error', (error) => { console.error('WebSocket error:', error); }); // Listen for connection close ws.addEventListener('close', (event) => { console.log('Disconnected from WebSocket server'); // Implement reconnection logic here }); ``` ### Authenticated Connection (Query String) ```javascript // Get access token from your auth system const accessToken = getAccessToken(); // Your function to get token // Connect with token in query string const ws = new WebSocket(`ws://localhost:3000/ws?token=${accessToken}`); ``` ### Authenticated Connection (Cookies) If you're using httpOnly cookies (recommended), the browser will automatically send cookies: ```javascript // Just connect - cookies are sent automatically const ws = new WebSocket('ws://localhost:3000/ws'); // Server will authenticate using cookie ``` ## Authentication ### Token Refresh Handling When your access token expires, you'll need to refresh and reconnect: ```javascript class WebSocketClient { constructor() { this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; } connect(token) { this.ws = new WebSocket(`ws://localhost:3000/ws?token=${token}`); this.ws.addEventListener('open', () => { console.log('Connected'); this.reconnectAttempts = 0; }); this.ws.addEventListener('close', (event) => { if (event.code === 1000) { // Normal closure console.log('Connection closed normally'); return; } // Attempt reconnection this.reconnect(); }); this.ws.addEventListener('error', (error) => { console.error('WebSocket error:', error); }); } async reconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Max reconnection attempts reached'); return; } this.reconnectAttempts++; console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`); // Wait before reconnecting (exponential backoff) await new Promise(resolve => setTimeout(resolve, Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)) ); // Refresh token if needed const newToken = await refreshAccessToken(); // Your function this.connect(newToken); } disconnect() { if (this.ws) { this.ws.close(1000, 'Client disconnect'); } } send(type, data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, data })); } } } // Usage const wsClient = new WebSocketClient(); wsClient.connect(accessToken); ``` ## Message Types ### Client → Server #### Ping/Pong (Keep-Alive) ```javascript // Send ping every 30 seconds to keep connection alive setInterval(() => { ws.send(JSON.stringify({ type: 'ping' })); }, 30000); // Server will respond with pong ``` ### Server → Client #### Connection Confirmation Sent immediately after successful connection: ```json { "type": "connected", "data": { "userId": "user_id_here", "timestamp": 1234567890000 } } ``` #### Pong Response Response to client ping: ```json { "type": "pong", "timestamp": 1234567890000 } ``` #### Public Broadcasts These are sent to all connected clients: **New Listing:** ```json { "type": "new_listing", "data": { "listing": { "id": "listing_123", "itemName": "AK-47 | Redline", "price": 45.99, "game": "cs2" }, "message": "New CS2 item listed: AK-47 | Redline" }, "timestamp": 1234567890000 } ``` **Price Update:** ```json { "type": "price_update", "data": { "listingId": "listing_123", "itemName": "AK-47 | Redline", "oldPrice": 45.99, "newPrice": 39.99, "percentChange": "-13.05" }, "timestamp": 1234567890000 } ``` **Listing Sold:** ```json { "type": "listing_sold", "data": { "listingId": "listing_123", "itemName": "AK-47 | Redline", "price": 45.99 }, "timestamp": 1234567890000 } ``` **Listing Removed:** ```json { "type": "listing_removed", "data": { "listingId": "listing_123", "itemName": "AK-47 | Redline", "reason": "Removed by seller" }, "timestamp": 1234567890000 } ``` #### Private Messages Sent to specific authenticated users: **Item Sold Notification:** ```json { "type": "item_sold", "data": { "transaction": { "id": "tx_123", "itemName": "AK-47 | Redline", "price": 45.99, "buyer": { "username": "BuyerName" } }, "message": "Your AK-47 | Redline has been sold for $45.99!" } } ``` **Purchase Confirmation:** ```json { "type": "purchase_confirmed", "data": { "transaction": { "id": "tx_123", "itemName": "AK-47 | Redline", "price": 45.99 }, "message": "Purchase confirmed! Trade offer will be sent shortly." } } ``` **Admin Message:** ```json { "type": "notification", "data": { "message": "Your account has been verified!", "priority": "high" } } ``` ## Server-Side Broadcasting ### Using WebSocket Manager ```javascript import { wsManager } from './utils/websocket.js'; // Broadcast to ALL connected clients (public + authenticated) wsManager.broadcastPublic('price_update', { itemId: '123', newPrice: 99.99, oldPrice: 149.99 }); // Send to specific user const userId = 'user_id_here'; const sent = wsManager.sendToUser(steamId, { type: 'notification', data: { message: 'Your item sold!' } }); if (!sent) { console.log('User not connected'); } // Broadcast to authenticated users only wsManager.broadcastToAuthenticated({ type: 'announcement', data: { message: 'Maintenance in 5 minutes' } }); // Broadcast with exclusions wsManager.broadcastToAll( { type: 'user_online', data: { username: 'NewUser' } }, ['exclude_steam_id_1', 'exclude_steam_id_2'] ); ``` ### In Route Handlers Example from marketplace routes: ```javascript fastify.post('/marketplace/listings', { preHandler: authenticate }, async (request, reply) => { const newListing = await createListing(request.body); // Broadcast to all clients wsManager.broadcastPublic('new_listing', { listing: newListing, message: `New item: ${newListing.itemName}` }); return reply.send({ success: true, listing: newListing }); }); ``` ## WebSocket Manager API ### Connection Management ```javascript // Check if user is connected const isOnline = wsManager.isUserConnected(steamId); // Get connection metadata const metadata = wsManager.getUserMetadata(steamId); // Returns: { steamId, connectedAt, lastActivity } // Get statistics const totalSockets = wsManager.getTotalSocketCount(); const authenticatedUsers = wsManager.getAuthenticatedUserCount(); ``` ### Broadcasting Methods ```javascript // Broadcast to everyone wsManager.broadcastToAll(messageObject, excludeSteamIds = []); // Broadcast to authenticated only wsManager.broadcastToAuthenticated(messageObject, excludeSteamIds = []); // Convenience method for public broadcasts wsManager.broadcastPublic(type, payload); // Equivalent to: // wsManager.broadcastToAll({ // type, // data: payload, // timestamp: Date.now() // }); // Send to specific user (by Steam ID) wsManager.sendToUser(steamId, messageObject); // Send to specific socket wsManager.sendToSocket(socket, messageObject); ``` ### Lifecycle Methods ```javascript // Start heartbeat (automatically done on server start) wsManager.startHeartbeat(30000); // 30 seconds // Stop heartbeat wsManager.stopHeartbeat(); // Close all connections (graceful shutdown) wsManager.closeAll(); ``` ## Best Practices ### 1. Message Structure Always use a consistent message structure: ```javascript { type: 'message_type', // Required: identifies the message data: { /* payload */ }, // Required: the actual data timestamp: 1234567890000 // Optional but recommended } ``` ### 2. Error Handling Always wrap JSON parsing in try-catch: ```javascript ws.addEventListener('message', (event) => { try { const message = JSON.parse(event.data); handleMessage(message); } catch (error) { console.error('Failed to parse message:', error); } }); ``` ### 3. Reconnection Strategy Implement exponential backoff for reconnections: ```javascript function calculateBackoff(attempt) { return Math.min(1000 * Math.pow(2, attempt), 30000); } ``` ### 4. Keep-Alive Send periodic pings to maintain connection: ```javascript const pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); // Clean up on disconnect ws.addEventListener('close', () => { clearInterval(pingInterval); }); ``` ### 5. Memory Management Clean up event listeners when reconnecting: ```javascript function connect() { // Remove old listeners if reconnecting if (ws) { ws.removeEventListener('message', messageHandler); ws.removeEventListener('close', closeHandler); } ws = new WebSocket(url); ws.addEventListener('message', messageHandler); ws.addEventListener('close', closeHandler); } ``` ### 6. User Status Tracking Check if users are online before sending: ```javascript // Check via API endpoint const response = await fetch(`/ws/status/${userId}`, { headers: { 'Authorization': `Bearer ${token}` } }); const { online } = await response.json(); if (online) { // User is connected, they'll receive WebSocket messages } ``` ### 7. Broadcasting Best Practices - **Use broadcastPublic** for data everyone needs (prices, listings) - **Use broadcastToAuthenticated** for user-specific announcements - **Use sendToUser** for private notifications - **Exclude users** when broadcasting user-generated events to avoid echoes ```javascript // When user updates their listing, don't send update back to them wsManager.broadcastToAll( { type: 'listing_update', data: listing }, [steamId] // Exclude the user who made the change ); ``` ### 8. Rate Limiting Consider rate limiting WebSocket messages on the client: ```javascript class RateLimitedWebSocket { constructor(url) { this.ws = new WebSocket(url); this.messageQueue = []; this.messagesPerSecond = 10; this.processQueue(); } send(message) { this.messageQueue.push(message); } processQueue() { setInterval(() => { const batch = this.messageQueue.splice(0, this.messagesPerSecond); batch.forEach(msg => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(msg)); } }); }, 1000); } } ``` ## Testing WebSocket Connection ### Using Browser Console ```javascript // Open console in browser and run: const ws = new WebSocket('ws://localhost:3000/ws'); ws.onmessage = (e) => console.log('Received:', JSON.parse(e.data)); ws.onopen = () => console.log('Connected'); ws.send(JSON.stringify({ type: 'ping' })); ``` ### Using wscat CLI Tool ```bash # Install wscat npm install -g wscat # Connect wscat -c ws://localhost:3000/ws # Or with token wscat -c "ws://localhost:3000/ws?token=YOUR_TOKEN" # Send ping > {"type":"ping"} # You'll receive pong response < {"type":"pong","timestamp":1234567890} ``` ## Production Considerations 1. **Use WSS (WebSocket Secure)** in production with SSL/TLS 2. **Implement rate limiting** on the server side 3. **Monitor connection counts** and set limits 4. **Use Redis** for distributed WebSocket management across multiple servers 5. **Log WebSocket events** for debugging and analytics 6. **Implement circuit breakers** for reconnection logic 7. **Consider using Socket.io** for automatic fallbacks and better browser support ## Common Issues & Solutions ### Issue: Connection Closes Immediately **Cause**: Token expired or invalid **Solution**: Refresh token before connecting ### Issue: Messages Not Received **Cause**: Connection not open or user not authenticated **Solution**: Check `ws.readyState` before sending ### Issue: Memory Leaks **Cause**: Not cleaning up event listeners **Solution**: Remove listeners on disconnect ### Issue: Duplicate Connections **Cause**: Not closing old connection before creating new one **Solution**: Always call `ws.close()` before reconnecting ## Example: Complete React Hook ```javascript import { useEffect, useRef, useState } from 'react'; function useWebSocket(url, token) { const [isConnected, setIsConnected] = useState(false); const [messages, setMessages] = useState([]); const ws = useRef(null); useEffect(() => { ws.current = new WebSocket(`${url}?token=${token}`); ws.current.onopen = () => { console.log('Connected'); setIsConnected(true); }; ws.current.onmessage = (event) => { const message = JSON.parse(event.data); setMessages(prev => [...prev, message]); }; ws.current.onclose = () => { console.log('Disconnected'); setIsConnected(false); }; // Cleanup on unmount return () => { ws.current?.close(); }; }, [url, token]); const sendMessage = (type, data) => { if (ws.current?.readyState === WebSocket.OPEN) { ws.current.send(JSON.stringify({ type, data })); } }; return { isConnected, messages, sendMessage }; } // Usage in component function MarketplaceComponent() { const { isConnected, messages } = useWebSocket( 'ws://localhost:3000/ws', accessToken ); useEffect(() => { messages.forEach(msg => { if (msg.type === 'new_listing') { console.log('New listing:', msg.data); } }); }, [messages]); return
Connected: {isConnected ? 'Yes' : 'No'}
; } ```