File size: 5,347 Bytes
87cb242 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | import WebSocket from 'ws';
import { IncomingMessage } from 'http';
import { v4 as uuidv4 } from 'uuid';
import { verifyToken, extractToken } from '../utils/jwt';
import { safeSend, parseClientMessage, ClientMessage } from '../utils/protocol';
import { setOnline, setOffline, refreshPresence } from '../redis/presenceClient';
import { flushOfflineQueue } from '../redis/messageClient';
import { registry } from '../services/connectionRegistry';
import { buildIceServers } from '../config';
import { logger } from '../utils/logger';
import { handleSignaling } from './signalingHandler';
import { handleMessage } from './messageHandler';
import { handleFileRequest } from './fileHandler';
import { handleFriend } from './friendHandler';
export async function onConnection(
ws: WebSocket,
req: IncomingMessage
): Promise<void> {
// ββ 1. Authenticate ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let uid: string;
try {
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
const token = extractToken(
req.headers.authorization,
url.searchParams.get('token') ?? undefined
);
const payload = verifyToken(token);
uid = payload.uid;
} catch (err) {
const msg = err instanceof Error ? err.message : 'Auth failed';
ws.send(JSON.stringify({ type: 'error', payload: { message: msg }, ts: Date.now() }));
ws.terminate();
return;
}
const socketId = uuidv4();
// ββ 2. Register in process registry + Redis Bucket 1 ββββββββββββββββββββ
registry.add(socketId, uid, ws);
// Redis errors must NOT crash the connection handler
try {
await setOnline(uid, socketId);
} catch (err) {
logger.error('setOnline failed', { uid, error: String(err) });
}
logger.info('Client connected', { uid, socketId, total: registry.size() });
// ββ 3. Send welcome + ICE server config βββββββββββββββββββββββββββββββββ
safeSend(ws, 'connected', { uid, socketId });
safeSend(ws, 'ice-servers', { iceServers: buildIceServers() });
// ββ 4. Flush offline message queue (Bucket 2) ββββββββββββββββββββββββββββ
try {
const pending = await flushOfflineQueue(uid);
if (pending.length > 0) {
safeSend(ws, 'offline-flush', { messages: pending, count: pending.length });
logger.info('Flushed offline queue', { uid, count: pending.length });
}
} catch (err) {
logger.error('Offline flush failed', { uid, error: String(err) });
}
// ββ 5. Message router ββββββββββββββββββββββββββββββββββββββββββββββββββββ
ws.on('message', (raw) => {
const msg = parseClientMessage(raw);
if (!msg) return; // Malformed β silently drop
registry.heartbeat(socketId); // Any message counts as a heartbeat
routeMessage(ws, uid, socketId, msg).catch((err: unknown) => {
logger.error('Message handler error', { uid, type: msg.type, error: String(err) });
});
});
// ββ 6. Cleanup on disconnect βββββββββββββββββββββββββββββββββββββββββββββ
ws.on('close', () => {
registry.remove(socketId);
setOffline(uid).catch((err: unknown) =>
logger.error('setOffline failed', { uid, error: String(err) })
);
logger.info('Client disconnected', { uid, socketId, total: registry.size() });
});
ws.on('error', (err) => {
logger.error('WebSocket error', { uid, socketId, error: err.message });
ws.terminate();
});
}
// ββ Message router ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function routeMessage(
ws: WebSocket,
uid: string,
socketId: string,
msg: ClientMessage
): Promise<void> {
switch (msg.type) {
case 'heartbeat':
await refreshPresence(uid).catch(() => {}); // Best effort
break;
case 'offer':
case 'answer':
case 'ice-candidate':
await handleSignaling(ws, uid, msg);
break;
case 'message':
await handleMessage(ws, uid, msg);
break;
case 'get-upload-url':
await handleFileRequest(ws, uid, msg);
break;
case 'relay':
await handleRelay(ws, uid, msg);
break;
case 'friend':
await handleFriend(ws, uid, msg);
break;
default:
safeSend(ws, 'error', { message: `Unknown message type: ${msg.type}` });
}
}
// ββ WebSocket relay fallback (when WebRTC ICE fails) βββββββββββββββββββββββββ
async function handleRelay(
_senderWs: WebSocket,
fromUid: string,
msg: ClientMessage
): Promise<void> {
const toUid = msg.payload.toUid as string | undefined;
if (!toUid || typeof toUid !== 'string') return;
const target = registry.getByUid(toUid);
if (!target) return;
safeSend(target.ws, 'relay', {
fromUid,
data: msg.payload.data,
}, msg.requestId);
}
|