refactor progress
This commit is contained in:
77
fluxer_relay_directory/src/services/GeoSelectionService.tsx
Normal file
77
fluxer_relay_directory/src/services/GeoSelectionService.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {RelayInfo} from '@app/repositories/RelayRepository';
|
||||
|
||||
const EARTH_RADIUS_KM = 6371;
|
||||
|
||||
export interface GeoLocation {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface RelayWithDistance extends RelayInfo {
|
||||
distance_km: number;
|
||||
}
|
||||
|
||||
export interface IGeoSelectionService {
|
||||
calculateDistance(from: GeoLocation, to: GeoLocation): number;
|
||||
sortByProximity(relays: Array<RelayInfo>, clientLocation: GeoLocation): Array<RelayWithDistance>;
|
||||
selectNearestRelays(relays: Array<RelayInfo>, clientLocation: GeoLocation, limit: number): Array<RelayWithDistance>;
|
||||
}
|
||||
|
||||
export class GeoSelectionService implements IGeoSelectionService {
|
||||
calculateDistance(from: GeoLocation, to: GeoLocation): number {
|
||||
const lat1Rad = this.toRadians(from.latitude);
|
||||
const lat2Rad = this.toRadians(to.latitude);
|
||||
const deltaLat = this.toRadians(to.latitude - from.latitude);
|
||||
const deltaLon = this.toRadians(to.longitude - from.longitude);
|
||||
|
||||
const a =
|
||||
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
||||
Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return EARTH_RADIUS_KM * c;
|
||||
}
|
||||
|
||||
sortByProximity(relays: Array<RelayInfo>, clientLocation: GeoLocation): Array<RelayWithDistance> {
|
||||
const relaysWithDistance: Array<RelayWithDistance> = relays.map((relay) => ({
|
||||
...relay,
|
||||
distance_km: this.calculateDistance(clientLocation, {
|
||||
latitude: relay.latitude,
|
||||
longitude: relay.longitude,
|
||||
}),
|
||||
}));
|
||||
|
||||
relaysWithDistance.sort((a, b) => a.distance_km - b.distance_km);
|
||||
|
||||
return relaysWithDistance;
|
||||
}
|
||||
|
||||
selectNearestRelays(relays: Array<RelayInfo>, clientLocation: GeoLocation, limit: number): Array<RelayWithDistance> {
|
||||
const sorted = this.sortByProximity(relays, clientLocation);
|
||||
return sorted.slice(0, limit);
|
||||
}
|
||||
|
||||
private toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
}
|
||||
144
fluxer_relay_directory/src/services/HealthCheckService.tsx
Normal file
144
fluxer_relay_directory/src/services/HealthCheckService.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {IRelayRepository, RelayInfo} from '@app/repositories/RelayRepository';
|
||||
import type {Logger} from 'pino';
|
||||
|
||||
export interface HealthCheckConfig {
|
||||
interval_ms: number;
|
||||
timeout_ms: number;
|
||||
unhealthy_threshold: number;
|
||||
}
|
||||
|
||||
export interface IHealthCheckService {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
checkRelay(relay: RelayInfo): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class HealthCheckService implements IHealthCheckService {
|
||||
private readonly repository: IRelayRepository;
|
||||
private readonly config: HealthCheckConfig;
|
||||
private readonly logger: Logger;
|
||||
private intervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(repository: IRelayRepository, config: HealthCheckConfig, logger: Logger) {
|
||||
this.repository = repository;
|
||||
this.config = config;
|
||||
this.logger = logger.child({service: 'health-check'});
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.intervalHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info({interval_ms: this.config.interval_ms}, 'Starting relay health check service');
|
||||
|
||||
this.intervalHandle = setInterval(() => {
|
||||
this.checkAllRelays().catch((err) => {
|
||||
this.logger.error({error: err}, 'Health check cycle failed');
|
||||
});
|
||||
}, this.config.interval_ms);
|
||||
|
||||
this.checkAllRelays().catch((err) => {
|
||||
this.logger.error({error: err}, 'Initial health check failed');
|
||||
});
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.intervalHandle) {
|
||||
clearInterval(this.intervalHandle);
|
||||
this.intervalHandle = null;
|
||||
this.logger.info('Stopped relay health check service');
|
||||
}
|
||||
}
|
||||
|
||||
async checkRelay(relay: RelayInfo): Promise<boolean> {
|
||||
const healthUrl = new URL('/_health', relay.url).toString();
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout_ms);
|
||||
|
||||
const response = await fetch(healthUrl, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const isHealthy = response.ok;
|
||||
|
||||
if (isHealthy) {
|
||||
this.repository.updateRelayHealth(relay.id, true, 0);
|
||||
this.logger.debug({relay_id: relay.id, relay_name: relay.name}, 'Relay health check passed');
|
||||
} else {
|
||||
this.handleFailedCheck(relay);
|
||||
this.logger.warn(
|
||||
{relay_id: relay.id, relay_name: relay.name, status: response.status},
|
||||
'Relay health check failed with non-OK status',
|
||||
);
|
||||
}
|
||||
|
||||
return isHealthy;
|
||||
} catch (error) {
|
||||
this.handleFailedCheck(relay);
|
||||
this.logger.warn(
|
||||
{relay_id: relay.id, relay_name: relay.name, error: String(error)},
|
||||
'Relay health check failed with error',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleFailedCheck(relay: RelayInfo): void {
|
||||
const newFailedChecks = relay.failed_checks + 1;
|
||||
const isHealthy = newFailedChecks < this.config.unhealthy_threshold;
|
||||
|
||||
this.repository.updateRelayHealth(relay.id, isHealthy, newFailedChecks);
|
||||
|
||||
if (!isHealthy) {
|
||||
this.logger.warn(
|
||||
{
|
||||
relay_id: relay.id,
|
||||
relay_name: relay.name,
|
||||
failed_checks: newFailedChecks,
|
||||
threshold: this.config.unhealthy_threshold,
|
||||
},
|
||||
'Relay marked as unhealthy',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAllRelays(): Promise<void> {
|
||||
const relays = this.repository.getAllRelays();
|
||||
|
||||
this.logger.debug({relay_count: relays.length}, 'Running health checks');
|
||||
|
||||
const results = await Promise.allSettled(relays.map((relay: RelayInfo) => this.checkRelay(relay)));
|
||||
|
||||
const healthy = results.filter(
|
||||
(r): r is PromiseFulfilledResult<boolean> => r.status === 'fulfilled' && r.value,
|
||||
).length;
|
||||
const unhealthy = relays.length - healthy;
|
||||
|
||||
this.logger.info({total: relays.length, healthy, unhealthy}, 'Health check cycle completed');
|
||||
}
|
||||
}
|
||||
103
fluxer_relay_directory/src/services/RelayRegistryService.tsx
Normal file
103
fluxer_relay_directory/src/services/RelayRegistryService.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import type {IRelayRepository, RelayInfo} from '@app/repositories/RelayRepository';
|
||||
import type {GeoLocation, IGeoSelectionService, RelayWithDistance} from '@app/services/GeoSelectionService';
|
||||
|
||||
export interface RegisterRelayRequest {
|
||||
name: string;
|
||||
url: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
region: string;
|
||||
capacity: number;
|
||||
public_key: string;
|
||||
}
|
||||
|
||||
export interface IRelayRegistryService {
|
||||
registerRelay(request: RegisterRelayRequest): RelayInfo;
|
||||
getRelay(id: string): RelayInfo | null;
|
||||
getRelayStatus(id: string): RelayInfo | null;
|
||||
listRelays(clientLocation?: GeoLocation, limit?: number): Array<RelayInfo | RelayWithDistance>;
|
||||
updateRelayHeartbeat(id: string): void;
|
||||
removeRelay(id: string): void;
|
||||
}
|
||||
|
||||
export class RelayRegistryService implements IRelayRegistryService {
|
||||
private readonly repository: IRelayRepository;
|
||||
private readonly geoService: IGeoSelectionService;
|
||||
|
||||
constructor(repository: IRelayRepository, geoService: IGeoSelectionService) {
|
||||
this.repository = repository;
|
||||
this.geoService = geoService;
|
||||
}
|
||||
|
||||
registerRelay(request: RegisterRelayRequest): RelayInfo {
|
||||
const now = new Date().toISOString();
|
||||
const relay: RelayInfo = {
|
||||
id: randomUUID(),
|
||||
name: request.name,
|
||||
url: request.url,
|
||||
latitude: request.latitude,
|
||||
longitude: request.longitude,
|
||||
region: request.region,
|
||||
capacity: request.capacity,
|
||||
current_connections: 0,
|
||||
public_key: request.public_key,
|
||||
registered_at: now,
|
||||
last_seen_at: now,
|
||||
healthy: true,
|
||||
failed_checks: 0,
|
||||
};
|
||||
|
||||
this.repository.saveRelay(relay);
|
||||
return relay;
|
||||
}
|
||||
|
||||
getRelay(id: string): RelayInfo | null {
|
||||
return this.repository.getRelay(id);
|
||||
}
|
||||
|
||||
getRelayStatus(id: string): RelayInfo | null {
|
||||
return this.repository.getRelay(id);
|
||||
}
|
||||
|
||||
listRelays(clientLocation?: GeoLocation, limit?: number): Array<RelayInfo | RelayWithDistance> {
|
||||
const healthyRelays = this.repository.getHealthyRelays();
|
||||
|
||||
if (!clientLocation) {
|
||||
return limit ? healthyRelays.slice(0, limit) : healthyRelays;
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
return this.geoService.selectNearestRelays(healthyRelays, clientLocation, limit);
|
||||
}
|
||||
|
||||
return this.geoService.sortByProximity(healthyRelays, clientLocation);
|
||||
}
|
||||
|
||||
updateRelayHeartbeat(id: string): void {
|
||||
this.repository.updateRelayLastSeen(id);
|
||||
}
|
||||
|
||||
removeRelay(id: string): void {
|
||||
this.repository.removeRelay(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user