import { Server, Socket } from 'socket.io';
import { createServer } from 'http';
import express from 'express';
// ============================================
// Types
// ============================================
interface User {
id: string;
username: string;
socketId: string;
rooms: Set<string>;
status: 'online' | 'away' | 'busy';
}
interface Message {
id: string;
roomId: string;
userId: string;
username: string;
content: string;
timestamp: Date;
type: 'text' | 'system' | 'file';
}
interface Room {
id: string;
name: string;
createdBy: string;
members: Set<string>;
isPrivate: boolean;
createdAt: Date;
}
// Server -> Client events
interface ServerToClientEvents {
'user:joined': (data: { userId: string; username: string; roomId: string }) => void;
'user:left': (data: { userId: string; username: string; roomId: string }) => void;
'user:status': (data: { userId: string; status: User['status'] }) => void;
'message:new': (message: Message) => void;
'message:typing': (data: { userId: string; username: string; roomId: string }) => void;
'room:created': (room: { id: string; name: string }) => void;
'room:members': (data: { roomId: string; members: { id: string; username: string }[] }) => void;
'error': (data: { message: string; code: string }) => void;
}
// Client -> Server events
interface ClientToServerEvents {
'user:login': (data: { username: string }, callback: (response: { userId: string }) => void) => void;
'user:status': (data: { status: User['status'] }) => void;
'room:join': (data: { roomId: string }) => void;
'room:leave': (data: { roomId: string }) => void;
'room:create': (data: { name: string; isPrivate?: boolean }, callback: (response: { roomId: string }) => void) => void;
'message:send': (data: { roomId: string; content: string; type?: Message['type'] }) => void;
'message:typing': (data: { roomId: string }) => void;
}
// ============================================
// Data Store
// ============================================
class DataStore {
users = new Map<string, User>();
rooms = new Map<string, Room>();
messages = new Map<string, Message[]>(); // roomId -> messages
private nextUserId = 1;
private nextMessageId = 1;
constructor() {
// Create default room
this.createRoom('general', 'system', false);
}
createUser(username: string, socketId: string): User {
const user: User = {
id: `user-${this.nextUserId++}`,
username,
socketId,
rooms: new Set(),
status: 'online',
};
this.users.set(user.id, user);
return user;
}
getUserBySocketId(socketId: string): User | undefined {
return Array.from(this.users.values()).find(u => u.socketId === socketId);
}
removeUser(socketId: string): User | undefined {
const user = this.getUserBySocketId(socketId);
if (user) {
this.users.delete(user.id);
}
return user;
}
createRoom(name: string, createdBy: string, isPrivate: boolean): Room {
const id = name.toLowerCase().replace(/\s+/g, '-');
const room: Room = {
id,
name,
createdBy,
members: new Set(),
isPrivate,
createdAt: new Date(),
};
this.rooms.set(id, room);
this.messages.set(id, []);
return room;
}
addMessage(roomId: string, userId: string, username: string, content: string, type: Message['type'] = 'text'): Message {
const message: Message = {
id: `msg-${this.nextMessageId++}`,
roomId,
userId,
username,
content,
timestamp: new Date(),
type,
};
const messages = this.messages.get(roomId) ?? [];
messages.push(message);
this.messages.set(roomId, messages);
return message;
}
getRecentMessages(roomId: string, limit = 50): Message[] {
const messages = this.messages.get(roomId) ?? [];
return messages.slice(-limit);
}
getRoomMembers(roomId: string): User[] {
const room = this.rooms.get(roomId);
if (!room) return [];
return Array.from(room.members)
.map(id => this.users.get(id))
.filter((u): u is User => u !== undefined);
}
}
const store = new DataStore();
// ============================================
// Server Setup
// ============================================
const app = express();
const httpServer = createServer(app);
const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer, {
cors: {
origin: process.env.CORS_ORIGIN || '*',
methods: ['GET', 'POST'],
},
pingTimeout: 60000,
pingInterval: 25000,
});
// ============================================
// Middleware
// ============================================
io.use((socket, next) => {
// Add authentication here if needed
const token = socket.handshake.auth.token;
if (token) {
// Verify token in production
socket.data.authenticated = true;
}
next();
});
// ============================================
// Connection Handler
// ============================================
io.on('connection', (socket: Socket<ClientToServerEvents, ServerToClientEvents>) => {
console.log(`Socket connected: ${socket.id}`);
// User Login
socket.on('user:login', ({ username }, callback) => {
const user = store.createUser(username, socket.id);
socket.data.userId = user.id;
socket.data.username = username;
// Auto-join general room
const generalRoom = store.rooms.get('general');
if (generalRoom) {
socket.join('general');
user.rooms.add('general');
generalRoom.members.add(user.id);
// Notify room
socket.to('general').emit('user:joined', {
userId: user.id,
username,
roomId: 'general',
});
// Send system message
const systemMsg = store.addMessage('general', 'system', 'System', `${username} joined the chat`, 'system');
io.to('general').emit('message:new', systemMsg);
}
callback({ userId: user.id });
});
// Update Status
socket.on('user:status', ({ status }) => {
const user = store.getUserBySocketId(socket.id);
if (!user) return;
user.status = status;
// Notify all rooms user is in
user.rooms.forEach(roomId => {
socket.to(roomId).emit('user:status', { userId: user.id, status });
});
});
// Join Room
socket.on('room:join', ({ roomId }) => {
const user = store.getUserBySocketId(socket.id);
const room = store.rooms.get(roomId);
if (!user || !room) {
socket.emit('error', { message: 'Invalid room', code: 'INVALID_ROOM' });
return;
}
socket.join(roomId);
user.rooms.add(roomId);
room.members.add(user.id);
// Notify room
socket.to(roomId).emit('user:joined', {
userId: user.id,
username: user.username,
roomId,
});
// Send room members
const members = store.getRoomMembers(roomId).map(m => ({ id: m.id, username: m.username }));
socket.emit('room:members', { roomId, members });
// Send recent messages
const recentMessages = store.getRecentMessages(roomId);
recentMessages.forEach(msg => socket.emit('message:new', msg));
});
// Leave Room
socket.on('room:leave', ({ roomId }) => {
const user = store.getUserBySocketId(socket.id);
const room = store.rooms.get(roomId);
if (!user || !room) return;
socket.leave(roomId);
user.rooms.delete(roomId);
room.members.delete(user.id);
socket.to(roomId).emit('user:left', {
userId: user.id,
username: user.username,
roomId,
});
});
// Create Room
socket.on('room:create', ({ name, isPrivate = false }, callback) => {
const user = store.getUserBySocketId(socket.id);
if (!user) {
socket.emit('error', { message: 'Not logged in', code: 'UNAUTHORIZED' });
return;
}
const room = store.createRoom(name, user.id, isPrivate);
// Auto-join creator
socket.join(room.id);
user.rooms.add(room.id);
room.members.add(user.id);
// Notify all (for public rooms)
if (!isPrivate) {
io.emit('room:created', { id: room.id, name: room.name });
}
callback({ roomId: room.id });
});
// Send Message
socket.on('message:send', ({ roomId, content, type = 'text' }) => {
const user = store.getUserBySocketId(socket.id);
if (!user || !user.rooms.has(roomId)) {
socket.emit('error', { message: 'Not in room', code: 'NOT_IN_ROOM' });
return;
}
const message = store.addMessage(roomId, user.id, user.username, content, type);
io.to(roomId).emit('message:new', message);
});
// Typing Indicator
socket.on('message:typing', ({ roomId }) => {
const user = store.getUserBySocketId(socket.id);
if (!user || !user.rooms.has(roomId)) return;
socket.to(roomId).emit('message:typing', {
userId: user.id,
username: user.username,
roomId,
});
});
// Disconnect
socket.on('disconnect', (reason) => {
console.log(`Socket disconnected: ${socket.id} - ${reason}`);
const user = store.removeUser(socket.id);
if (user) {
user.rooms.forEach(roomId => {
const room = store.rooms.get(roomId);
if (room) {
room.members.delete(user.id);
socket.to(roomId).emit('user:left', {
userId: user.id,
username: user.username,
roomId,
});
// System message
const systemMsg = store.addMessage(roomId, 'system', 'System', `${user.username} left the chat`, 'system');
io.to(roomId).emit('message:new', systemMsg);
}
});
}
});
});
// ============================================
// HTTP Endpoints
// ============================================
app.get('/health', (_, res) => {
res.json({
status: 'healthy',
connections: io.engine.clientsCount,
rooms: store.rooms.size,
users: store.users.size,
});
});
app.get('/rooms', (_, res) => {
const publicRooms = Array.from(store.rooms.values())
.filter(r => !r.isPrivate)
.map(r => ({ id: r.id, name: r.name, members: r.members.size }));
res.json(publicRooms);
});
// ============================================
// Start Server
// ============================================
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
console.log(`🔌 WebSocket Server: http://localhost:${PORT}`);
});
export { io, store };