we-superapp / apps /api /src /core /ws.js
nenbuj
Initial deploy: «МЫ» супер-апп
c562e21
Raw History Blame Contribute Delete
2.77 kB
// Ядро: WebSocket-слой + presence. Ведёт Claude.
// Клиент: ws://localhost:3000/ws?token=<jwt>, затем шлёт { 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<connection state>
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;
}