const express = require('express'); const path = require('path'); const http = require('http'); const fs = require('fs'); const { WebSocketServer } = require('ws'); const { v4: uuidv4 } = require('uuid'); const { defaultState } = require('./defaultState'); const app = express(); const port = process.env.PORT || 3001; const subfolder = '/ag-beats'; const distPath = path.join(__dirname, 'dist'); // --- Helper function for random colors --- const COLORS = ['#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#43AA8B', '#4D908E', '#577590', '#277DA1']; const getRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)]; // --- HTTP and Static Server Setup --- app.use(subfolder, express.static(distPath)); app.get(subfolder + '/*', (req, res) => { res.sendFile(path.join(distPath, 'index.html')); }); app.get('/', (req, res) => { res.redirect(subfolder); }); const httpServer = http.createServer(app); // --- WebSocket Server --- const wss = new WebSocketServer({ path: '/ag-beats', server: httpServer }); const sessions = new Map(); wss.on('connection', (ws, req) => { const url = new URL(req.url, `http://${req.headers.host}`); const sessionId = url.searchParams.get('sessionId'); const clientId = uuidv4(); const clientColor = getRandomColor(); if (!sessionId) { return ws.close(1008, 'Session ID required'); } if (!sessions.has(sessionId)) { // Deep copy the default state to ensure each session has its own mutable state const initialState = JSON.parse(JSON.stringify(defaultState)); sessions.set(sessionId, { clients: new Map(), state: initialState }); } const session = sessions.get(sessionId); session.clients.set(clientId, { ws, color: clientColor }); console.log(`Client ${clientId} connected to session ${sessionId}`); // Welcome message with client's own ID ws.send(JSON.stringify({ type: 'welcome', payload: { clientId, } })); // Inform all clients about the current users const userList = Array.from(session.clients.entries()).map(([id, { color }]) => ({ id, color })); const userUpdateMessage = JSON.stringify({ type: 'user-update', payload: { users: userList } }); session.clients.forEach(({ ws: clientWs }) => clientWs.send(userUpdateMessage)); ws.on('message', (messageBuffer) => { const message = messageBuffer.toString(); const parsedMessage = JSON.parse(message); // Handle state requests if (parsedMessage.type === 'get_state') { ws.send(JSON.stringify({ type: 'session_state', payload: session.state })); return; } // Add sender's ID to the message for client-side identification const messageToSend = JSON.stringify({ ...parsedMessage, senderId: clientId }); // Persist state on the server, excluding cursor movements if (parsedMessage.type !== 'cursor-move') { session.state = { ...session.state, ...parsedMessage.payload }; } // Broadcast to all clients in the session, including the sender session.clients.forEach(({ ws: clientWs }) => { if (clientWs.readyState === clientWs.OPEN) { clientWs.send(messageToSend); } }); }); ws.on('close', () => { console.log(`Client ${clientId} disconnected from session ${sessionId}`); session.clients.delete(clientId); if (session.clients.size === 0) { sessions.delete(sessionId); console.log(`Session ${sessionId} closed.`); } else { // Inform remaining clients that a user has left const userList = Array.from(session.clients.keys()).map(id => ({ id, color: session.clients.get(id).color })); const userUpdateMessage = JSON.stringify({ type: 'user-update', payload: { users: userList } }); session.clients.forEach(({ ws: clientWs }) => clientWs.send(userUpdateMessage)); } }); ws.on('error', (error) => console.error(`WebSocket error for client ${clientId}:`, error)); }); httpServer.listen(port, '0.0.0.0', () => { console.log(`AG Beats server started on port ${port} with HTTP.`); if (process.send) { process.send('ready'); } });