Realtime Chat Application
Build a realtime chat application with Azura and WebSockets
In this example, we'll build a realtime chat application using Azura's WebSocket support. This example demonstrates how to create a chat server that allows users to join rooms, send messages, and receive updates in real-time.
Key Features
This example demonstrates the following features of Azura:
WebSocket integration for realtime communication
Room-based chat system
User presence tracking
Message broadcasting
Private messaging
Typing indicators
Message history
JWT authentication for secure connections
Server Setup
Let's start by setting up the WebSocket server with Azura:
typescript
// src/index.ts
import { AzuraServer, createWebSocket } from '@atosjs/azura';
import { JwtUtil } from './utils/jwt.util';
import { ChatService } from './services/chat.service';
import { AuthMiddleware } from './middleware/auth.middleware';
// Create Azura server
const app = new AzuraServer();
// Create WebSocket server
const wss = createWebSocket(app.server);
// Create services
const jwtUtil = new JwtUtil();
const chatService = new ChatService();
const authMiddleware = new AuthMiddleware(jwtUtil);
// Handle WebSocket connections
wss.on('connection', async (socket, req) => {
try {
// Authenticate the connection
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
if (!token) {
socket.close(4001, 'Authentication required');
return;
}
// Verify token
const user = await authMiddleware.verifyToken(token);
if (!user) {
socket.close(4001, 'Invalid token');
return;
}
// Set user data on socket
socket.userId = user.id;
socket.username = user.username;
// Add user to the online users list
chatService.addUser(user.id, user.username, socket);
console.log(`User connected: ${user.username} (${user.id})`);
// Send welcome message
socket.send(JSON.stringify({
type: 'connected',
data: {
userId: user.id,
username: user.username,
message: `Welcome, ${user.username}!`
}
}));
// Broadcast user joined
chatService.broadcastSystemMessage({
type: 'userJoined',
data: {
userId: user.id,
username: user.username,
timestamp: new Date().toISOString()
}
});
// Handle messages
socket.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
handleMessage(socket, message);
} catch (error) {
console.error('Error parsing message:', error);
socket.send(JSON.stringify({
type: 'error',
data: { message: 'Invalid message format' }
}));
}
});
// Handle disconnection
socket.on('close', () => {
console.log(`User disconnected: ${user.username} (${user.id})`);
// Remove user from online users
chatService.removeUser(user.id);
// Leave all rooms
chatService.leaveAllRooms(user.id);
// Broadcast user left
chatService.broadcastSystemMessage({
type: 'userLeft',
data: {
userId: user.id,
username: user.username,
timestamp: new Date().toISOString()
}
});
});
} catch (error) {
console.error('WebSocket connection error:', error);
socket.close(4000, 'Connection error');
}
});
// Start the server
app.listen(3000);
console.log('Server running at http://localhost:3000');
Message Handling
Now, let's implement the message handling logic:
typescript
// src/services/chat.service.ts
export class ChatService {
private users = new Map();
private rooms = new Map();
private userRooms = new Map();
// User management
addUser(userId, username, socket) {
this.users.set(userId, { userId, username, socket });
this.userRooms.set(userId, new Set());
}
removeUser(userId) {
this.users.delete(userId);
this.userRooms.delete(userId);
}
getUser(userId) {
return this.users.get(userId);
}
getAllUsers() {
return Array.from(this.users.values()).map(({ userId, username }) => ({ userId, username }));
}
// Room management
createRoom(roomId, name, isPrivate = false) {
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, {
id: roomId,
name,
isPrivate,
users: new Set(),
messages: []
});
return true;
}
return false;
}
joinRoom(userId, roomId) {
const user = this.users.get(userId);
const room = this.rooms.get(roomId);
if (!user || !room) return false;
room.users.add(userId);
this.userRooms.get(userId).add(roomId);
// Notify room members
this.broadcastToRoom(roomId, {
type: 'userJoinedRoom',
data: {
roomId,
userId,
username: user.username,
timestamp: new Date().toISOString()
}
});
return true;
}
leaveRoom(userId, roomId) {
const room = this.rooms.get(roomId);
if (!room) return false;
room.users.delete(userId);
this.userRooms.get(userId)?.delete(roomId);
// Notify room members
this.broadcastToRoom(roomId, {
type: 'userLeftRoom',
data: {
roomId,
userId,
timestamp: new Date().toISOString()
}
});
// Delete room if empty and not the general room
if (room.users.size === 0 && roomId !== 'general') {
this.rooms.delete(roomId);
}
return true;
}
leaveAllRooms(userId) {
const userRooms = this.userRooms.get(userId);
if (!userRooms) return;
for (const roomId of userRooms) {
this.leaveRoom(userId, roomId);
}
}
getRoomUsers(roomId) {
const room = this.rooms.get(roomId);
if (!room) return [];
return Array.from(room.users).map(userId => {
const user = this.users.get(userId);
return { userId, username: user.username };
});
}
getRoomMessages(roomId, limit = 50) {
const room = this.rooms.get(roomId);
if (!room) return [];
return room.messages.slice(-limit);
}
// Message handling
sendMessage(userId, roomId, content) {
const user = this.users.get(userId);
const room = this.rooms.get(roomId);
if (!user || !room) return false;
// Check if user is in the room
if (!room.users.has(userId)) return false;
const message = {
id: Date.now().toString(),
userId,
username: user.username,
roomId,
content,
timestamp: new Date().toISOString()
};
// Store message
room.messages.push(message);
// Broadcast to room
this.broadcastToRoom(roomId, {
type: 'message',
data: message
});
return true;
}
sendPrivateMessage(fromUserId, toUserId, content) {
const fromUser = this.users.get(fromUserId);
const toUser = this.users.get(toUserId);
if (!fromUser || !toUser) return false;
const message = {
id: Date.now().toString(),
fromUserId,
fromUsername: fromUser.username,
toUserId,
toUsername: toUser.username,
content,
timestamp: new Date().toISOString()
};
// Send to recipient
toUser.socket.send(JSON.stringify({
type: 'privateMessage',
data: message
}));
// Send confirmation to sender
fromUser.socket.send(JSON.stringify({
type: 'privateMessageSent',
data: message
}));
return true;
}
sendTypingStatus(userId, roomId, isTyping) {
const user = this.users.get(userId);
const room = this.rooms.get(roomId);
if (!user || !room) return false;
// Check if user is in the room
if (!room.users.has(userId)) return false;
// Broadcast typing status to room
this.broadcastToRoom(roomId, {
type: 'typing',
data: {
userId,
username: user.username,
roomId,
isTyping,
timestamp: new Date().toISOString()
}
}, userId); // Exclude sender
return true;
}
// Broadcasting
broadcastToRoom(roomId, message, excludeUserId = null) {
const room = this.rooms.get(roomId);
if (!room) return;
for (const userId of room.users) {
if (excludeUserId && userId === excludeUserId) continue;
const user = this.users.get(userId);
if (user && user.socket.readyState === 1) { // WebSocket.OPEN
user.socket.send(JSON.stringify(message));
}
}
}
broadcastSystemMessage(message) {
for (const [userId, user] of this.users.entries()) {
if (user.socket.readyState === 1) { // WebSocket.OPEN
user.socket.send(JSON.stringify(message));
}
}
}
}
Client Implementation
Here's a simple client implementation using JavaScript:
javascript
// chat-client.js
class ChatClient {
constructor(url, token) {
this.url = url;
this.token = token;
this.socket = null;
this.connected = false;
this.currentRoom = null;
this.user = null;
this.eventListeners = {};
}
connect() {
return new Promise((resolve, reject) => {
this.socket = new WebSocket(`${this.url}?token=${this.token}`);
this.socket.onopen = () => {
console.log('WebSocket connection established');
this.connected = true;
};
this.socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
if (message.type === 'connected') {
this.user = {
userId: message.data.userId,
username: message.data.username
};
resolve(this.user);
}
} catch (error) {
console.error('Error parsing message:', error);
}
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
this.socket.onclose = (event) => {
console.log(`WebSocket connection closed: ${event.code} ${event.reason}`);
this.connected = false;
this.emit('disconnected', { code: event.code, reason: event.reason });
};
});
}
disconnect() {
if (this.socket) {
this.socket.close();
}
}
// Room methods
joinRoom(roomId) {
this.sendMessage({
type: 'joinRoom',
data: { roomId }
});
}
leaveRoom(roomId) {
this.sendMessage({
type: 'leaveRoom',
data: { roomId }
});
}
createRoom(name, isPrivate = false) {
this.sendMessage({
type: 'createRoom',
data: { name, isPrivate }
});
}
// Message methods
sendMessage(roomId, content) {
this.sendMessage({
type: 'message',
data: { roomId, content }
});
}
sendPrivateMessage(toUserId, content) {
this.sendMessage({
type: 'privateMessage',
data: { toUserId, content }
});
}
sendTypingStatus(roomId, isTyping) {
this.sendMessage({
type: 'typing',
data: { roomId, isTyping }
});
}
// Utility methods
getRoomUsers(roomId) {
this.sendMessage({
type: 'getRoomUsers',
data: { roomId }
});
}
getRoomMessages(roomId, limit = 50) {
this.sendMessage({
type: 'getRoomMessages',
data: { roomId, limit }
});
}
// Internal methods
sendMessage(message) {
if (!this.connected) {
throw new Error('Not connected');
}
this.socket.send(JSON.stringify(message));
}
handleMessage(message) {
// Emit event for the message type
this.emit(message.type, message.data);
// Handle specific messages
switch (message.type) {
case 'roomJoined':
this.currentRoom = message.data.roomId;
break;
case 'roomLeft':
if (this.currentRoom === message.data.roomId) {
this.currentRoom = null;
}
break;
}
}
// Event handling
on(event, callback) {
if (!this.eventListeners[event]) {
this.eventListeners[event] = [];
}
this.eventListeners[event].push(callback);
}
off(event, callback) {
if (!this.eventListeners[event]) return;
if (callback) {
this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback);
} else {
delete this.eventListeners[event];
}
}
emit(event, data) {
if (!this.eventListeners[event]) return;
for (const callback of this.eventListeners[event]) {
callback(data);
}
}
}
// Usage example
async function startChat() {
const client = new ChatClient('ws://localhost:3000/chat', 'your-jwt-token');
try {
const user = await client.connect();
console.log(`Connected as ${user.username}`);
// Join the general room
client.joinRoom('general');
// Listen for messages
client.on('message', (data) => {
console.log(`${data.username}: ${data.content}`);
});
// Listen for private messages
client.on('privateMessage', (data) => {
console.log(`[Private] ${data.fromUsername}: ${data.content}`);
});
// Listen for typing status
client.on('typing', (data) => {
console.log(`${data.username} is typing...`);
});
// Listen for errors
client.on('error', (data) => {
console.error('Error:', data.message);
});
// Send a message to the general room
client.sendMessage('general', 'Hello, everyone!');
// Send a private message
client.sendPrivateMessage('user123', 'Hello, this is a private message');
// Send typing status
client.sendTypingStatus('general', true);
setTimeout(() => {
client.sendTypingStatus('general', false);
}, 3000);
} catch (error) {
console.error('Failed to connect:', error);
}
}
startChat();
Complete Example
This is a comprehensive example of a realtime chat application built with Azura's WebSocket support. You can find the complete source code for this example in our GitHub repository.