// Ядро: WebSocket-слой + presence. Ведёт Claude. // Клиент: ws://localhost:3000/ws?token=, затем шлёт { type:'subscribe', space_id }. // Сервер ретранслирует события bus.emit(space_id, event) всем подписчикам пространства. import { verifyToken } from './auth.js'; import { db } from './db.js'; import { bus } from './bus.js'; // space_id -> Set const rooms = new Map(); function presenceOf(spaceId) { const set = rooms.get(spaceId); if (!set) return []; return [...new Set([...set].map((c) => c.userId))]; } function broadcastPresence(spaceId) { const online = presenceOf(spaceId); send(spaceId, { type: 'presence', space_id: spaceId, payload: { online } }); } function send(spaceId, event) { const set = rooms.get(spaceId); if (!set) return; const data = JSON.stringify(event); for (const c of set) { if (c.socket.readyState === 1) c.socket.send(data); } } export function registerWs(fastify) { fastify.get('/ws', { websocket: true }, (socket, req) => { const url = new URL(req.url, 'http://localhost'); const token = url.searchParams.get('token'); const decoded = token ? verifyToken(token) : null; if (!decoded) { socket.close(4001, 'unauthorized'); return; } const conn = { socket, userId: decoded.id, spaceId: null, unsub: null }; socket.on('message', (raw) => { let msg; try { msg = JSON.parse(raw.toString()); } catch { return; } if (msg.type === 'subscribe' && msg.space_id) { // проверяем членство const member = db .prepare('SELECT id FROM space_members WHERE space_id = ? AND user_id = ?') .get(msg.space_id, conn.userId); if (!member) { socket.send(JSON.stringify({ type: 'error', payload: { code: 'FORBIDDEN' } })); return; } leave(conn); join(conn, msg.space_id); } }); socket.on('close', () => leave(conn)); }); } function join(conn, spaceId) { conn.spaceId = spaceId; if (!rooms.has(spaceId)) rooms.set(spaceId, new Set()); rooms.get(spaceId).add(conn); // подписка на события пространства из шины conn.unsub = bus.onSpace(spaceId, (event) => { if (conn.socket.readyState === 1) conn.socket.send(JSON.stringify(event)); }); broadcastPresence(spaceId); } function leave(conn) { if (conn.unsub) { conn.unsub(); conn.unsub = null; } const set = conn.spaceId && rooms.get(conn.spaceId); if (set) { set.delete(conn); if (set.size === 0) rooms.delete(conn.spaceId); else broadcastPresence(conn.spaceId); } conn.spaceId = null; }