import express from 'express'; import path from 'path'; import http from 'http'; import { WebSocketServer, WebSocket } from 'ws'; import { v4 as uuidv4 } from 'uuid'; import { defaultState } from '../src/defaultState.js'; import { getPersistedSession, persistSession } from './db.js'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const port = process.env.PORT || 3001; const subfolder = '/ag-beats'; const distPath = path.join(__dirname, '../dist'); const COLORS = ['#F94144', '#F3722C', '#F8961E', '#F9C74F', '#90BE6D', '#43AA8B', '#4D908E', '#577590', '#277DA1']; const getRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)]; 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); const wss = new WebSocketServer({ path: '/ag-beats', server: httpServer }); interface ClientInfo { ws: WebSocket; color: string; } interface Session { clients: Map; state: any; } const sessions = new Map(); wss.on('connection', (ws: WebSocket, 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)) { const persisted = getPersistedSession(sessionId); const initialState = persisted || 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}`); ws.send(JSON.stringify({ type: 'welcome', payload: { clientId } })); const broadcastUserUpdate = () => { const userList = Array.from(session.clients.entries()).map(([id, { color }]) => ({ id, color })); const msg = JSON.stringify({ type: 'user-update', payload: { users: userList } }); session.clients.forEach(({ ws: c }) => c.readyState === WebSocket.OPEN && c.send(msg)); }; broadcastUserUpdate(); ws.on('message', (messageBuffer) => { const message = JSON.parse(messageBuffer.toString()); if (message.type === 'get_state') { ws.send(JSON.stringify({ type: 'session_state', payload: session.state })); return; } const messageToSend = JSON.stringify({ ...message, senderId: clientId }); if (message.type !== 'cursor-move') { session.state = { ...session.state, ...message.payload }; persistSession(sessionId, session.state); } session.clients.forEach(({ ws: c }) => { if (c.readyState === WebSocket.OPEN) c.send(messageToSend); }); }); ws.on('close', () => { console.log(`Client ${clientId} disconnected from session ${sessionId}`); session.clients.delete(clientId); if (session.clients.size === 0) { // keep session in DB but remove from memory sessions.delete(sessionId); } else { broadcastUserUpdate(); } }); }); httpServer.listen(port, () => console.log(`AG Beats (TS) started on port ${port}`));