15 KiB
WebSocket Integration Guide
This guide explains how to use the WebSocket system in TurboTrades for real-time communication.
Table of Contents
- Overview
- Client-Side Connection
- Authentication
- Message Types
- Server-Side Broadcasting
- WebSocket Manager API
- 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
- Use WSS (WebSocket Secure) in production with SSL/TLS
- Implement rate limiting on the server side
- Monitor connection counts and set limits
- Use Redis for distributed WebSocket management across multiple servers
- Log WebSocket events for debugging and analytics
- Implement circuit breakers for reconnection logic
- 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>;
}