refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View 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);
}
}

View 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');
}
}

View 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);
}
}