/* * 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 . */ 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; 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 { 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); } }