690 lines
15 KiB
Markdown
690 lines
15 KiB
Markdown
# 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 <div>Connected: {isConnected ? 'Yes' : 'No'}</div>;
|
|
}
|
|
```
|