Files
TurboTrades/WEBSOCKET_GUIDE.md
2026-01-10 04:57:43 +00:00

15 KiB

WebSocket Integration Guide

This guide explains how to use the WebSocket system in TurboTrades for real-time communication.

Table of Contents

  1. Overview
  2. Client-Side Connection
  3. Authentication
  4. Message Types
  5. Server-Side Broadcasting
  6. WebSocket Manager API
  7. 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

// 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)

// 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:

// 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:

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)

// 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:

{
  "type": "connected",
  "data": {
    "userId": "user_id_here",
    "timestamp": 1234567890000
  }
}

Pong Response

Response to client ping:

{
  "type": "pong",
  "timestamp": 1234567890000
}

Public Broadcasts

These are sent to all connected clients:

New Listing:

{
  "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:

{
  "type": "price_update",
  "data": {
    "listingId": "listing_123",
    "itemName": "AK-47 | Redline",
    "oldPrice": 45.99,
    "newPrice": 39.99,
    "percentChange": "-13.05"
  },
  "timestamp": 1234567890000
}

Listing Sold:

{
  "type": "listing_sold",
  "data": {
    "listingId": "listing_123",
    "itemName": "AK-47 | Redline",
    "price": 45.99
  },
  "timestamp": 1234567890000
}

Listing Removed:

{
  "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:

{
  "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:

{
  "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:

{
  "type": "notification",
  "data": {
    "message": "Your account has been verified!",
    "priority": "high"
  }
}

Server-Side Broadcasting

Using WebSocket Manager

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:

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

// 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

// 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

// 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:

{
  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:

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:

function calculateBackoff(attempt) {
  return Math.min(1000 * Math.pow(2, attempt), 30000);
}

4. Keep-Alive

Send periodic pings to maintain connection:

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:

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:

// 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
// 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:

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

// 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

# 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

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 <div>Connected: {isConnected ? 'Yes' : 'No'}</div>;
}