import debug from "debug"; import express from "express"; import http from "http"; import { Server as SocketIO } from "socket.io"; type UserToFollow = { clientId: string; username: string }; type OnUserFollowedPayload = { userToFollow: UserToFollow; action: "follow" | "unfollow"; }; const serverDebug = debug("server"); const ioDebug = debug("io"); const socketDebug = debug("socket"); require("dotenv").config( process.env.NODE_ENV !== "development" ? { path: ".env.production" } : { path: ".env.development" }, ); const app = express(); const port = process.env.PORT || (process.env.NODE_ENV !== "development" ? 80 : 3002); // default port to listen app.use(express.static("public")); app.get("/", (req, res) => { res.send("Excalidraw collaboration server is up :)"); }); const server = http.createServer(app); server.listen(port, () => { serverDebug(`listening on port: ${port}`); }); try { const io = new SocketIO(server, { transports: ["websocket", "polling"], cors: { allowedHeaders: ["Content-Type", "Authorization"], origin: process.env.CORS_ORIGIN || "*", credentials: true, }, allowEIO3: true, }); io.on("connection", (socket) => { ioDebug("connection established!"); io.to(`${socket.id}`).emit("init-room"); socket.on("join-room", async (roomID) => { socketDebug(`${socket.id} has joined ${roomID}`); await socket.join(roomID); const sockets = await io.in(roomID).fetchSockets(); if (sockets.length <= 1) { io.to(`${socket.id}`).emit("first-in-room"); } else { socketDebug(`${socket.id} new-user emitted to room ${roomID}`); socket.broadcast.to(roomID).emit("new-user", socket.id); } io.in(roomID).emit( "room-user-change", sockets.map((socket) => socket.id), ); }); socket.on( "server-broadcast", (roomID: string, encryptedData: ArrayBuffer, iv: Uint8Array) => { socketDebug(`${socket.id} sends update to ${roomID}`); socket.broadcast.to(roomID).emit("client-broadcast", encryptedData, iv); }, ); socket.on( "server-volatile-broadcast", (roomID: string, encryptedData: ArrayBuffer, iv: Uint8Array) => { socketDebug(`${socket.id} sends volatile update to ${roomID}`); socket.volatile.broadcast .to(roomID) .emit("client-broadcast", encryptedData, iv); }, ); socket.on("on-user-follow", async (payload: OnUserFollowedPayload) => { const roomID = `follow_${payload.userToFollow.clientId}`; switch (payload.action) { case "follow": await socket.join(roomID); const sockets = await io.in(roomID).fetchSockets(); if (sockets.length === 1) { io.to(payload.userToFollow.clientId).emit("broadcast-follow"); } break; case "unfollow": await socket.leave(roomID); const _sockets = await io.in(roomID).fetchSockets(); if (_sockets.length === 0) { io.to(payload.userToFollow.clientId).emit("broadcast-unfollow"); } break; } }); // TODO follow-mode unfollow on disconnect? socket.on("disconnecting", async () => { socketDebug(`${socket.id} has disconnected`); for (const roomID in socket.rooms) { const otherClients = (await io.in(roomID).fetchSockets()).filter( (_socket) => _socket.id !== socket.id, ); if (otherClients.length > 0) { socket.broadcast.to(roomID).emit( "room-user-change", otherClients.map((socket) => socket.id), ); } } }); socket.on("disconnect", () => { socket.removeAllListeners(); socket.disconnect(); }); }); } catch (error) { console.error(error); }