diff --git a/backend/src/ws.ts b/backend/src/ws.ts index f3c42ef..f64b447 100644 --- a/backend/src/ws.ts +++ b/backend/src/ws.ts @@ -2,12 +2,12 @@ import { FastifyInstance } from 'fastify'; import { pool } from './db.js'; import webpush from 'web-push'; -// userId -> Set of WebSocket-like connections +// uid -> Set of WebSocket-like connections export const connections = new Map>(); -function broadcast(userIds: string[], data: object) { +function broadcast(uids: string[], data: object) { const msg = JSON.stringify(data); - for (const uid of userIds) { + for (const uid of uids) { const sockets = connections.get(uid); if (sockets) { sockets.forEach((ws: any) => { @@ -26,9 +26,9 @@ async function getChatMemberIds(chatId: string): Promise { return rows.map((r: any) => r.user_id); } -async function sendPushToOfflineUsers(userIds: string[], payload: object) { +async function sendPushToOfflineUsers(uids: string[], payload: object) { const online = new Set(connections.keys()); - const offline = userIds.filter(id => !online.has(id)); + const offline = uids.filter(id => !online.has(id)); if (offline.length === 0) return; const { rows } = await pool.query( @@ -52,23 +52,23 @@ async function sendPushToOfflineUsers(userIds: string[], payload: object) { } export function setupWebSocket(app: FastifyInstance) { - (app as any).get('/ws', { websocket: true }, (socket: any, req: any) => { - let userId: string | null = null; - + (app as any).get('/ws', { websocket: true }, (connection: any, req: any) => { + // @fastify/websocket v8 passes a Duplex stream — actual ws.WebSocket is at connection.socket + let uid: string; try { const token = req.query?.token; const decoded = app.jwt.verify<{ id: string }>(token); - userId = decoded.id; + uid = decoded.id; } catch { - socket.close(1008, 'Unauthorized'); + connection.socket.close(1008, 'Unauthorized'); return; } - const uid = userId; + const ws = connection.socket; // actual ws.WebSocket - // Register connection + // Register connection (store ws.WebSocket for broadcast) if (!connections.has(uid)) connections.set(uid, new Set()); - connections.get(uid)!.add(socket); + connections.get(uid)!.add(ws); // Update last_seen + notify online (fire-and-forget) pool.query('UPDATE users SET last_seen = NOW() WHERE id = $1', [uid]).catch(() => {}); @@ -79,7 +79,7 @@ export function setupWebSocket(app: FastifyInstance) { } }).catch(() => {}); - socket.on('message', async (raw: any, isBinary: boolean) => { + ws.on('message', async (raw: any, isBinary: boolean) => { try { const str = isBinary ? raw.toString('utf8') : raw.toString(); console.log('[WS] received:', str.substring(0, 200)); @@ -87,28 +87,28 @@ export function setupWebSocket(app: FastifyInstance) { if (type === 'typing') { const members = await getChatMemberIds(payload.chatId); - broadcast(members.filter(id => id !== userId), { + broadcast(members.filter(id => id !== uid), { type: 'typing', - payload: { chatId: payload.chatId, userId, typing: payload.typing } + payload: { chatId: payload.chatId, userId: uid, typing: payload.typing } }); } if (type === 'read_messages') { await pool.query( `UPDATE chat_members SET last_read_at = NOW() WHERE chat_id = $1 AND user_id = $2`, - [payload.chatId, userId] + [payload.chatId, uid] ); const members = await getChatMemberIds(payload.chatId); - broadcast(members.filter(id => id !== userId), { + broadcast(members.filter(id => id !== uid), { type: 'messages_read', - payload: { chatId: payload.chatId, userId } + payload: { chatId: payload.chatId, userId: uid } }); } if (type === 'send_message') { const { rows: [member] } = await pool.query( 'SELECT role, can_send_messages FROM chat_members WHERE chat_id = $1 AND user_id = $2', - [payload.chatId, userId] + [payload.chatId, uid] ); if (!member) return; @@ -120,11 +120,11 @@ export function setupWebSocket(app: FastifyInstance) { `INSERT INTO messages (chat_id, sender_id, content, type, reply_to_id) VALUES ($1, $2, $3, $4, $5) RETURNING *`, - [payload.chatId, userId, payload.content, payload.type || 'text', payload.replyToId || null] + [payload.chatId, uid, payload.content, payload.type || 'text', payload.replyToId || null] ); const { rows: [sender] } = await pool.query( - 'SELECT id, username, display_name, avatar_color FROM users WHERE id = $1', [userId] + 'SELECT id, username, display_name, avatar_color FROM users WHERE id = $1', [uid] ); const fullMsg = { @@ -145,7 +145,7 @@ export function setupWebSocket(app: FastifyInstance) { await pool.query( 'UPDATE chat_members SET last_read_at = NOW() WHERE chat_id = $1 AND user_id = $2', - [payload.chatId, userId] + [payload.chatId, uid] ); } @@ -156,9 +156,9 @@ export function setupWebSocket(app: FastifyInstance) { if (!msg) return; const { rows: [member] } = await pool.query( - 'SELECT role FROM chat_members WHERE chat_id = $1 AND user_id = $2', [msg.chat_id, userId] + 'SELECT role FROM chat_members WHERE chat_id = $1 AND user_id = $2', [msg.chat_id, uid] ); - if (msg.sender_id !== userId && !['owner', 'admin'].includes(member?.role)) return; + if (msg.sender_id !== uid && !['owner', 'admin'].includes(member?.role)) return; await pool.query('UPDATE messages SET is_deleted = TRUE WHERE id = $1', [payload.messageId]); const members = await getChatMemberIds(msg.chat_id); @@ -169,7 +169,7 @@ export function setupWebSocket(app: FastifyInstance) { const { rows: [msg] } = await pool.query( 'SELECT sender_id, chat_id FROM messages WHERE id = $1', [payload.messageId] ); - if (!msg || msg.sender_id !== userId) return; + if (!msg || msg.sender_id !== uid) return; const { rows: [updated] } = await pool.query( `UPDATE messages SET content = $1, is_edited = TRUE, edited_at = NOW() WHERE id = $2 @@ -178,7 +178,7 @@ export function setupWebSocket(app: FastifyInstance) { ); const { rows: [sender] } = await pool.query( - 'SELECT id, username, display_name, avatar_color FROM users WHERE id = $1', [userId] + 'SELECT id, username, display_name, avatar_color FROM users WHERE id = $1', [uid] ); const fullMsg = { @@ -197,10 +197,10 @@ export function setupWebSocket(app: FastifyInstance) { } }); - socket.on('close', async () => { + ws.on('close', async () => { const set = connections.get(uid); if (set) { - set.delete(socket); + set.delete(ws); if (set.size === 0) { connections.delete(uid); await pool.query('UPDATE users SET last_seen = NOW() WHERE id = $1', [uid]); @@ -210,7 +210,7 @@ export function setupWebSocket(app: FastifyInstance) { ); for (const row of memberChats) { const ids = await getChatMemberIds(row.chat_id); - broadcast(ids, { type: 'user_online', payload: { userId: uid, online: false } }); + broadcast(ids, { type: 'user_online', payload: { uid: uid, online: false } }); } } }