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

@@ -21,6 +21,12 @@ import type {
ServerSourceSelector,
UnbanServerMemberRequest
} from '../domain/server-directory.models';
import {
buildRoomSignalSelector,
buildRoomSignalSource,
type RoomSignalSource,
type RoomSignalSourceInput
} from '../domain/room-signal-source';
import { ServerEndpointCompatibilityService } from '../infrastructure/server-endpoint-compatibility.service';
import { ServerEndpointHealthService } from '../infrastructure/server-endpoint-health.service';
import { ServerEndpointStateService } from './server-endpoint-state.service';
@@ -38,6 +44,7 @@ export class ServerDirectoryFacade {
private readonly endpointCompatibility = inject(ServerEndpointCompatibilityService);
private readonly endpointHealth = inject(ServerEndpointHealthService);
private readonly api = inject(ServerDirectoryApiService);
private readonly initialServerHealthCheck: Promise<void>;
private shouldSearchAllServers = true;
constructor() {
@@ -47,7 +54,11 @@ export class ServerDirectoryFacade {
this.activeServer = this.endpointState.activeServer;
this.loadConnectionSettings();
void this.testAllServers();
this.initialServerHealthCheck = this.testAllServers().catch(() => undefined);
}
async awaitInitialServerHealthCheck(): Promise<void> {
await this.initialServerHealthCheck;
}
addServer(server: { name: string; url: string }): ServerEndpoint {
@@ -110,6 +121,81 @@ export class ServerDirectoryFacade {
return !!refreshedEndpoint && refreshedEndpoint.status !== 'incompatible';
}
resolveRoomEndpoint(
source?: RoomSignalSourceInput,
options?: { ensureEndpoint?: boolean; setActive?: boolean }
): ServerEndpoint | null {
const normalizedSource = buildRoomSignalSource(source);
if (normalizedSource.sourceId) {
const endpointById = this.endpointState.resolveCanonicalEndpoint(
this.servers().find((endpoint) => endpoint.id === normalizedSource.sourceId) ?? null
);
if (endpointById) {
return endpointById;
}
}
if (!normalizedSource.sourceUrl) {
return null;
}
const endpointByUrl = this.endpointState.resolveCanonicalEndpoint(
this.findServerByUrl(normalizedSource.sourceUrl) ?? null
);
if (endpointByUrl) {
return endpointByUrl;
}
if (!options?.ensureEndpoint) {
return null;
}
return this.ensureServerEndpoint({
name: normalizedSource.sourceName ?? 'Signal Server',
url: normalizedSource.sourceUrl
}, {
setActive: options.setActive
});
}
normaliseRoomSignalSource(
source?: RoomSignalSourceInput,
options?: { ensureEndpoint?: boolean; setActive?: boolean }
): RoomSignalSource {
const endpoint = this.resolveRoomEndpoint(source, options);
return buildRoomSignalSource(source, endpoint);
}
buildRoomSignalSelector(
source?: RoomSignalSourceInput,
options?: { ensureEndpoint?: boolean; setActive?: boolean }
): ServerSourceSelector | undefined {
return buildRoomSignalSelector(this.normaliseRoomSignalSource(source, options));
}
getFallbackRoomEndpoints(source?: RoomSignalSourceInput): ServerEndpoint[] {
const primaryEndpoint = this.resolveRoomEndpoint(source);
const primarySource = this.normaliseRoomSignalSource(source);
const primaryUrl = primarySource.sourceUrl ? this.endpointState.sanitiseUrl(primarySource.sourceUrl) : null;
const primaryInstanceId = primaryEndpoint?.instanceId ?? null;
return this.activeServers().filter((endpoint) => {
if (endpoint.status === 'incompatible') {
return false;
}
if (primaryInstanceId && endpoint.instanceId === primaryInstanceId) {
return false;
}
return !primaryUrl || this.endpointState.sanitiseUrl(endpoint.url) !== primaryUrl;
});
}
setSearchAllServers(enabled: boolean): void {
this.shouldSearchAllServers = enabled;
}
@@ -159,6 +245,10 @@ export class ServerDirectoryFacade {
return this.api.getServer(serverId, selector);
}
findServerAcrossActiveEndpoints(serverId: string, source?: RoomSignalSourceInput): Observable<ServerInfo | null> {
return this.api.findServerAcrossActiveEndpoints(serverId, source);
}
registerServer(
server: Omit<ServerInfo, 'createdAt'> & { id?: string; password?: string | null },
selector?: ServerSourceSelector

View File

@@ -121,6 +121,20 @@ export class ServerEndpointStateService {
return this._servers().find((endpoint) => this.sanitiseUrl(endpoint.url) === sanitisedUrl);
}
resolveCanonicalEndpoint(endpoint: ServerEndpoint | null | undefined): ServerEndpoint | null {
if (!endpoint?.instanceId) {
return endpoint ?? null;
}
const equivalentEndpoints = this._servers().filter((candidate) => candidate.instanceId === endpoint.instanceId);
if (equivalentEndpoints.length <= 1) {
return endpoint;
}
return [...equivalentEndpoints].sort((left, right) => this.compareEndpointPreference(left, right))[0] ?? endpoint;
}
removeServer(endpointId: string): void {
const endpoints = this._servers();
const target = endpoints.find((endpoint) => endpoint.id === endpointId);
@@ -208,6 +222,7 @@ export class ServerEndpointStateService {
return {
...endpoint,
instanceId: versions?.serverInstanceId ?? endpoint.instanceId,
status,
latency,
isActive: status === 'incompatible' ? false : endpoint.isActive,
@@ -312,4 +327,50 @@ export class ServerEndpointStateService {
private saveEndpoints(): void {
this.storage.saveEndpoints(this._servers());
}
private compareEndpointPreference(left: ServerEndpoint, right: ServerEndpoint): number {
const scoreDifference = this.endpointPreferenceScore(right) - this.endpointPreferenceScore(left);
if (scoreDifference !== 0) {
return scoreDifference;
}
return left.url.localeCompare(right.url);
}
private endpointPreferenceScore(endpoint: ServerEndpoint): number {
let score = 0;
if (endpoint.isDefault) {
score += 4;
}
if (this.usesSecureProtocol(endpoint.url)) {
score += 2;
}
if (!this.isIpHost(endpoint.url)) {
score += 1;
}
return score;
}
private usesSecureProtocol(url: string): boolean {
try {
return new URL(url).protocol === 'https:';
} catch {
return false;
}
}
private isIpHost(url: string): boolean {
try {
const hostname = new URL(url).hostname;
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname) || hostname.includes(':');
} catch {
return false;
}
}
}