Files
ag-beats/server/index.ts
2025-12-20 18:53:49 +02:00

104 lines
3.5 KiB
TypeScript

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<string, ClientInfo>;
state: any;
}
const sessions = new Map<string, Session>();
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}`));