const { randomUUID } = require('crypto') const { Server } = require('socket.io') function createRealtimeServer(server, { clientOrigin, database, onError }) { const io = new Server(server, { cors: { origin: clientOrigin, methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'], }, }) const typingByRoom = new Map() // io.use: Middleware die bei jeder eingehenden WebSocket-Verbindung läuft. // Prüft ob room und userId im Handshake vorhanden sind und ob der User in der DB existiert. // Wer vorher nicht POST /join aufgerufen hat, hat keine gültige userId und wird abgelehnt. io.use(async (socket, next) => { try { const auth = socket.handshake.auth || {} const query = socket.handshake.query || {} const room = readParam(auth.room, query.room) const userId = readParam(auth.userId, query.userId) if (!room || !userId) { return next(new Error('Missing room or userId')) } const user = await database.getUser(userId) if (!user || user.room !== room) { return next(new Error('User not authorized for this room')) } // room und userId in socket.data speichern — so sind sie in registerConnection verfügbar. socket.data.room = room socket.data.userId = userId next() } catch (error) { next(error) } }) io.on('connection', (socket) => { registerConnection(io, socket, database, onError, typingByRoom).catch((error) => { onError('Socket connection setup failed', error) socket.disconnect(true) }) }) return io } async function registerConnection(io, socket, database, onError, typingByRoom) { const { room, userId } = socket.data // socket.join: Fügt den Socket einem Room-Kanal hinzu — io.to(room).emit() erreicht nur diese Sockets. socket.join(room) const user = await database.setUserOnline(userId, room) if (!user) { socket.disconnect(true) return } socket.data.username = user.username socket.emit('connection:ready', { userId, room }) // Teilnehmerliste an alle im Room senden — User sieht sofort wer online ist. await broadcastParticipants(io, database, room) // typingByRoom: Map pro Room — speichert wer gerade tippt { userId → { userId, username } }. socket.on('typing:start', () => { if (!typingByRoom.has(room)) typingByRoom.set(room, new Map()) typingByRoom.get(room).set(userId, { userId, username: socket.data.username }) io.to(room).emit('typing:update', [...typingByRoom.get(room).values()]) }) socket.on('typing:stop', () => { typingByRoom.get(room)?.delete(userId) io.to(room).emit('typing:update', [...(typingByRoom.get(room)?.values() ?? [])]) }) socket.on('message:send', async (payload) => { try { const text = typeof payload?.text === 'string' ? payload.text : payload?.payload?.text if (!text || !text.trim()) { return } // Nochmal prüfen ob der User noch im richtigen Room ist — könnte sich seit dem Verbinden geändert haben. const activeUser = await database.getUser(userId) if (!activeUser || activeUser.room !== room) { return } // Tipp-Indikator sofort entfernen wenn eine Nachricht gesendet wird. typingByRoom.get(room)?.delete(userId) io.to(room).emit('typing:update', [...(typingByRoom.get(room)?.values() ?? [])]) const message = await database.addMessage({ id: randomUUID(), room, userId, username: activeUser.username, text: text.trim(), }) // Nachricht an alle im Room senden — inkl. Sender selbst. io.to(room).emit('message:new', message) } catch (error) { onError('Failed to handle socket message', error) } }) socket.on('disconnect', () => { typingByRoom.get(room)?.delete(userId) io.to(room).emit('typing:update', [...(typingByRoom.get(room)?.values() ?? [])]) // User als offline markieren und Teilnehmerliste aktualisieren. database .setUserOffline(userId) .then(() => broadcastParticipants(io, database, room)) .catch((error) => onError('Failed to update presence', error)) }) socket.on('error', (error) => { onError('Socket error', error) }) } async function broadcastParticipants(io, database, room) { const participants = await database.getParticipants(room) io.to(room).emit('participants:update', participants) } // Liest einen Parameter zuerst aus der primären Quelle (auth), dann aus dem Fallback (query). // Schützt gegen leere Strings — nur echte Werte werden akzeptiert. function readParam(primary, fallback) { if (typeof primary === 'string' && primary.trim()) return primary if (typeof fallback === 'string' && fallback.trim()) return fallback return null } module.exports = createRealtimeServer