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 };