'use strict'; /* * VR ProgrammierHub — Backend-Grundgeruest (Phase 1) * Ein einziger Node-Prozess: liefert die Frontend-Assets aus UND haelt den * Echtzeit-Raumzustand (WebSocket-Hub, komplett im RAM). * * Protokoll siehe Spezifikation Abschnitt 9.2. Auth (Login/Rollen) folgt als * naechster Schritt — hier zunaechst Name + Rolle aus dem "hello"-Frame. */ const express = require('express'); const http = require('http'); const path = require('path'); const { WebSocketServer } = require('ws'); const PORT = process.env.PORT || 3000; // ── HTTP: statische Assets ────────────────────────────────────────────── const app = express(); app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] })); app.get('/health', (_req, res) => res.json({ ok: true, users: clients.size, uptime: process.uptime() })); const server = http.createServer(app); // ── Realtime-Hub: fluechtiger Raumzustand im Arbeitsspeicher ──────────── const wss = new WebSocketServer({ server, path: '/ws' }); let nextId = 1; /** id -> { ws, alive, state } */ const clients = new Map(); function send(ws, obj) { if (ws.readyState === 1) ws.send(JSON.stringify(obj)); } function broadcast(obj, exceptId) { const msg = JSON.stringify(obj); for (const [id, c] of clients) if (id !== exceptId && c.ws.readyState === 1) c.ws.send(msg); } wss.on('connection', (ws) => { const id = nextId++; const state = { id, name: '…', role: 'student', p: [0, 0, 8], rot: 0, anim: 'idle', label: '', help: false }; const client = { ws, alive: true, state, joined: false }; clients.set(id, client); ws.on('pong', () => (client.alive = true)); ws.on('message', (data) => { let m; try { m = JSON.parse(data); } catch { return; } switch (m.t) { case 'hello': { state.name = String(m.name || 'Gast').slice(0, 24) || 'Gast'; state.role = m.role === 'teacher' ? 'teacher' : 'student'; if (Array.isArray(m.p) && m.p.length === 3) state.p = m.p.map(Number); // eigene id + Vollzustand aller anderen an den neuen Client send(ws, { t: 'welcome', id, users: [...clients.values()].filter((c) => c.state.id !== id).map((c) => c.state) }); client.joined = true; broadcast({ t: 'join', user: state }, id); // andere ueber Neuzugang informieren break; } case 'move': { if (!client.joined) break; if (Array.isArray(m.p) && m.p.length === 3) state.p = m.p.map(Number); if (typeof m.rot === 'number') state.rot = m.rot; if (typeof m.anim === 'string') state.anim = m.anim; broadcast({ t: 'update', id, p: state.p, rot: state.rot, anim: state.anim }, id); break; } case 'status': { state.label = String(m.label || '').slice(0, 40); broadcast({ t: 'status', id, label: state.label }, id); break; } case 'help': { state.help = !!m.on; broadcast({ t: 'help', id, on: state.help }, id); break; } } }); ws.on('close', () => { clients.delete(id); broadcast({ t: 'leave', id }); }); ws.on('error', () => {}); }); // ── Heartbeat: tote Verbindungen aufraeumen ───────────────────────────── setInterval(() => { for (const [id, c] of clients) { if (!c.alive) { try { c.ws.terminate(); } catch {} clients.delete(id); broadcast({ t: 'leave', id }); continue; } c.alive = false; try { c.ws.ping(); } catch {} } }, 15000); server.listen(PORT, () => console.log(`[ProgrammierHub] laeuft auf :${PORT} (WS-Pfad /ws)`));