const fs = require('fs') const path = require('path') // JsonStore: Dateibasierter Fallback-Store wenn kein MongoDB vorhanden ist. // Alle Daten liegen als JSON-Datei auf der Festplatte — kein externer Datenbankserver nötig. // Nachteil gegenüber MongoDB: Kein Concurrency-Schutz bei mehreren gleichzeitigen Schreiboperationen. class JsonStore { constructor(filePath) { this.filePath = filePath this.state = { users: [], messages: [] } // In-Memory-State — alle Operationen laufen darauf. this._ensureStore() } // Stellt sicher dass die JSON-Datei und das Verzeichnis existieren. // Wenn die Datei beschädigt ist (kein gültiges JSON), wird sie neu erstellt. _ensureStore() { const dir = path.dirname(this.filePath) fs.mkdirSync(dir, { recursive: true }) // Verzeichnis anlegen falls nicht vorhanden. if (!fs.existsSync(this.filePath)) { this._persist() return } try { const contents = fs.readFileSync(this.filePath, 'utf-8') this.state = JSON.parse(contents) // Datei in den In-Memory-State laden. } catch (error) { // Datei vorhanden aber nicht lesbar oder kein gültiges JSON — neu erstellen. console.warn('JSON store damaged, recreating', error) this._persist() } } // Schreibt den aktuellen In-Memory-State in die JSON-Datei. // null, 2 formatiert das JSON mit Einrückung — besser lesbar beim Debugging. _persist() { fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2), 'utf-8') } // Upsert: User aktualisieren wenn vorhanden, sonst neu anlegen. // Spread-Operator {...this.state.users[index], ...} übernimmt alle bestehenden Felder // und überschreibt nur die geänderten — joinedAt bleibt beim Update erhalten. async upsertUser({ id, username, room }) { const trimmed = username?.trim() if (!trimmed) { throw new Error('Username is required') } const now = new Date().toISOString() const index = this.state.users.findIndex((user) => user.id === id) if (index >= 0) { const updated = { ...this.state.users[index], username: trimmed, room, isOnline: true, lastActiveAt: now, } this.state.users[index] = updated this._persist() return updated } const user = { id, username: trimmed, room, isOnline: true, joinedAt: now, lastActiveAt: now, } this.state.users.push(user) this._persist() return user } // Object.assign mutiert das gefundene Objekt direkt im Array — kein Index-Lookup nötig. async setUserOnline(userId, room) { const user = this.state.users.find((entry) => entry.id === userId) if (!user) { return null } Object.assign(user, { isOnline: true, room, lastActiveAt: new Date().toISOString(), }) this._persist() return user } // lastActiveAt wird auch beim Offline-Setzen aktualisiert — zeigt wann der User zuletzt aktiv war. async setUserOffline(userId) { const user = this.state.users.find((entry) => entry.id === userId) if (!user) { return null } Object.assign(user, { isOnline: false, lastActiveAt: new Date().toISOString(), }) this._persist() return user } async updateUsername(userId, username) { const user = this.state.users.find((entry) => entry.id === userId) if (!user) return null user.username = username.trim() user.lastActiveAt = new Date().toISOString() this._persist() return user } // splice(index, 1) entfernt genau ein Element an der gefundenen Position. // Destructuring [removed] holt das gelöschte Element aus dem zurückgegebenen Array. async removeUser(userId) { const index = this.state.users.findIndex((entry) => entry.id === userId) if (index === -1) return null const [removed] = this.state.users.splice(index, 1) this._persist() return removed } async getUser(userId) { return this.state.users.find((entry) => entry.id === userId) || null } // .map() gibt nur die öffentlichen Felder zurück — interne Felder werden nicht nach außen gegeben. // Sortierung: Online-User zuerst, dann nach letzter Aktivität absteigend. async getParticipants(room) { return this.state.users .filter((entry) => entry.room === room) .map(({ id, username, isOnline, joinedAt, lastActiveAt }) => ({ id, username, isOnline, joinedAt, lastActiveAt, })) .sort((a, b) => { if (a.isOnline === b.isOnline) { return new Date(b.lastActiveAt) - new Date(a.lastActiveAt) } return a.isOnline ? -1 : 1 }) } // createdAt wird serverseitig gesetzt — dem Client-Timestamp wird nicht vertraut. async addMessage({ id, room, userId, username, text }) { const message = { id, room, userId, username, text, createdAt: new Date().toISOString(), } this.state.messages.push(message) this._persist() return message } // slice(-limit) gibt die letzten N Nachrichten zurück — neueste am Ende, älteste zuerst. async getMessages(room, limit = 100) { return this.state.messages .filter((message) => message.room === room) .slice(-limit) } // roomMap: Objekt das pro Room die Teilnehmerzahl zählt — schneller als mehrere filter()-Aufrufe. // localeCompare: Alphabetische Sortierung mit Sprachunterstützung (Umlaute etc.). async getRooms() { const roomMap = {} for (const user of this.state.users) { if (!roomMap[user.room]) { roomMap[user.room] = { name: user.room, participantCount: 0 } } if (user.isOnline) roomMap[user.room].participantCount++ } return Object.values(roomMap).sort((a, b) => a.name.localeCompare(b.name)) } } module.exports = JsonStore