Total refactoring performed
This commit is contained in:
24
server/db.ts
Normal file
24
server/db.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
const db = new Database('session_state.db');
|
||||
|
||||
// Initialize tables
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
state TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
const getSessionStmt = db.prepare('SELECT state FROM sessions WHERE id = ?');
|
||||
const upsertSessionStmt = db.prepare('INSERT INTO sessions (id, state) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET state = excluded.state, updated_at = CURRENT_TIMESTAMP');
|
||||
|
||||
export function getPersistedSession(id: string) {
|
||||
const row = getSessionStmt.get(id) as { state: string } | undefined;
|
||||
return row ? JSON.parse(row.state) : null;
|
||||
}
|
||||
|
||||
export function persistSession(id: string, state: any) {
|
||||
upsertSessionStmt.run(id, JSON.stringify(state));
|
||||
}
|
||||
103
server/index.ts
Normal file
103
server/index.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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}`));
|
||||
Reference in New Issue
Block a user