fix: Broken voice states and connectivity drops

This commit is contained in:
2026-04-11 12:32:22 +02:00
parent 0865c2fe33
commit ef1182d46f
28 changed files with 1244 additions and 162 deletions

View File

@@ -1,10 +1,15 @@
import { Router } from 'express';
import { randomUUID } from 'crypto';
import { getAllPublicServers } from '../cqrs';
import { getReleaseManifestUrl } from '../config/variables';
import { SERVER_BUILD_VERSION } from '../generated/build-version';
import { connectedUsers } from '../websocket/state';
const router = Router();
const SERVER_INSTANCE_ID = typeof process.env.METOYOU_SERVER_INSTANCE_ID === 'string'
&& process.env.METOYOU_SERVER_INSTANCE_ID.trim().length > 0
? process.env.METOYOU_SERVER_INSTANCE_ID.trim()
: randomUUID();
function getServerProjectVersion(): string {
return typeof process.env.METOYOU_SERVER_VERSION === 'string' && process.env.METOYOU_SERVER_VERSION.trim().length > 0
@@ -20,6 +25,7 @@ router.get('/health', async (_req, res) => {
timestamp: Date.now(),
serverCount: servers.length,
connectedUsers: connectedUsers.size,
serverInstanceId: SERVER_INSTANCE_ID,
serverVersion: getServerProjectVersion(),
releaseManifestUrl: getReleaseManifestUrl()
});

View File

@@ -10,8 +10,14 @@ interface WsMessage {
export function broadcastToServer(serverId: string, message: WsMessage, excludeOderId?: string): void {
console.log(`Broadcasting to server ${serverId}, excluding ${excludeOderId}:`, message.type);
// Deduplicate by oderId so users with multiple connections (e.g. from
// different signal URLs routing to the same server) receive the
// broadcast only once.
const sentToOderIds = new Set<string>();
connectedUsers.forEach((user) => {
if (user.serverIds.has(serverId) && user.oderId !== excludeOderId) {
if (user.serverIds.has(serverId) && user.oderId !== excludeOderId && !sentToOderIds.has(user.oderId)) {
sentToOderIds.add(user.oderId);
console.log(` -> Sending to ${user.displayName} (${user.oderId})`);
user.ws.send(JSON.stringify(message));
}

View File

@@ -44,12 +44,18 @@ function sendServerUsers(user: ConnectedUser, serverId: string): void {
function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: string): void {
const newOderId = readMessageId(message['oderId']) ?? connectionId;
const newScope = typeof message['connectionScope'] === 'string' ? message['connectionScope'] : undefined;
// Close stale connections from the same identity so offer routing
// always targets the freshest socket (e.g. after page refresh).
// Close stale connections from the same identity AND the same connection
// scope so offer routing always targets the freshest socket (e.g. after
// page refresh). Connections with a *different* scope (= a different
// signal URL that happens to route to this server) are left untouched so
// multi-signal-URL setups don't trigger an eviction loop.
connectedUsers.forEach((existing, existingId) => {
if (existingId !== connectionId && existing.oderId === newOderId) {
console.log(`Closing stale connection for ${newOderId} (old=${existingId}, new=${connectionId})`);
if (existingId !== connectionId
&& existing.oderId === newOderId
&& existing.connectionScope === newScope) {
console.log(`Closing stale connection for ${newOderId} (old=${existingId}, new=${connectionId}, scope=${newScope ?? 'none'})`);
try {
existing.ws.close();
@@ -61,6 +67,7 @@ function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: s
user.oderId = newOderId;
user.displayName = normalizeDisplayName(message['displayName'], normalizeDisplayName(user.displayName));
user.connectionScope = newScope;
connectedUsers.set(connectionId, user);
console.log(`User identified: ${user.displayName} (${user.oderId})`);
}

View File

@@ -6,6 +6,13 @@ export interface ConnectedUser {
serverIds: Set<string>;
viewedServerId?: string;
displayName?: string;
/**
* Opaque scope string sent by the client (typically the signal URL it
* connected through). Stale-connection eviction only targets connections
* that share the same (oderId, connectionScope) pair, so multiple signal
* URLs routing to the same server coexist without an eviction loop.
*/
connectionScope?: string;
/** Timestamp of the last pong received (used to detect dead connections). */
lastPong: number;
}