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

@@ -29,6 +29,7 @@ import type {
ServerSourceSelector,
UnbanServerMemberRequest
} from '../domain/server-directory.models';
import type { RoomSignalSourceInput } from '../domain/room-signal-source';
@Injectable({ providedIn: 'root' })
export class ServerDirectoryApiService {
@@ -45,14 +46,16 @@ export class ServerDirectoryApiService {
resolveEndpoint(selector?: ServerSourceSelector): ServerEndpoint | null {
if (selector?.sourceId) {
return this.endpointState.servers().find((endpoint) => endpoint.id === selector.sourceId) ?? null;
const endpoint = this.endpointState.servers().find((candidate) => candidate.id === selector.sourceId) ?? null;
return this.endpointState.resolveCanonicalEndpoint(endpoint);
}
if (selector?.sourceUrl) {
return this.endpointState.findServerByUrl(selector.sourceUrl) ?? null;
return this.endpointState.resolveCanonicalEndpoint(this.endpointState.findServerByUrl(selector.sourceUrl) ?? null);
}
return (
return this.endpointState.resolveCanonicalEndpoint(
this.endpointState.activeServer() ??
this.endpointState.servers().find((endpoint) => endpoint.status !== 'incompatible') ??
this.endpointState.servers()[0] ??
@@ -92,6 +95,45 @@ export class ServerDirectoryApiService {
);
}
findServerAcrossActiveEndpoints(serverId: string, source?: RoomSignalSourceInput): Observable<ServerInfo | null> {
const candidateEndpoints = this.getSearchableEndpoints();
const preferredSourceUrl = source?.sourceUrl ? this.endpointState.sanitiseUrl(source.sourceUrl) : undefined;
const prioritizedEndpoints = [...candidateEndpoints].sort((left, right) => {
if (preferredSourceUrl) {
const leftMatches = this.endpointState.sanitiseUrl(left.url) === preferredSourceUrl;
const rightMatches = this.endpointState.sanitiseUrl(right.url) === preferredSourceUrl;
if (leftMatches !== rightMatches) {
return leftMatches ? -1 : 1;
}
}
if (source?.sourceId) {
const leftMatches = left.id === source.sourceId;
const rightMatches = right.id === source.sourceId;
if (leftMatches !== rightMatches) {
return leftMatches ? -1 : 1;
}
}
return 0;
});
if (prioritizedEndpoints.length === 0) {
return this.getServer(serverId, this.resolveSelector(source));
}
return forkJoin(
prioritizedEndpoints.map((endpoint) => this.getServer(serverId, {
sourceId: endpoint.id,
sourceUrl: endpoint.url
}))
).pipe(
map((servers) => servers.find((server): server is ServerInfo => !!server) ?? null)
);
}
registerServer(
server: Omit<ServerInfo, 'createdAt'> & { id?: string; password?: string | null },
selector?: ServerSourceSelector
@@ -221,11 +263,17 @@ export class ServerDirectoryApiService {
}
private resolveBaseServerUrl(selector?: ServerSourceSelector): string {
const resolvedEndpoint = this.resolveEndpoint(selector);
if (resolvedEndpoint) {
return resolvedEndpoint.url;
}
if (selector?.sourceUrl) {
return this.endpointState.sanitiseUrl(selector.sourceUrl);
}
return this.resolveEndpoint(selector)?.url ?? this.endpointState.getPrimaryDefaultServerUrl();
return this.endpointState.getPrimaryDefaultServerUrl();
}
private unwrapServersResponse(response: { servers: ServerInfo[]; total: number } | ServerInfo[]): ServerInfo[] {
@@ -245,7 +293,7 @@ export class ServerDirectoryApiService {
}
private searchAllEndpoints(query: string): Observable<ServerInfo[]> {
const onlineEndpoints = this.endpointState.activeServers().filter((endpoint) => endpoint.status !== 'offline');
const onlineEndpoints = this.getSearchableEndpoints();
if (onlineEndpoints.length === 0) {
return this.searchSingleEndpoint(query, this.getApiBaseUrl(), this.endpointState.activeServer());
@@ -258,7 +306,7 @@ export class ServerDirectoryApiService {
}
private getAllServersFromAllEndpoints(): Observable<ServerInfo[]> {
const onlineEndpoints = this.endpointState.activeServers().filter((endpoint) => endpoint.status !== 'offline');
const onlineEndpoints = this.getSearchableEndpoints();
if (onlineEndpoints.length === 0) {
return this.http.get<{ servers: ServerInfo[]; total: number }>(`${this.getApiBaseUrl()}/servers`).pipe(
@@ -277,6 +325,30 @@ export class ServerDirectoryApiService {
).pipe(map((resultArrays) => resultArrays.flat()));
}
private getSearchableEndpoints(): ServerEndpoint[] {
const activeEndpoints = this.endpointState.activeServers().filter((endpoint) => endpoint.status !== 'offline');
if (activeEndpoints.length > 0) {
return activeEndpoints;
}
const activeServer = this.endpointState.activeServer();
return activeServer ? [activeServer] : [];
}
private resolveSelector(source?: RoomSignalSourceInput): ServerSourceSelector | undefined {
if (source?.sourceId) {
return { sourceId: source.sourceId };
}
if (source?.sourceUrl) {
return { sourceUrl: source.sourceUrl };
}
return undefined;
}
private deduplicateById<T extends { id: string }>(items: T[]): T[] {
const seen = new Set<string>();
@@ -298,13 +370,14 @@ export class ServerDirectoryApiService {
const candidate = server as Record<string, unknown>;
const sourceName = this.getStringValue(candidate['sourceName']);
const sourceUrl = this.getStringValue(candidate['sourceUrl']);
const resolvedSource = this.endpointState.resolveCanonicalEndpoint(source);
return {
id: this.getStringValue(candidate['id']) ?? '',
name: this.getStringValue(candidate['name']) ?? 'Unnamed server',
description: this.getStringValue(candidate['description']),
topic: this.getStringValue(candidate['topic']),
hostName: this.getStringValue(candidate['hostName']) ?? sourceName ?? source?.name ?? 'Unknown API',
hostName: this.getStringValue(candidate['hostName']) ?? sourceName ?? resolvedSource?.name ?? 'Unknown API',
ownerId: this.getStringValue(candidate['ownerId']),
ownerName: this.getStringValue(candidate['ownerName']),
ownerPublicKey: this.getStringValue(candidate['ownerPublicKey']),
@@ -319,9 +392,13 @@ export class ServerDirectoryApiService {
roleAssignments: this.getRoleAssignmentsValue(candidate['roleAssignments']),
channelPermissions: this.getChannelPermissionOverridesValue(candidate['channelPermissions']),
createdAt: this.getNumberValue(candidate['createdAt'], Date.now()),
sourceId: this.getStringValue(candidate['sourceId']) ?? source?.id,
sourceName: sourceName ?? source?.name,
sourceUrl: sourceUrl ? this.endpointState.sanitiseUrl(sourceUrl) : source ? this.endpointState.sanitiseUrl(source.url) : undefined
sourceId: this.getStringValue(candidate['sourceId']) ?? resolvedSource?.id,
sourceName: sourceName ?? resolvedSource?.name,
sourceUrl: sourceUrl
? this.endpointState.sanitiseUrl(sourceUrl)
: resolvedSource
? this.endpointState.sanitiseUrl(resolvedSource.url)
: undefined
};
}

View File

@@ -30,12 +30,16 @@ export class ServerEndpointHealthService {
payload.serverVersion,
clientVersion
);
const serverInstanceId = typeof payload.serverInstanceId === 'string' && payload.serverInstanceId.trim().length > 0
? payload.serverInstanceId.trim()
: undefined;
if (!versionCompatibility.isCompatible) {
return {
status: 'incompatible',
latency,
versions: {
serverInstanceId,
serverVersion: versionCompatibility.serverVersion,
clientVersion
}
@@ -46,6 +50,7 @@ export class ServerEndpointHealthService {
status: 'online',
latency,
versions: {
serverInstanceId,
serverVersion: versionCompatibility.serverVersion,
clientVersion
}