You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
8.4 KiB
303 lines
8.4 KiB
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../../data/nostr/nostr_service.dart';
|
|
import '../../data/nostr/models/nostr_relay.dart';
|
|
import '../../data/sync/sync_engine.dart';
|
|
|
|
/// Controller for managing Nostr relay UI state and operations.
|
|
///
|
|
/// This controller separates business logic from UI presentation,
|
|
/// making the UI module testable and modular.
|
|
class RelayManagementController extends ChangeNotifier {
|
|
/// Nostr service for relay operations.
|
|
final NostrService nostrService;
|
|
|
|
/// Sync engine for triggering manual syncs.
|
|
final SyncEngine? syncEngine;
|
|
|
|
/// List of relays being managed.
|
|
List<NostrRelay> _relays = [];
|
|
|
|
/// Whether a sync operation is in progress.
|
|
bool _isSyncing = false;
|
|
|
|
/// Error message if any operation fails.
|
|
String? _error;
|
|
|
|
/// Whether health check is in progress.
|
|
bool _isCheckingHealth = false;
|
|
|
|
/// Creates a [RelayManagementController] instance.
|
|
RelayManagementController({
|
|
required this.nostrService,
|
|
this.syncEngine,
|
|
}) {
|
|
_loadRelays();
|
|
}
|
|
|
|
/// List of relays.
|
|
List<NostrRelay> get relays => List.unmodifiable(_relays);
|
|
|
|
/// Whether sync is in progress.
|
|
bool get isSyncing => _isSyncing;
|
|
|
|
/// Current error message, if any.
|
|
String? get error => _error;
|
|
|
|
/// Whether health check is in progress.
|
|
bool get isCheckingHealth => _isCheckingHealth;
|
|
|
|
/// Loads relays from the Nostr service.
|
|
void _loadRelays() {
|
|
_relays = nostrService.getRelays();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Adds a relay to the service.
|
|
///
|
|
/// [relayUrl] - The WebSocket URL of the relay.
|
|
///
|
|
/// Returns true if the relay was added successfully, false if it already exists.
|
|
bool addRelay(String relayUrl) {
|
|
try {
|
|
_error = null;
|
|
|
|
// Validate URL format
|
|
if (!relayUrl.startsWith('wss://') && !relayUrl.startsWith('ws://')) {
|
|
_error = 'Invalid relay URL. Must start with wss:// or ws://';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
nostrService.addRelay(relayUrl);
|
|
_loadRelays();
|
|
return true;
|
|
} catch (e) {
|
|
_error = 'Failed to add relay: $e';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Removes a relay from the service.
|
|
///
|
|
/// [relayUrl] - The URL of the relay to remove.
|
|
void removeRelay(String relayUrl) {
|
|
try {
|
|
_error = null;
|
|
nostrService.removeRelay(relayUrl);
|
|
_loadRelays();
|
|
} catch (e) {
|
|
_error = 'Failed to remove relay: $e';
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Connects to a relay and updates status.
|
|
///
|
|
/// [relayUrl] - The URL of the relay to connect to.
|
|
///
|
|
/// Throws if connection fails (for use in health checks where we want to catch individual failures).
|
|
Future<void> connectRelay(String relayUrl) async {
|
|
_error = null;
|
|
await nostrService.connectRelay(relayUrl);
|
|
_loadRelays();
|
|
}
|
|
|
|
/// Disconnects from a relay.
|
|
///
|
|
/// [relayUrl] - The URL of the relay to disconnect from.
|
|
void disconnectRelay(String relayUrl) {
|
|
try {
|
|
_error = null;
|
|
nostrService.disconnectRelay(relayUrl);
|
|
_loadRelays();
|
|
} catch (e) {
|
|
_error = 'Failed to disconnect from relay: $e';
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Tests connectivity to a single relay.
|
|
///
|
|
/// [relayUrl] - The URL of the relay to test.
|
|
///
|
|
/// Returns true if connection successful, false otherwise.
|
|
Future<bool> testRelay(String relayUrl) async {
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final stream = await nostrService
|
|
.connectRelay(relayUrl)
|
|
.timeout(
|
|
const Duration(seconds: 3),
|
|
onTimeout: () {
|
|
throw Exception('Connection timeout');
|
|
},
|
|
);
|
|
_loadRelays();
|
|
// Cancel the stream subscription to clean up
|
|
stream.listen(null).cancel();
|
|
return true;
|
|
} catch (e) {
|
|
// Connection failed - disconnect to mark as unhealthy
|
|
try {
|
|
nostrService.disconnectRelay(relayUrl);
|
|
_loadRelays();
|
|
} catch (_) {
|
|
// Ignore disconnect errors
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Toggles a relay on/off (enables/disables it).
|
|
///
|
|
/// [relayUrl] - The URL of the relay to toggle.
|
|
void toggleRelay(String relayUrl) {
|
|
try {
|
|
_error = null;
|
|
final relay = _relays.firstWhere((r) => r.url == relayUrl);
|
|
nostrService.setRelayEnabled(relayUrl, !relay.isEnabled);
|
|
_loadRelays();
|
|
} catch (e) {
|
|
_error = 'Failed to toggle relay: $e';
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Toggles all relays on/off.
|
|
void toggleAllRelays() {
|
|
try {
|
|
_error = null;
|
|
final allEnabled = _relays.every((r) => r.isEnabled);
|
|
nostrService.setAllRelaysEnabled(!allEnabled);
|
|
_loadRelays();
|
|
} catch (e) {
|
|
_error = 'Failed to toggle all relays: $e';
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Checks health of all relays by attempting to connect.
|
|
///
|
|
/// Updates relay connection status.
|
|
///
|
|
/// Never throws - all connection failures are handled gracefully.
|
|
Future<void> checkRelayHealth() async {
|
|
_isCheckingHealth = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
// Process each relay health check, catching all errors
|
|
final futures = <Future<void>>[];
|
|
for (final relay in _relays) {
|
|
// Wrap each connection attempt to ensure all exceptions are caught
|
|
futures.add(
|
|
Future<void>(() async {
|
|
try {
|
|
// Attempt to connect - this may throw synchronously or asynchronously
|
|
// Wrap in another try-catch to catch synchronous exceptions from WebSocket.connect
|
|
try {
|
|
final stream = await nostrService
|
|
.connectRelay(relay.url)
|
|
.timeout(
|
|
const Duration(seconds: 2),
|
|
onTimeout: () {
|
|
throw Exception('Connection timeout');
|
|
},
|
|
);
|
|
// If we get here, connection succeeded
|
|
_loadRelays();
|
|
// Cancel the stream subscription to clean up
|
|
stream.listen(null).cancel();
|
|
} catch (e) {
|
|
// Connection failed - disconnect to mark as unhealthy
|
|
try {
|
|
nostrService.disconnectRelay(relay.url);
|
|
_loadRelays();
|
|
} catch (_) {
|
|
// Ignore disconnect errors
|
|
}
|
|
// Re-throw to be caught by outer catch
|
|
throw e;
|
|
}
|
|
} catch (e) {
|
|
// Catch all exceptions - connection failures are expected in tests
|
|
// Disconnect relay to mark as unhealthy
|
|
try {
|
|
nostrService.disconnectRelay(relay.url);
|
|
_loadRelays();
|
|
} catch (_) {
|
|
// Ignore disconnect errors
|
|
}
|
|
// Don't re-throw - this method should never throw
|
|
}
|
|
}).catchError((_) {
|
|
// Final safety net - ensure no exceptions escape
|
|
try {
|
|
nostrService.disconnectRelay(relay.url);
|
|
_loadRelays();
|
|
} catch (_) {
|
|
// Ignore all errors
|
|
}
|
|
return Future<void>.value();
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Wait for all health checks to complete (or fail gracefully)
|
|
// Use eagerError: false so one failure doesn't stop others
|
|
try {
|
|
await Future.wait(futures, eagerError: false);
|
|
} catch (_) {
|
|
// Swallow any errors - all individual failures already handled above
|
|
}
|
|
|
|
_isCheckingHealth = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Triggers a manual sync using the sync engine.
|
|
///
|
|
/// Returns true if sync was triggered successfully.
|
|
Future<bool> triggerManualSync() async {
|
|
if (syncEngine == null) {
|
|
_error = 'Sync engine not configured';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
_isSyncing = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
// Trigger sync to all configured relays via Nostr
|
|
// This is a simplified sync - in a real app, you'd sync specific items
|
|
await syncEngine!.syncAll();
|
|
return true;
|
|
} catch (e) {
|
|
_error = 'Sync failed: $e';
|
|
return false;
|
|
} finally {
|
|
_isSyncing = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Clears the current error message.
|
|
void clearError() {
|
|
_error = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Don't dispose nostrService or syncEngine - they're managed elsewhere
|
|
super.dispose();
|
|
}
|
|
}
|
|
|