WebSocket
This guide covers creating WebSocket servers with Verb for real-time, bidirectional communication.
Overview
WebSocket provides full-duplex communication channels over a single TCP connection, making it ideal for:
- Real-time chat applications
- Live data feeds
- Gaming applications
- Collaborative editing
- Live notifications
Basic WebSocket Server
Creating a WebSocket Server
typescript
import { createServer, ServerProtocol } from "verb";
const app = createServer(ServerProtocol.WEBSOCKET);
// HTTP routes still work
app.get("/", (req, res) => {
res.html(`
<html>
<head><title>WebSocket Demo</title></head>
<body>
<h1>WebSocket Server</h1>
<script>
const ws = new WebSocket('ws://localhost:3000');
ws.onopen = () => console.log('Connected');
ws.onmessage = (e) => console.log('Message:', e.data);
</script>
</body>
</html>
`);
});
// WebSocket configuration
app.websocket({
open: (ws) => {
console.log("WebSocket connection opened");
ws.send("Welcome to WebSocket server!");
},
message: (ws, message) => {
console.log("Received:", message);
ws.send(`Echo: ${message}`);
},
close: (ws, code, reason) => {
console.log("WebSocket closed:", code, reason);
}
});
app.listen(3000);
WebSocket Secure (WSS)
typescript
import { createServer, ServerProtocol } from "verb";
const app = createServer(ServerProtocol.WEBSOCKETS);
app.websocket({
open: (ws) => {
console.log("Secure WebSocket connection opened");
ws.send("Secure WebSocket connected!");
},
message: (ws, message) => {
console.log("Secure message:", message);
ws.send(`Secure echo: ${message}`);
},
close: (ws, code, reason) => {
console.log("Secure WebSocket closed:", code, reason);
}
});
app.listen(443);
WebSocket Configuration
Advanced Configuration
typescript
const app = createServer(ServerProtocol.WEBSOCKET);
app.websocket({
// Connection opened
open: (ws) => {
console.log("New WebSocket connection");
// Send welcome message
ws.send(JSON.stringify({
type: "welcome",
message: "Connected to WebSocket server"
}));
},
// Message received
message: (ws, message) => {
try {
const data = JSON.parse(message);
console.log("Received:", data);
// Handle different message types
switch (data.type) {
case "ping":
ws.send(JSON.stringify({ type: "pong" }));
break;
case "chat":
broadcastMessage(data.message);
break;
default:
ws.send(JSON.stringify({
type: "error",
message: "Unknown message type"
}));
}
} catch (error) {
ws.send(JSON.stringify({
type: "error",
message: "Invalid JSON"
}));
}
},
// Connection closed
close: (ws, code, reason) => {
console.log(`WebSocket closed: ${code} - ${reason}`);
},
// Error handling
error: (ws, error) => {
console.error("WebSocket error:", error);
}
});
Configuration Options
typescript
const app = createServer(ServerProtocol.WEBSOCKET);
app.websocket({
// Basic handlers
open: (ws) => { /* ... */ },
message: (ws, message) => { /* ... */ },
close: (ws, code, reason) => { /* ... */ },
error: (ws, error) => { /* ... */ },
// Configuration options (conceptual - depends on Bun's WebSocket implementation)
maxPayloadLength: 16 * 1024 * 1024, // 16MB
idleTimeout: 120, // 2 minutes
compression: true,
backpressureLimit: 64 * 1024, // 64KB
// Subprotocol support
protocols: ["chat", "notification"],
// Custom headers
headers: {
"X-WebSocket-Server": "Verb"
}
});
Real-time Chat Example
Chat Server
typescript
import { createServer, ServerProtocol } from "verb";
const app = createServer(ServerProtocol.WEBSOCKET);
// Store active connections
const connections = new Set();
const rooms = new Map();
// Serve chat interface
app.get("/", (req, res) => {
res.html(`
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
<style>
#messages { height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
#messageInput { width: 80%; }
#sendBtn { width: 15%; }
</style>
</head>
<body>
<h1>WebSocket Chat</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Type a message..." />
<button id="sendBtn">Send</button>
<script>
const ws = new WebSocket('ws://localhost:3000');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
ws.onopen = () => {
addMessage('Connected to chat server');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
addMessage(\`\${data.username}: \${data.message}\`);
}
};
const sendMessage = () => {
const message = messageInput.value.trim();
if (message) {
ws.send(JSON.stringify({
type: 'message',
username: 'User',
message: message
}));
messageInput.value = '';
}
};
const addMessage = (message) => {
messages.innerHTML += \`<div>\${message}</div>\`;
messages.scrollTop = messages.scrollHeight;
};
sendBtn.onclick = sendMessage;
messageInput.onkeypress = (e) => {
if (e.key === 'Enter') sendMessage();
};
</script>
</body>
</html>
`);
});
// WebSocket handlers
app.websocket({
open: (ws) => {
console.log("User connected");
connections.add(ws);
// Send user count
broadcast({
type: "userCount",
count: connections.size
});
},
message: (ws, message) => {
try {
const data = JSON.parse(message);
if (data.type === "message") {
// Broadcast message to all connected clients
broadcast({
type: "message",
username: data.username || "Anonymous",
message: data.message,
timestamp: new Date().toISOString()
});
}
} catch (error) {
ws.send(JSON.stringify({
type: "error",
message: "Invalid message format"
}));
}
},
close: (ws) => {
console.log("User disconnected");
connections.delete(ws);
// Send updated user count
broadcast({
type: "userCount",
count: connections.size
});
}
});
// Helper function to broadcast to all connections
const broadcast = (data: any) => {
const message = JSON.stringify(data);
for (const ws of connections) {
try {
ws.send(message);
} catch (error) {
// Remove broken connections
connections.delete(ws);
}
}
};
app.listen(3000);
console.log("Chat server running on http://localhost:3000");
Room-based Chat
Multi-room Chat Server
typescript
import { createServer, ServerProtocol } from "verb";
const app = createServer(ServerProtocol.WEBSOCKET);
// Store connections and rooms
const connections = new Map(); // ws -> { userId, username, room }
const rooms = new Map(); // roomId -> Set of websockets
app.websocket({
open: (ws) => {
console.log("New connection");
},
message: (ws, message) => {
try {
const data = JSON.parse(message);
switch (data.type) {
case "join":
handleJoin(ws, data);
break;
case "leave":
handleLeave(ws, data);
break;
case "message":
handleMessage(ws, data);
break;
case "ping":
ws.send(JSON.stringify({ type: "pong" }));
break;
}
} catch (error) {
ws.send(JSON.stringify({
type: "error",
message: "Invalid message format"
}));
}
},
close: (ws) => {
const connection = connections.get(ws);
if (connection) {
handleLeave(ws, { room: connection.room });
connections.delete(ws);
}
}
});
const handleJoin = (ws: any, data: any) => {
const { username, room } = data;
// Leave previous room if any
const currentConnection = connections.get(ws);
if (currentConnection) {
handleLeave(ws, { room: currentConnection.room });
}
// Join new room
connections.set(ws, { username, room });
if (!rooms.has(room)) {
rooms.set(room, new Set());
}
rooms.get(room)!.add(ws);
// Notify room
broadcastToRoom(room, {
type: "userJoined",
username,
message: `${username} joined the room`
});
// Send room info to user
ws.send(JSON.stringify({
type: "joined",
room,
userCount: rooms.get(room)!.size
}));
};
const handleLeave = (ws: any, data: any) => {
const { room } = data;
const connection = connections.get(ws);
if (connection && rooms.has(room)) {
rooms.get(room)!.delete(ws);
// Clean up empty rooms
if (rooms.get(room)!.size === 0) {
rooms.delete(room);
} else {
broadcastToRoom(room, {
type: "userLeft",
username: connection.username,
message: `${connection.username} left the room`
});
}
}
};
function handleMessage(ws, data) {
const connection = connections.get(ws);
if (!connection) {
return ws.send(JSON.stringify({
type: "error",
message: "Not in a room"
}));
}
broadcastToRoom(connection.room, {
type: "message",
username: connection.username,
message: data.message,
timestamp: new Date().toISOString()
});
}
function broadcastToRoom(room, data) {
const message = JSON.stringify(data);
const roomConnections = rooms.get(room);
if (roomConnections) {
for (const ws of roomConnections) {
try {
ws.send(message);
} catch (error) {
// Remove broken connections
roomConnections.delete(ws);
connections.delete(ws);
}
}
}
}
app.listen(3000);
Live Data Feed
Real-time Data Server
typescript
import { createServer, ServerProtocol } from "verb";
const app = createServer(ServerProtocol.WEBSOCKET);
// Store subscribers
const subscribers = new Map(); // ws -> { topics: Set }
app.websocket({
open: (ws) => {
subscribers.set(ws, { topics: new Set() });
ws.send(JSON.stringify({
type: "connected",
message: "Ready to receive data feeds"
}));
},
message: (ws, message) => {
try {
const data = JSON.parse(message);
switch (data.type) {
case "subscribe":
handleSubscribe(ws, data.topic);
break;
case "unsubscribe":
handleUnsubscribe(ws, data.topic);
break;
case "getTopics":
sendAvailableTopics(ws);
break;
}
} catch (error) {
ws.send(JSON.stringify({
type: "error",
message: "Invalid message format"
}));
}
},
close: (ws) => {
subscribers.delete(ws);
}
});
function handleSubscribe(ws, topic) {
const subscriber = subscribers.get(ws);
if (subscriber) {
subscriber.topics.add(topic);
ws.send(JSON.stringify({
type: "subscribed",
topic,
message: `Subscribed to ${topic}`
}));
}
}
function handleUnsubscribe(ws, topic) {
const subscriber = subscribers.get(ws);
if (subscriber) {
subscriber.topics.delete(topic);
ws.send(JSON.stringify({
type: "unsubscribed",
topic,
message: `Unsubscribed from ${topic}`
}));
}
}
function sendAvailableTopics(ws) {
ws.send(JSON.stringify({
type: "topics",
topics: ["stocks", "crypto", "news", "weather"]
}));
}
// Simulate data feeds
function startDataFeeds() {
setInterval(() => {
// Stock data
publishToTopic("stocks", {
symbol: "AAPL",
price: (Math.random() * 200 + 100).toFixed(2),
change: (Math.random() * 10 - 5).toFixed(2)
});
// Crypto data
publishToTopic("crypto", {
symbol: "BTC",
price: (Math.random() * 50000 + 30000).toFixed(2),
change: (Math.random() * 2000 - 1000).toFixed(2)
});
}, 1000);
setInterval(() => {
// Weather data
publishToTopic("weather", {
location: "New York",
temperature: (Math.random() * 30 + 50).toFixed(1),
humidity: (Math.random() * 100).toFixed(1)
});
}, 5000);
}
function publishToTopic(topic, data) {
const message = JSON.stringify({
type: "data",
topic,
data,
timestamp: new Date().toISOString()
});
for (const [ws, subscriber] of subscribers) {
if (subscriber.topics.has(topic)) {
try {
ws.send(message);
} catch (error) {
subscribers.delete(ws);
}
}
}
}
startDataFeeds();
app.listen(3000);
Authentication and Authorization
Authenticated WebSocket
typescript
import { createServer, ServerProtocol } from "verb";
const app = createServer(ServerProtocol.WEBSOCKET);
// Store authenticated connections
const authenticatedConnections = new Map();
app.websocket({
open: (ws) => {
// Connection opened but not authenticated
ws.send(JSON.stringify({
type: "auth_required",
message: "Please authenticate"
}));
// Set authentication timeout
const authTimeout = setTimeout(() => {
ws.send(JSON.stringify({
type: "auth_timeout",
message: "Authentication timeout"
}));
ws.close();
}, 10000); // 10 seconds
ws.authTimeout = authTimeout;
},
message: (ws, message) => {
try {
const data = JSON.parse(message);
if (!authenticatedConnections.has(ws)) {
// Handle authentication
if (data.type === "auth") {
handleAuthentication(ws, data);
} else {
ws.send(JSON.stringify({
type: "error",
message: "Authentication required"
}));
}
} else {
// Handle authenticated messages
handleAuthenticatedMessage(ws, data);
}
} catch (error) {
ws.send(JSON.stringify({
type: "error",
message: "Invalid message format"
}));
}
},
close: (ws) => {
if (ws.authTimeout) {
clearTimeout(ws.authTimeout);
}
authenticatedConnections.delete(ws);
}
});
async function handleAuthentication(ws, data) {
const { token } = data;
try {
// Verify token (implement your own verification)
const user = await verifyToken(token);
// Clear auth timeout
if (ws.authTimeout) {
clearTimeout(ws.authTimeout);
}
// Store authenticated connection
authenticatedConnections.set(ws, {
userId: user.id,
username: user.username,
role: user.role
});
ws.send(JSON.stringify({
type: "auth_success",
user: {
id: user.id,
username: user.username,
role: user.role
}
}));
} catch (error) {
ws.send(JSON.stringify({
type: "auth_failed",
message: "Invalid token"
}));
ws.close();
}
}
function handleAuthenticatedMessage(ws, data) {
const user = authenticatedConnections.get(ws);
switch (data.type) {
case "message":
// Handle authenticated messages
broadcastMessage({
type: "message",
userId: user.userId,
username: user.username,
message: data.message,
timestamp: new Date().toISOString()
});
break;
case "admin_command":
// Admin-only commands
if (user.role === "admin") {
handleAdminCommand(ws, data);
} else {
ws.send(JSON.stringify({
type: "error",
message: "Insufficient permissions"
}));
}
break;
}
}
function broadcastMessage(data) {
const message = JSON.stringify(data);
for (const [ws, user] of authenticatedConnections) {
try {
ws.send(message);
} catch (error) {
authenticatedConnections.delete(ws);
}
}
}
async function verifyToken(token) {
// Implement your token verification logic
// This is a placeholder
if (token === "valid-token") {
return { id: "123", username: "john", role: "user" };
}
throw new Error("Invalid token");
}
app.listen(3000);
Error Handling
Connection Error Handling
typescript
const app = createServer(ServerProtocol.WEBSOCKET);
app.websocket({
open: (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
console.log("WebSocket connection opened");
},
message: (ws, message) => {
try {
const data = JSON.parse(message);
// Handle message
handleMessage(ws, data);
} catch (error) {
console.error("Message parsing error:", error);
ws.send(JSON.stringify({
type: "error",
message: "Invalid message format",
code: "INVALID_JSON"
}));
}
},
close: (ws, code, reason) => {
console.log(`WebSocket closed: ${code} - ${reason}`);
// Clean up resources
cleanupConnection(ws);
},
error: (ws, error) => {
console.error("WebSocket error:", error);
// Send error to client if connection is still open
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: "error",
message: "Connection error occurred",
code: "CONNECTION_ERROR"
}));
}
}
});
// Health check with ping/pong
setInterval(() => {
for (const [ws, connection] of connections) {
if (!ws.isAlive) {
console.log("Terminating inactive connection");
ws.terminate();
connections.delete(ws);
} else {
ws.isAlive = false;
ws.ping();
}
}
}, 30000); // Check every 30 seconds
Performance Optimization
Connection Management
typescript
const app = createServer(ServerProtocol.WEBSOCKET);
// Connection pool management
const maxConnections = 10000;
const connections = new Map();
app.websocket({
open: (ws) => {
if (connections.size >= maxConnections) {
ws.send(JSON.stringify({
type: "error",
message: "Server capacity reached"
}));
ws.close();
return;
}
connections.set(ws, {
connectedAt: Date.now(),
lastActivity: Date.now()
});
},
message: (ws, message) => {
const connection = connections.get(ws);
if (connection) {
connection.lastActivity = Date.now();
}
// Handle message
handleMessage(ws, message);
},
close: (ws) => {
connections.delete(ws);
}
});
// Clean up inactive connections
setInterval(() => {
const now = Date.now();
const timeout = 5 * 60 * 1000; // 5 minutes
for (const [ws, connection] of connections) {
if (now - connection.lastActivity > timeout) {
console.log("Closing inactive connection");
ws.close();
connections.delete(ws);
}
}
}, 60000); // Check every minute
Testing
WebSocket Testing
typescript
import { test, expect } from "bun:test";
import { createServer, ServerProtocol } from "verb";
test("WebSocket connection", async () => {
const app = createServer(ServerProtocol.WEBSOCKET);
let messageReceived = false;
app.websocket({
open: (ws) => {
ws.send("Hello");
},
message: (ws, message) => {
messageReceived = true;
expect(message).toBe("Hello back");
}
});
// Start server
const server = app.listen(3001);
// Test WebSocket connection
const ws = new WebSocket("ws://localhost:3001");
ws.onopen = () => {
ws.send("Hello back");
};
// Wait for message
await new Promise(resolve => setTimeout(resolve, 100));
expect(messageReceived).toBe(true);
ws.close();
server.stop();
});
Best Practices
- Authentication: Always authenticate WebSocket connections
- Rate Limiting: Implement rate limiting for messages
- Connection Limits: Set maximum connection limits
- Health Checks: Implement ping/pong for connection health
- Error Handling: Handle errors gracefully
- Resource Cleanup: Clean up resources on connection close
- Message Validation: Validate incoming messages
- Security: Use WSS for secure connections
- Monitoring: Monitor connection metrics
- Graceful Shutdown: Handle server shutdown properly