fix: [Experimental hotfix 1] Fix Signaling issues Toju App
1. Server: WebSocket ping/pong heartbeat (index.ts) Added a 30-second ping interval that pings all connected clients Connections without a pong response for 45 seconds are terminated and cleaned up Extracted removeDeadConnection() to deduplicate the cleanup logic between close events and dead connection reaping 2. Server: Fixed sendServerUsers filter bug (handler.ts:13) Removed && cu.displayName from the filter — users who joined a server before their identify message was processed were silently invisible to everyone. This was the direct cause of "can't see each other" in session 2. 3. Client: Typing message now includes serverId Added serverId: this.webrtc.currentServerId to the typing payload Added a currentServerId getter on WebRTCService
This commit is contained in:
@@ -10,7 +10,7 @@ interface WsMessage {
|
|||||||
/** Sends the current user list for a given server to a single connected user. */
|
/** Sends the current user list for a given server to a single connected user. */
|
||||||
function sendServerUsers(user: ConnectedUser, serverId: string): void {
|
function sendServerUsers(user: ConnectedUser, serverId: string): void {
|
||||||
const users = Array.from(connectedUsers.values())
|
const users = Array.from(connectedUsers.values())
|
||||||
.filter(cu => cu.serverIds.has(serverId) && cu.oderId !== user.oderId && cu.displayName)
|
.filter(cu => cu.serverIds.has(serverId) && cu.oderId !== user.oderId)
|
||||||
.map(cu => ({ oderId: cu.oderId, displayName: cu.displayName ?? 'Anonymous' }));
|
.map(cu => ({ oderId: cu.oderId, displayName: cu.displayName ?? 'Anonymous' }));
|
||||||
|
|
||||||
user.ws.send(JSON.stringify({ type: 'server_users', serverId, users }));
|
user.ws.send(JSON.stringify({ type: 'server_users', serverId, users }));
|
||||||
|
|||||||
@@ -9,13 +9,73 @@ import { connectedUsers } from './state';
|
|||||||
import { broadcastToServer } from './broadcast';
|
import { broadcastToServer } from './broadcast';
|
||||||
import { handleWebSocketMessage } from './handler';
|
import { handleWebSocketMessage } from './handler';
|
||||||
|
|
||||||
|
/** How often to ping all connected clients (ms). */
|
||||||
|
const PING_INTERVAL_MS = 30_000;
|
||||||
|
/** Maximum time a client can go without a pong before we consider it dead (ms). */
|
||||||
|
const PONG_TIMEOUT_MS = 45_000;
|
||||||
|
|
||||||
|
function removeDeadConnection(connectionId: string): void {
|
||||||
|
const user = connectedUsers.get(connectionId);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
console.log(`Removing dead connection: ${user.displayName ?? 'Unknown'} (${user.oderId})`);
|
||||||
|
|
||||||
|
user.serverIds.forEach((sid) => {
|
||||||
|
broadcastToServer(sid, {
|
||||||
|
type: 'user_left',
|
||||||
|
oderId: user.oderId,
|
||||||
|
displayName: user.displayName,
|
||||||
|
serverId: sid
|
||||||
|
}, user.oderId);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
user.ws.terminate();
|
||||||
|
} catch {
|
||||||
|
console.warn(`Failed to terminate WebSocket for ${user.displayName ?? 'Unknown'} (${user.oderId})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedUsers.delete(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
export function setupWebSocket(server: Server<typeof IncomingMessage, typeof ServerResponse>): void {
|
export function setupWebSocket(server: Server<typeof IncomingMessage, typeof ServerResponse>): void {
|
||||||
const wss = new WebSocketServer({ server });
|
const wss = new WebSocketServer({ server });
|
||||||
|
// Periodically ping all clients and reap dead connections
|
||||||
|
const pingInterval = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
connectedUsers.forEach((user, connectionId) => {
|
||||||
|
if (now - user.lastPong > PONG_TIMEOUT_MS) {
|
||||||
|
removeDeadConnection(connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.ws.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
user.ws.ping();
|
||||||
|
} catch {
|
||||||
|
console.warn(`Failed to ping client ${user.displayName ?? 'Unknown'} (${user.oderId})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, PING_INTERVAL_MS);
|
||||||
|
|
||||||
|
wss.on('close', () => clearInterval(pingInterval));
|
||||||
|
|
||||||
wss.on('connection', (ws: WebSocket) => {
|
wss.on('connection', (ws: WebSocket) => {
|
||||||
const connectionId = uuidv4();
|
const connectionId = uuidv4();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
connectedUsers.set(connectionId, { oderId: connectionId, ws, serverIds: new Set() });
|
connectedUsers.set(connectionId, { oderId: connectionId, ws, serverIds: new Set(), lastPong: now });
|
||||||
|
|
||||||
|
ws.on('pong', () => {
|
||||||
|
const user = connectedUsers.get(connectionId);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
user.lastPong = Date.now();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
@@ -28,20 +88,7 @@ export function setupWebSocket(server: Server<typeof IncomingMessage, typeof Ser
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
const user = connectedUsers.get(connectionId);
|
removeDeadConnection(connectionId);
|
||||||
|
|
||||||
if (user) {
|
|
||||||
user.serverIds.forEach((sid) => {
|
|
||||||
broadcastToServer(sid, {
|
|
||||||
type: 'user_left',
|
|
||||||
oderId: user.oderId,
|
|
||||||
displayName: user.displayName,
|
|
||||||
serverId: sid
|
|
||||||
}, user.oderId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedUsers.delete(connectionId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'connected', connectionId, serverTime: Date.now() }));
|
ws.send(JSON.stringify({ type: 'connected', connectionId, serverTime: Date.now() }));
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ export interface ConnectedUser {
|
|||||||
serverIds: Set<string>;
|
serverIds: Set<string>;
|
||||||
viewedServerId?: string;
|
viewedServerId?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
/** Timestamp of the last pong received (used to detect dead connections). */
|
||||||
|
lastPong: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -513,6 +513,11 @@ export class WebRTCService implements OnDestroy {
|
|||||||
this.activeServerId = serverId;
|
this.activeServerId = serverId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The server ID currently being viewed / active, or `null`. */
|
||||||
|
get currentServerId(): string | null {
|
||||||
|
return this.activeServerId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an identify message to the signaling server.
|
* Send an identify message to the signaling server.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export class ChatMessagesComponent {
|
|||||||
|
|
||||||
handleTypingStarted(): void {
|
handleTypingStarted(): void {
|
||||||
try {
|
try {
|
||||||
this.webrtc.sendRawMessage({ type: 'typing' });
|
this.webrtc.sendRawMessage({ type: 'typing', serverId: this.webrtc.currentServerId });
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user