fix: Broken voice states and connectivity drops
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user