From 2db3339071956a87157ce9902beb214c3d98225e Mon Sep 17 00:00:00 2001 From: gitea Date: Sat, 15 Nov 2025 00:10:47 +0100 Subject: [PATCH] reconnect to relay if down --- lib/data/nostr/models/nostr_relay.dart | 9 +- lib/data/nostr/nostr_service.dart | 111 +++++++++++++++++- .../relay_management_controller.dart | 1 + 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/lib/data/nostr/models/nostr_relay.dart b/lib/data/nostr/models/nostr_relay.dart index 65f7048..59ce98b 100644 --- a/lib/data/nostr/models/nostr_relay.dart +++ b/lib/data/nostr/models/nostr_relay.dart @@ -9,11 +9,18 @@ class NostrRelay { /// Whether the relay is enabled (should be used). bool isEnabled; + /// Number of consecutive connection failures. + int retryCount; + + /// Maximum number of retry attempts before disabling. + static const int maxRetryAttempts = 3; + /// Creates a [NostrRelay] instance. NostrRelay({ required this.url, this.isConnected = false, this.isEnabled = true, + this.retryCount = 0, }); /// Creates a [NostrRelay] from a URL string. @@ -23,7 +30,7 @@ class NostrRelay { @override String toString() { - return 'NostrRelay(url: $url, connected: $isConnected, enabled: $isEnabled)'; + return 'NostrRelay(url: $url, connected: $isConnected, enabled: $isEnabled, retryCount: $retryCount)'; } @override diff --git a/lib/data/nostr/nostr_service.dart b/lib/data/nostr/nostr_service.dart index 7fa2d70..9fd00f4 100644 --- a/lib/data/nostr/nostr_service.dart +++ b/lib/data/nostr/nostr_service.dart @@ -29,6 +29,9 @@ class NostrService { final Map>> _messageControllers = {}; + /// Retry timers for relays that are attempting to reconnect. + final Map _retryTimers = {}; + /// Creates a [NostrService] instance. NostrService(); @@ -67,6 +70,9 @@ class NostrService { orElse: () => throw NostrException('Relay not found: $relayUrl'), ); relay.isEnabled = enabled; + if (enabled) { + relay.retryCount = 0; // Reset retry count when enabling + } // If disabling, also disconnect if (!enabled && relay.isConnected) { @@ -156,6 +162,7 @@ class NostrService { // No errors occurred, connection is established connectionConfirmed = true; relay.isConnected = true; + relay.retryCount = 0; // Reset retry count on successful connection Logger.info('Connection confirmed for relay: $relayUrl (no errors after 500ms)'); } }); @@ -167,6 +174,7 @@ class NostrService { if (!connectionConfirmed) { connectionConfirmed = true; relay.isConnected = true; + relay.retryCount = 0; // Reset retry count on successful connection Logger.info('Connection confirmed for relay: $relayUrl (first message received)'); connectionTimer?.cancel(); } @@ -207,9 +215,7 @@ class NostrService { connectionTimer?.cancel(); Logger.error('WebSocket error for relay: $relayUrl', error); relay.isConnected = false; - // Automatically disable relay when connection error occurs - relay.isEnabled = false; - Logger.warning('Relay $relayUrl disabled due to connection error'); + _handleConnectionFailure(relayUrl, relay, controller, 'connection error'); controller.addError(NostrException('Relay error: $error')); }, onDone: () { @@ -217,9 +223,7 @@ class NostrService { connectionTimer?.cancel(); Logger.warning('WebSocket stream closed for relay: $relayUrl'); relay.isConnected = false; - // Automatically disable relay when connection closes - relay.isEnabled = false; - Logger.warning('Relay $relayUrl disabled due to stream closure'); + _handleConnectionFailure(relayUrl, relay, controller, 'stream closure'); controller.close(); }, ); @@ -230,6 +234,101 @@ class NostrService { } } + /// Handles connection failure with retry logic. + /// + /// [relayUrl] - The URL of the relay that failed. + /// [relay] - The relay object. + /// [controller] - The stream controller for this relay. + /// [reason] - Reason for the failure (for logging). + void _handleConnectionFailure( + String relayUrl, + NostrRelay relay, + StreamController> controller, + String reason, + ) { + // Cancel any existing retry timer + _retryTimers[relayUrl]?.cancel(); + _retryTimers.remove(relayUrl); + + // Only retry if relay is enabled + if (!relay.isEnabled) { + Logger.info('Relay $relayUrl is disabled, skipping retry'); + return; + } + + relay.retryCount++; + Logger.warning('Relay $relayUrl failed ($reason), retry attempt ${relay.retryCount}/${NostrRelay.maxRetryAttempts}'); + + if (relay.retryCount >= NostrRelay.maxRetryAttempts) { + // Max retries reached - disable the relay + relay.isEnabled = false; + relay.retryCount = 0; // Reset for future attempts + Logger.warning('Relay $relayUrl disabled after ${NostrRelay.maxRetryAttempts} failed attempts'); + } else { + // Schedule retry after delay (5 seconds) + _scheduleRetry(relayUrl, relay, controller, reason); + } + } + + /// Schedules a retry attempt for a failed relay connection. + /// + /// [relayUrl] - The URL of the relay to retry. + /// [relay] - The relay object. + /// [controller] - The stream controller for this relay. + /// [reason] - Reason for the retry (for logging). + void _scheduleRetry( + String relayUrl, + NostrRelay relay, + StreamController> controller, + String reason, + ) { + Logger.info('Scheduling retry for relay $relayUrl in 5 seconds (attempt ${relay.retryCount}/${NostrRelay.maxRetryAttempts})'); + _retryTimers[relayUrl] = Timer(const Duration(seconds: 5), () { + _retryTimers.remove(relayUrl); + if (relay.isEnabled && !relay.isConnected) { + Logger.info('Retrying connection to relay: $relayUrl'); + try { + // Clean up old connection + disconnectRelay(relayUrl); + // Attempt to reconnect + connectRelay(relayUrl).then((stream) { + // Reset retry count on successful connection + relay.retryCount = 0; + Logger.info('Relay $relayUrl reconnected successfully'); + // Cancel the stream subscription as we're just testing the connection + stream.listen(null).cancel(); + }).catchError((e) { + Logger.warning('Retry failed for relay $relayUrl: $e'); + // Handle retry failure - check if we should retry again or disable + if (relay.isEnabled) { + if (relay.retryCount >= NostrRelay.maxRetryAttempts) { + relay.isEnabled = false; + relay.retryCount = 0; + Logger.warning('Relay $relayUrl disabled after ${NostrRelay.maxRetryAttempts} failed attempts'); + } else { + // Schedule another retry (retryCount already incremented in _handleConnectionFailure) + _scheduleRetry(relayUrl, relay, controller, 'retry failure'); + } + } + }); + } catch (e) { + Logger.error('Error during retry for relay $relayUrl', e); + // Handle synchronous exception + if (relay.isEnabled) { + if (relay.retryCount >= NostrRelay.maxRetryAttempts) { + relay.isEnabled = false; + relay.retryCount = 0; + Logger.warning('Relay $relayUrl disabled after ${NostrRelay.maxRetryAttempts} failed attempts'); + } else { + // Schedule another retry (retryCount already incremented in _handleConnectionFailure) + _scheduleRetry(relayUrl, relay, controller, 'synchronous retry failure'); + } + } + } + } + }); + } + /// Disconnects from a relay. /// /// [relayUrl] - The URL of the relay to disconnect from. diff --git a/lib/ui/relay_management/relay_management_controller.dart b/lib/ui/relay_management/relay_management_controller.dart index 1b9e89a..f7ea719 100644 --- a/lib/ui/relay_management/relay_management_controller.dart +++ b/lib/ui/relay_management/relay_management_controller.dart @@ -56,6 +56,7 @@ class RelayManagementController extends ChangeNotifier { url: relay.url, isConnected: relay.isConnected, isEnabled: relay.isEnabled, + retryCount: relay.retryCount, )).toList(); notifyListeners(); }