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

@@ -33,6 +33,8 @@ export class ServerMembershipSignalingHandler<TMessage> {
return;
}
this.migrateServerSignalUrl(roomId, resolvedSignalUrl);
this.dependencies.signalingCoordinator.setServerSignalUrl(roomId, resolvedSignalUrl);
this.dependencies.signalingCoordinator.setLastJoinedServer(resolvedSignalUrl, {
serverId: roomId,
@@ -55,6 +57,8 @@ export class ServerMembershipSignalingHandler<TMessage> {
return;
}
this.migrateServerSignalUrl(serverId, resolvedSignalUrl);
this.dependencies.signalingCoordinator.setServerSignalUrl(serverId, resolvedSignalUrl);
this.dependencies.signalingCoordinator.setLastJoinedServer(resolvedSignalUrl, {
serverId,
@@ -99,7 +103,9 @@ export class ServerMembershipSignalingHandler<TMessage> {
return;
}
for (const { signalUrl, serverIds } of this.dependencies.signalingCoordinator.getJoinedServerEntries()) {
const joinedEntries = this.dependencies.signalingCoordinator.getJoinedServerEntries();
for (const { signalUrl, serverIds } of joinedEntries) {
for (const joinedServerId of serverIds) {
this.dependencies.signalingTransport.sendRawMessageToSignalUrl(signalUrl, {
type: SIGNALING_TYPE_LEAVE_SERVER,
@@ -109,6 +115,11 @@ export class ServerMembershipSignalingHandler<TMessage> {
}
this.dependencies.signalingCoordinator.clearJoinedServers();
for (const { signalUrl } of joinedEntries) {
this.dependencies.signalingCoordinator.pruneUnusedSignalUrl(signalUrl);
}
this.dependencies.runFullCleanup();
}
@@ -116,11 +127,13 @@ export class ServerMembershipSignalingHandler<TMessage> {
const resolvedSignalUrl = this.dependencies.signalingCoordinator.getServerSignalUrl(serverId);
if (resolvedSignalUrl) {
this.dependencies.signalingCoordinator.removeJoinedServer(resolvedSignalUrl, serverId);
this.dependencies.signalingTransport.sendRawMessageToSignalUrl(resolvedSignalUrl, {
type: SIGNALING_TYPE_LEAVE_SERVER,
serverId
});
this.dependencies.signalingCoordinator.removeJoinedServer(resolvedSignalUrl, serverId);
this.dependencies.signalingCoordinator.pruneUnusedSignalUrl(resolvedSignalUrl);
} else {
this.dependencies.signalingTransport.sendRawMessage({
type: SIGNALING_TYPE_LEAVE_SERVER,
@@ -143,4 +156,26 @@ export class ServerMembershipSignalingHandler<TMessage> {
?? this.dependencies.signalingCoordinator.getServerSignalUrl(serverId)
?? this.getCurrentSignalingUrl();
}
private migrateServerSignalUrl(serverId: string, nextSignalUrl: string): void {
const previousSignalUrl = this.dependencies.signalingCoordinator.getServerSignalUrl(serverId);
if (!previousSignalUrl || previousSignalUrl === nextSignalUrl) {
return;
}
this.dependencies.signalingTransport.sendRawMessageToSignalUrl(previousSignalUrl, {
type: SIGNALING_TYPE_LEAVE_SERVER,
serverId
});
this.dependencies.signalingCoordinator.removeJoinedServer(previousSignalUrl, serverId);
this.dependencies.signalingCoordinator.pruneUnusedSignalUrl(previousSignalUrl);
this.dependencies.logger.info('Migrated server to a new signaling route', {
previousSignalUrl,
serverId,
signalUrl: nextSignalUrl
});
}
}

View File

@@ -110,6 +110,10 @@ export class ServerSignalingCoordinator<TMessage> {
this.lastJoinedServerBySignalUrl.set(signalUrl, joinedServer);
}
getLastJoinedServer(signalUrl: string): JoinedServerInfo | null {
return this.lastJoinedServerBySignalUrl.get(signalUrl) ?? null;
}
clearLastJoinedServers(): void {
this.lastJoinedServerBySignalUrl.clear();
}
@@ -156,15 +160,57 @@ export class ServerSignalingCoordinator<TMessage> {
}
removeJoinedServer(signalUrl: string, serverId: string): void {
this.getOrCreateMemberServerSet(signalUrl).delete(serverId);
const memberServerIds = this.memberServerIdsBySignalUrl.get(signalUrl);
if (!memberServerIds) {
this.repairLastJoinedServer(signalUrl, serverId);
return;
}
memberServerIds.delete(serverId);
if (memberServerIds.size === 0) {
this.memberServerIdsBySignalUrl.delete(signalUrl);
}
this.repairLastJoinedServer(signalUrl, serverId);
}
removeJoinedServerEverywhere(serverId: string): void {
for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
memberServerIds.delete(serverId);
for (const signalUrl of Array.from(this.memberServerIdsBySignalUrl.keys())) {
this.removeJoinedServer(signalUrl, serverId);
this.pruneUnusedSignalUrl(signalUrl);
}
}
pruneUnusedSignalUrl(signalUrl: string): void {
if (this.getMemberServerIdsForSignalUrl(signalUrl).size > 0) {
return;
}
this.memberServerIdsBySignalUrl.delete(signalUrl);
this.lastJoinedServerBySignalUrl.delete(signalUrl);
const subscriptions = this.signalingSubscriptions.get(signalUrl);
if (subscriptions) {
for (const subscription of subscriptions) {
subscription.unsubscribe();
}
this.signalingSubscriptions.delete(signalUrl);
}
const manager = this.signalingManagers.get(signalUrl);
if (manager) {
manager.destroy();
this.signalingManagers.delete(signalUrl);
}
this.removeSignalUrlFromPeerTracking(signalUrl);
}
getMemberServerIdsForSignalUrl(signalUrl: string): ReadonlySet<string> {
return this.memberServerIdsBySignalUrl.get(signalUrl) ?? new Set<string>();
}
@@ -344,6 +390,45 @@ export class ServerSignalingCoordinator<TMessage> {
return trackedServerIds;
}
private repairLastJoinedServer(signalUrl: string, removedServerId: string): void {
const lastJoined = this.lastJoinedServerBySignalUrl.get(signalUrl);
if (!lastJoined) {
return;
}
const memberServerIds = this.memberServerIdsBySignalUrl.get(signalUrl);
if (!memberServerIds || memberServerIds.size === 0) {
this.lastJoinedServerBySignalUrl.delete(signalUrl);
return;
}
if (lastJoined.serverId !== removedServerId && memberServerIds.has(lastJoined.serverId)) {
return;
}
const nextServerId = memberServerIds.values().next().value as string | undefined;
if (!nextServerId) {
this.lastJoinedServerBySignalUrl.delete(signalUrl);
return;
}
this.lastJoinedServerBySignalUrl.set(signalUrl, {
...lastJoined,
serverId: nextServerId
});
}
private removeSignalUrlFromPeerTracking(signalUrl: string): void {
const peerIds = new Set<string>([...this.peerKnownSignalUrls.keys(), ...this.peerServerMap.keys()]);
for (const peerId of peerIds) {
this.removePeerSignalScope(peerId, signalUrl);
}
}
private removePeerSignalScope(peerId: string, signalUrl: string): void {
const trackedSignalUrls = this.peerServerMap.get(peerId);

View File

@@ -1,6 +1,7 @@
import type { SignalingMessage } from '../../../shared-kernel';
import { PeerData } from '../realtime.types';
import {
SIGNALING_TYPE_ACCESS_DENIED,
SIGNALING_TYPE_ANSWER,
SIGNALING_TYPE_CONNECTED,
SIGNALING_TYPE_ICE_CANDIDATE,
@@ -98,6 +99,10 @@ export class IncomingSignalingMessageHandler {
this.handleIceCandidateSignalingMessage(message, signalUrl);
return;
case SIGNALING_TYPE_ACCESS_DENIED:
this.handleAccessDeniedSignalingMessage(message, signalUrl);
return;
default:
return;
}
@@ -357,6 +362,16 @@ export class IncomingSignalingMessageHandler {
this.dependencies.peerManager.handleIceCandidate(fromUserId, candidate);
}
private handleAccessDeniedSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
if (!message.serverId) {
return;
}
// Remove the server from the coordinator for this signal URL so it won't
// be re-joined on the next reconnect cycle.
this.dependencies.signalingCoordinator.removeJoinedServer(signalUrl, message.serverId);
}
private scheduleUserJoinedFallbackOffer(peerId: string, signalUrl: string, serverId?: string): void {
this.clearUserJoinedFallbackOffer(peerId);

View File

@@ -118,6 +118,13 @@ export class SignalingTransportHandler<TMessage> {
if (serverSignalUrl && this.sendRawMessageToSignalUrl(serverSignalUrl, message)) {
return;
}
this.dependencies.logger.warn('[signaling] Missing server signal route for outbound raw message', {
serverId,
type: messageType
});
return;
}
const connectedManagers = this.getConnectedSignalingManagers();
@@ -161,14 +168,14 @@ export class SignalingTransportHandler<TMessage> {
displayName: normalizedDisplayName
};
const identifyMessage = {
type: SIGNALING_TYPE_IDENTIFY,
oderId,
displayName: normalizedDisplayName
};
if (signalUrl) {
this.sendRawMessageToSignalUrl(signalUrl, identifyMessage);
this.sendRawMessageToSignalUrl(signalUrl, {
type: SIGNALING_TYPE_IDENTIFY,
oderId,
displayName: normalizedDisplayName,
connectionScope: signalUrl
});
return;
}
@@ -178,8 +185,13 @@ export class SignalingTransportHandler<TMessage> {
return;
}
for (const { manager } of connectedManagers) {
manager.sendRawMessage(identifyMessage);
for (const { signalUrl: managerSignalUrl, manager } of connectedManagers) {
manager.sendRawMessage({
type: SIGNALING_TYPE_IDENTIFY,
oderId,
displayName: normalizedDisplayName,
connectionScope: managerSignalUrl
});
}
}
}

View File

@@ -283,7 +283,8 @@ export class SignalingManager {
if (credentials) {
this.sendRawMessage({ type: SIGNALING_TYPE_IDENTIFY,
oderId: credentials.oderId,
displayName: credentials.displayName });
displayName: credentials.displayName,
connectionScope: this.lastSignalingUrl ?? undefined });
}
const memberIds = this.getMemberServerIds();
@@ -296,17 +297,10 @@ export class SignalingManager {
const lastJoined = this.getLastJoinedServer();
if (lastJoined) {
if (lastJoined && memberIds.has(lastJoined.serverId)) {
this.sendRawMessage({ type: SIGNALING_TYPE_VIEW_SERVER,
serverId: lastJoined.serverId });
}
} else {
const lastJoined = this.getLastJoinedServer();
if (lastJoined) {
this.sendRawMessage({ type: SIGNALING_TYPE_JOIN_SERVER,
serverId: lastJoined.serverId });
}
}
}