Add eslint

This commit is contained in:
2026-03-03 22:56:12 +01:00
parent d641229f9d
commit ad0e28bf84
92 changed files with 2656 additions and 1127 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering, @angular-eslint/prefer-inject, @typescript-eslint/no-invalid-void-type */
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, of, throwError, forkJoin } from 'rxjs';
@@ -27,7 +28,6 @@ export interface ServerEndpoint {
/** localStorage key that persists the user's configured endpoints. */
const ENDPOINTS_STORAGE_KEY = 'metoyou_server_endpoints';
/** Timeout (ms) for server health-check and alternative-endpoint pings. */
const HEALTH_CHECK_TIMEOUT_MS = 5000;
@@ -38,8 +38,10 @@ const HEALTH_CHECK_TIMEOUT_MS = 5000;
function buildDefaultServerUrl(): string {
if (typeof window !== 'undefined' && window.location) {
const protocol = window.location.protocol === 'https:' ? 'https' : 'http';
return `${protocol}://localhost:3001`;
}
return 'http://localhost:3001';
}
@@ -49,7 +51,7 @@ const DEFAULT_ENDPOINT: Omit<ServerEndpoint, 'id'> = {
url: buildDefaultServerUrl(),
isActive: true,
isDefault: true,
status: 'unknown',
status: 'unknown'
};
/**
@@ -72,7 +74,7 @@ export class ServerDirectoryService {
/** The currently active endpoint, falling back to the first in the list. */
readonly activeServer = computed(
() => this._servers().find((endpoint) => endpoint.isActive) ?? this._servers()[0],
() => this._servers().find((endpoint) => endpoint.isActive) ?? this._servers()[0]
);
constructor(private readonly http: HttpClient) {
@@ -92,8 +94,9 @@ export class ServerDirectoryService {
url: sanitisedUrl,
isActive: false,
isDefault: false,
status: 'unknown',
status: 'unknown'
};
this._servers.update((endpoints) => [...endpoints, newEndpoint]);
this.saveEndpoints();
}
@@ -106,17 +109,23 @@ export class ServerDirectoryService {
removeServer(endpointId: string): void {
const endpoints = this._servers();
const target = endpoints.find((endpoint) => endpoint.id === endpointId);
if (target?.isDefault) return;
if (target?.isDefault)
return;
const wasActive = target?.isActive;
this._servers.update((list) => list.filter((endpoint) => endpoint.id !== endpointId));
if (wasActive) {
this._servers.update((list) => {
if (list.length > 0) list[0].isActive = true;
if (list.length > 0)
list[0].isActive = true;
return [...list];
});
}
this.saveEndpoints();
}
@@ -125,8 +134,8 @@ export class ServerDirectoryService {
this._servers.update((endpoints) =>
endpoints.map((endpoint) => ({
...endpoint,
isActive: endpoint.id === endpointId,
})),
isActive: endpoint.id === endpointId
}))
);
this.saveEndpoints();
}
@@ -135,12 +144,12 @@ export class ServerDirectoryService {
updateServerStatus(
endpointId: string,
status: ServerEndpoint['status'],
latency?: number,
latency?: number
): void {
this._servers.update((endpoints) =>
endpoints.map((endpoint) =>
endpoint.id === endpointId ? { ...endpoint, status, latency } : endpoint,
),
endpoint.id === endpointId ? { ...endpoint, status, latency } : endpoint
)
);
this.saveEndpoints();
}
@@ -158,7 +167,9 @@ export class ServerDirectoryService {
*/
async testServer(endpointId: string): Promise<boolean> {
const endpoint = this._servers().find((entry) => entry.id === endpointId);
if (!endpoint) return false;
if (!endpoint)
return false;
this.updateServerStatus(endpointId, 'checking');
const startTime = Date.now();
@@ -166,7 +177,7 @@ export class ServerDirectoryService {
try {
const response = await fetch(`${endpoint.url}/api/health`, {
method: 'GET',
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
});
const latency = Date.now() - startTime;
@@ -174,6 +185,7 @@ export class ServerDirectoryService {
this.updateServerStatus(endpointId, 'online', latency);
return true;
}
this.updateServerStatus(endpointId, 'offline');
return false;
} catch {
@@ -181,14 +193,16 @@ export class ServerDirectoryService {
try {
const response = await fetch(`${endpoint.url}/api/servers`, {
method: 'GET',
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
});
const latency = Date.now() - startTime;
if (response.ok) {
this.updateServerStatus(endpointId, 'online', latency);
return true;
}
} catch { /* both checks failed */ }
this.updateServerStatus(endpointId, 'offline');
return false;
}
@@ -197,6 +211,7 @@ export class ServerDirectoryService {
/** Probe all configured endpoints in parallel. */
async testAllServers(): Promise<void> {
const endpoints = this._servers();
await Promise.all(endpoints.map((endpoint) => this.testServer(endpoint.id)));
}
@@ -208,10 +223,13 @@ export class ServerDirectoryService {
/** Get the WebSocket URL derived from the active endpoint. */
getWebSocketUrl(): string {
const active = this.activeServer();
if (!active) {
const protocol = (typeof window !== 'undefined' && window.location?.protocol === 'https:') ? 'wss' : 'ws';
return `${protocol}://localhost:3001`;
}
return active.url.replace(/^http/, 'ws');
}
@@ -224,6 +242,7 @@ export class ServerDirectoryService {
if (this.shouldSearchAllServers) {
return this.searchAllEndpoints(query);
}
return this.searchSingleEndpoint(query, this.buildApiBaseUrl());
}
@@ -232,6 +251,7 @@ export class ServerDirectoryService {
if (this.shouldSearchAllServers) {
return this.getAllServersFromAllEndpoints();
}
return this.http
.get<{ servers: ServerInfo[]; total: number }>(`${this.buildApiBaseUrl()}/servers`)
.pipe(
@@ -239,7 +259,7 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to get servers:', error);
return of([]);
}),
})
);
}
@@ -251,13 +271,13 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to get server:', error);
return of(null);
}),
})
);
}
/** Register a new server listing in the directory. */
registerServer(
server: Omit<ServerInfo, 'createdAt'> & { id?: string },
server: Omit<ServerInfo, 'createdAt'> & { id?: string }
): Observable<ServerInfo> {
return this.http
.post<ServerInfo>(`${this.buildApiBaseUrl()}/servers`, server)
@@ -265,14 +285,14 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to register server:', error);
return throwError(() => error);
}),
})
);
}
/** Update an existing server listing. */
updateServer(
serverId: string,
updates: Partial<ServerInfo>,
updates: Partial<ServerInfo>
): Observable<ServerInfo> {
return this.http
.patch<ServerInfo>(`${this.buildApiBaseUrl()}/servers/${serverId}`, updates)
@@ -280,7 +300,7 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to update server:', error);
return throwError(() => error);
}),
})
);
}
@@ -292,7 +312,7 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to unregister server:', error);
return throwError(() => error);
}),
})
);
}
@@ -304,24 +324,24 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to get server users:', error);
return of([]);
}),
})
);
}
/** Send a join request for a server and receive the signaling URL. */
requestJoin(
request: JoinRequest,
request: JoinRequest
): Observable<{ success: boolean; signalingUrl?: string }> {
return this.http
.post<{ success: boolean; signalingUrl?: string }>(
`${this.buildApiBaseUrl()}/servers/${request.roomId}/join`,
request,
request
)
.pipe(
catchError((error) => {
console.error('Failed to send join request:', error);
return throwError(() => error);
}),
})
);
}
@@ -333,7 +353,7 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to notify leave:', error);
return of(undefined);
}),
})
);
}
@@ -345,7 +365,7 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to update user count:', error);
return of(undefined);
}),
})
);
}
@@ -357,7 +377,7 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to send heartbeat:', error);
return of(undefined);
}),
})
);
}
@@ -368,19 +388,24 @@ export class ServerDirectoryService {
private buildApiBaseUrl(): string {
const active = this.activeServer();
const rawUrl = active ? active.url : buildDefaultServerUrl();
let base = rawUrl.replace(/\/+$/, '');
if (base.toLowerCase().endsWith('/api')) {
base = base.slice(0, -4);
}
return `${base}/api`;
}
/** Strip trailing slashes and `/api` suffix from a URL. */
private sanitiseUrl(rawUrl: string): string {
let cleaned = rawUrl.trim().replace(/\/+$/, '');
if (cleaned.toLowerCase().endsWith('/api')) {
cleaned = cleaned.slice(0, -4);
}
return cleaned;
}
@@ -389,18 +414,21 @@ export class ServerDirectoryService {
* response shapes from the directory API.
*/
private unwrapServersResponse(
response: { servers: ServerInfo[]; total: number } | ServerInfo[],
response: { servers: ServerInfo[]; total: number } | ServerInfo[]
): ServerInfo[] {
if (Array.isArray(response)) return response;
if (Array.isArray(response))
return response;
return response.servers ?? [];
}
/** Search a single endpoint for servers matching a query. */
private searchSingleEndpoint(
query: string,
apiBaseUrl: string,
apiBaseUrl: string
): Observable<ServerInfo[]> {
const params = new HttpParams().set('q', query);
return this.http
.get<{ servers: ServerInfo[]; total: number }>(`${apiBaseUrl}/servers`, { params })
.pipe(
@@ -408,14 +436,14 @@ export class ServerDirectoryService {
catchError((error) => {
console.error('Failed to search servers:', error);
return of([]);
}),
})
);
}
/** Fan-out search across all non-offline endpoints, deduplicating results. */
private searchAllEndpoints(query: string): Observable<ServerInfo[]> {
const onlineEndpoints = this._servers().filter(
(endpoint) => endpoint.status !== 'offline',
(endpoint) => endpoint.status !== 'offline'
);
if (onlineEndpoints.length === 0) {
@@ -428,22 +456,22 @@ export class ServerDirectoryService {
results.map((server) => ({
...server,
sourceId: endpoint.id,
sourceName: endpoint.name,
})),
),
),
sourceName: endpoint.name
}))
)
)
);
return forkJoin(requests).pipe(
map((resultArrays) => resultArrays.flat()),
map((servers) => this.deduplicateById(servers)),
map((servers) => this.deduplicateById(servers))
);
}
/** Retrieve all servers from all non-offline endpoints. */
private getAllServersFromAllEndpoints(): Observable<ServerInfo[]> {
const onlineEndpoints = this._servers().filter(
(endpoint) => endpoint.status !== 'offline',
(endpoint) => endpoint.status !== 'offline'
);
if (onlineEndpoints.length === 0) {
@@ -451,7 +479,7 @@ export class ServerDirectoryService {
.get<{ servers: ServerInfo[]; total: number }>(`${this.buildApiBaseUrl()}/servers`)
.pipe(
map((response) => this.unwrapServersResponse(response)),
catchError(() => of([])),
catchError(() => of([]))
);
}
@@ -461,14 +489,15 @@ export class ServerDirectoryService {
.pipe(
map((response) => {
const results = this.unwrapServersResponse(response);
return results.map((server) => ({
...server,
sourceId: endpoint.id,
sourceName: endpoint.name,
sourceName: endpoint.name
}));
}),
catchError(() => of([] as ServerInfo[])),
),
catchError(() => of([] as ServerInfo[]))
)
);
return forkJoin(requests).pipe(map((resultArrays) => resultArrays.flat()));
@@ -477,8 +506,11 @@ export class ServerDirectoryService {
/** Remove duplicate servers (by `id`), keeping the first occurrence. */
private deduplicateById<T extends { id: string }>(items: T[]): T[] {
const seen = new Set<string>();
return items.filter((item) => {
if (seen.has(item.id)) return false;
if (seen.has(item.id))
return false;
seen.add(item.id);
return true;
});
@@ -487,6 +519,7 @@ export class ServerDirectoryService {
/** Load endpoints from localStorage, migrating protocol if needed. */
private loadEndpoints(): void {
const stored = localStorage.getItem(ENDPOINTS_STORAGE_KEY);
if (!stored) {
this.initialiseDefaultEndpoint();
return;
@@ -510,6 +543,7 @@ export class ServerDirectoryService {
if (endpoint.isDefault && /^https?:\/\/localhost:\d+$/.test(endpoint.url)) {
return { ...endpoint, url: endpoint.url.replace(/^https?/, expectedProtocol) };
}
return endpoint;
});
@@ -523,6 +557,7 @@ export class ServerDirectoryService {
/** Create and persist the built-in default endpoint. */
private initialiseDefaultEndpoint(): void {
const defaultEndpoint: ServerEndpoint = { ...DEFAULT_ENDPOINT, id: uuidv4() };
this._servers.set([defaultEndpoint]);
this.saveEndpoints();
}