import socketio from "socket.io" import withWsAuth from "@middlewares/withWsAuth" function generateFnHandler(fn, socket) { return async (...args) => { if (typeof socket === "undefined") { socket = arguments[0] } try { fn(socket, ...args) } catch (error) { console.error(`[HANDLER_ERROR] ${error.message} >`, error.stack) if (typeof socket.emit !== "function") { return false } return socket.emit("error", { message: error.message, }) } } } class Room { constructor(io, roomName) { if (!io) { throw new Error("io is required") } this.io = io this.roomName = roomName } connections = [] limitations = { maxMessageLength: 540, } events = { "room:send:message": (socket, payload) => { let { message } = payload if (!message || typeof message !== "string") { return socket.emit("error", { message: "Invalid message", }) } if (message.length > this.limitations.maxMessageLength) { message = message.substring(0, this.limitations.maxMessageLength) } this.io.to(this.roomName).emit("room:recive:message", { timestamp: payload.timestamp ?? Date.now(), content: String(message), user: { user_id: socket.userData._id, username: socket.userData.username, fullName: socket.userData.fullName, avatar: socket.userData.avatar, }, }) } } join = (socket) => { if (socket.connectedRoom) { console.warn(`[${socket.id}][@${socket.userData.username}] already connected to room ${socket.connectedRoom}`) this.leave(socket) } socket.connectedRoom = this.roomName // join room socket.join(this.roomName) // emit to self socket.emit("room:joined", { room: this.roomName, limitations: this.limitations, connectedUsers: this.connections.map((conn) => { return conn.user_id }), }) // emit to others this.io.to(this.roomName).emit("room:user:joined", { user: { user_id: socket.userData._id, username: socket.userData.username, fullName: socket.userData.fullName, avatar: socket.userData.avatar, } }) for (const [event, fn] of Object.entries(this.events)) { const handler = generateFnHandler(fn, socket) if (!Array.isArray(socket.handlers)) { socket.handlers = [] } socket.handlers.push([event, handler]) socket.on(event, handler) } // add to connections this.connections.push(socket) console.log(`[${socket.id}][@${socket.userData.username}] joined room ${this.roomName}`) } leave = (socket) => { if (!socket.connectedRoom) { console.warn(`[${socket.id}][@${socket.userData.username}] not connected to any room`) return } if (socket.connectedRoom !== this.roomName) { console.warn(`[${socket.id}][@${socket.userData.username}] not connected to room ${this.roomName}, cannot leave`) return false } socket.leave(this.roomName) socket.emit("room:left", { room: this.roomName, }) this.io.to(this.roomName).emit("room:user:left", { user: { user_id: socket.userData._id, username: socket.userData.username, fullName: socket.userData.fullName, avatar: socket.userData.avatar, } }) for (const [event, handler] of socket.handlers) { socket.off(event, handler) } this.connections.splice(this.connections.indexOf(socket), 1) console.log(`[${socket.id}][@${socket.userData.username}] left room ${this.roomName}`) } } class RoomsController { constructor(io) { if (!io) { throw new Error("io is required") } this.io = io } rooms = [] checkRoomExists = (roomName) => { return this.rooms.some((room) => room.roomName === roomName) } createRoom = async (roomName) => { if (this.checkRoomExists(roomName)) { throw new Error(`Room ${roomName} already exists`) } const room = new Room(this.io, roomName) this.rooms.push(room) return room } connectSocketToRoom = async (socket, roomName) => { if (!this.checkRoomExists(roomName)) { await this.createRoom(roomName) } const room = this.rooms.find((room) => room.roomName === roomName) return room.join(socket) } disconnectSocketFromRoom = async (socket, roomName) => { if (!this.checkRoomExists(roomName)) { return false } const room = this.rooms.find((room) => room.roomName === roomName) return room.leave(socket) } } export default class ChatServer { constructor(server) { this.io = socketio(server, { cors: { origin: "*", methods: ["GET", "POST"], credentials: true, } }) this.RoomsController = new RoomsController(this.io) } connectionPool = [] events = { "connection": (socket) => { console.log(`[${socket.id}][${socket.userData.username}] connected to hub.`) this.connectionPool.push(socket) socket.on("disconnect", () => this.events.disconnect) // Rooms socket.on("join:room", (data) => this.RoomsController.connectSocketToRoom(socket, data.room)) socket.on("leave:room", (data) => this.RoomsController.disconnectSocketFromRoom(socket, data?.room ?? socket.connectedRoom)) }, "disconnect": (socket) => { console.log(`[${socket.id}][@${socket.userData.username}] disconnected to hub.`) if (socket.connectedRoom) { this.Rooms.leave(socket) } // remove from connection pool this.connectionPool = this.connectionPool.filter((client) => client.id !== socket.id) }, } initialize = async () => { this.io.use(withWsAuth) Object.entries(this.events).forEach(([event, handler]) => { this.io.on(event, (socket) => { try { handler(socket) } catch (error) { console.error(error) } }) }) } }