Stack:
Frontend: Next.js
Backend: Express/Node.js
const express = require('express');
const { createClient } = require('@supabase/supabase-js');
const cors = require('cors');
const http = require('http');
const { WebSocketServer } = require('ws');
const WebSocket = require('ws');
global.WebSocket = WebSocket; // so I can use in my http routes
// Create the Express app
const app = express();
// Create an HTTP server from the Express app
const server = http.createServer(app);
// Create WebSocket server
const wss = new WebSocketServer({ noServer: true });
// Database stuff
app.use(cors({
origin: '*', // or '*' for all origins
methods: ['GET', 'POST'],
}));
app.use(express.json());
// Routes and other stuff
// Store connected clients and their rooms
const clients = new Map();
const rooms = new Map();
// WebSocket connection handling
wss.on('connection', (ws, req) => {
const id = Math.random().toString(36).substring(2, 10);
console.log('A user connected:', id);
// Store client with ID
clients.set(ws, { id });
// Send initial connection confirmation
ws.send(JSON.stringify({
type: 'connected',
id
}));
// Handle messages
ws.on('message', (message) => {
try {
const data = JSON.parse(message.toString());
console.log(`Received message from ${id}:`, data.type);
// Handle different message types
switch (data.type) {
case 'joinDraft':
const draftId = data.draftId;
// Add client to room
if (!rooms.has(draftId)) {
rooms.set(draftId, new Set());
}
rooms.get(draftId).add(ws);
clients.get(ws).room = draftId;
console.log(`Client ${id} joined draft room: ${draftId}`);
// Send confirmation
ws.send(JSON.stringify({
type: 'joinedDraft',
draftId,
clientCount: rooms.get(draftId).size
}));
// Notify others in the room
broadcastToRoom(draftId, {
type: 'userJoined',
clientId: id,
clientCount: rooms.get(draftId).size
}, ws);
break;
case 'leaveDraft':
const roomToLeave = clients.get(ws).room;
if (roomToLeave && rooms.has(roomToLeave)) {
const clientCount = rooms.get(roomToLeave).size - 1;
// Notify others before removing
broadcastToRoom(roomToLeave, {
type: 'userLeft',
clientId: id,
clientCount: clientCount
}, ws);
rooms.get(roomToLeave).delete(ws);
console.log(`Client ${id} left draft room: ${roomToLeave}`);
// Clean up empty rooms
if (rooms.get(roomToLeave).size === 0) {
rooms.delete(roomToLeave);
console.log(`Room ${roomToLeave} deleted (empty)`);
}
// Remove room from client info
delete clients.get(ws).room;
}
break;
case 'ping':
ws.send(JSON.stringify({
type: 'pong',
timestamp: Date.now()
}));
break;
default:
// Broadcast to room if client is in a room
const room = clients.get(ws).room;
if (room && rooms.has(room)) {
broadcastToRoom(room, data, ws);
}
}
} catch (error) {
console.error('Error processing message:', error);
ws.send(JSON.stringify({
type: 'error',
message: 'Failed to process message'
}));
}
});
// Handle disconnection
ws.on('close', () => {
const clientInfo = clients.get(ws);
console.log('User disconnected:', clientInfo?.id);
// Remove from rooms and notify others
if (clientInfo?.room && rooms.has(clientInfo.room)) {
const roomId = clientInfo.room;
const clientCount = rooms.get(roomId).size - 1;
// Notify others in the room
broadcastToRoom(roomId, {
type: 'userDisconnected',
clientId: clientInfo.id,
clientCount: clientCount
}, ws);
rooms.get(roomId).delete(ws);
// Clean up empty rooms
if (rooms.get(roomId).size === 0) {
rooms.delete(roomId);
console.log(`Room ${roomId} deleted (empty after disconnect)`);
}
}
// Remove client
clients.delete(ws);
});
// Handle errors
ws.on('error', (error) => {
console.error(`WebSocket error for client ${id}:`, error);
});
});
// Helper function to broadcast to a room (excluding sender if provided)
function broadcastToRoom(roomId, message, excludeClient = null) {
if (rooms.has(roomId)) {
const roomClients = rooms.get(roomId);
roomClients.forEach((client) => {
if (client !== excludeClient && client.readyState === WebSocket.OPEN) {
try {
client.send(JSON.stringify(message));
} catch (error) {
console.error('Error sending message to client:', error);
}
}
});
return true;
}
return false;
}
// Make broadcast function available to routes
app.set('broadcastToRoom', broadcastToRoom);
// Ping clients periodically to keep connections alive
setInterval(() => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() }));
}
});
}, 30000); // Every 30 seconds
const port = process.env.PORT || 3000;
server.on('upgrade', (req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req)
})
})
server.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Frontend:
useEffect(() => {
// Using the correct endpoint with the WebSocket path
const ws = new WebSocket('wss://*myurlhere*.onrender.com/');
// Add connection timeout handling
const connectionTimeout = setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
console.error('WebSocket connection timed out');
ws.close();
}
}, 10000); // 10 second timeout
ws.onopen = () => {
console.log('Connected to WebSocket server');
clearTimeout(connectionTimeout);
setIsConnected(true);
setSocket(ws);
// Send a ping to keep the connection alive
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // Every 30 seconds
// Clear interval on component unmount
return () => clearInterval(pingInterval);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('Received message:', data);
// Handle different message types here
} catch (error) {
console.error('Error parsing message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
console.log('Connection state:', ws.readyState);
console.error('Connection failed. Please check if the server is running and accessible.');
// Try reconnecting with a fallback URL if needed
// attemptReconnect();
};
ws.onclose = (event) => {
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
setIsConnected(false);
clearTimeout(connectionTimeout);
};
return () => {
clearTimeout(connectionTimeout);
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, []);
Connecting to the render backend works when I use npx wscat -c wss:..
but as soon as I use my frontend, it can’t connect to the websocket server. HTTP works fine. Anybody know what the issue might be?