From d655840e856fcd6b7018c746bb3eb721fa417746 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 24 Dec 2025 22:15:33 +0800 Subject: [PATCH] :necktie: No longer rapid websocket reconnect (stops after 5 times in a minute) --- lib/pods/websocket.dart | 43 +++++++++++++++++++++++++++++++++-- lib/widgets/app_scaffold.dart | 32 ++++++++++++++++++-------- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/lib/pods/websocket.dart b/lib/pods/websocket.dart index 285709dd..6641cc0c 100644 --- a/lib/pods/websocket.dart +++ b/lib/pods/websocket.dart @@ -52,6 +52,11 @@ class WebSocketService { DateTime? _heartbeatAt; Duration? heartbeatDelay; + // Reconnection tracking + int _reconnectCount = 0; + DateTime? _reconnectWindowStart; + static const int _maxReconnectsPerMinute = 5; + Stream get dataStream => _streamController.stream; Stream get statusStream => _statusStreamController.stream; @@ -79,8 +84,9 @@ class WebSocketService { _scheduleHeartbeat(); _channel!.stream.listen( (data) { - final dataStr = - data is Uint8List ? utf8.decode(data) : data.toString(); + final dataStr = data is Uint8List + ? utf8.decode(data) + : data.toString(); final packet = WebSocketPacket.fromJson(jsonDecode(dataStr)); if (packet.type == 'error.dupe') { _statusStreamController.sink.add(WebSocketState.duplicateDevice()); @@ -123,6 +129,34 @@ class WebSocketService { } void _scheduleReconnect() { + // Check if we've exceeded the reconnect limit + final now = DateTime.now(); + if (_reconnectWindowStart == null || + now.difference(_reconnectWindowStart!).inMinutes >= 1) { + // Reset window if it's been more than 1 minute since the window started + _reconnectWindowStart = now; + _reconnectCount = 0; + } + + _reconnectCount++; + + if (_reconnectCount > _maxReconnectsPerMinute) { + talker.error( + '[WebSocket] Reconnect limit exceeded: $_maxReconnectsPerMinute reconnections in the last minute. Stopping auto-reconnect.', + ); + _statusStreamController.sink.add(WebSocketState.serverDown()); + return; + } + + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(const Duration(milliseconds: 500), () { + _statusStreamController.sink.add(WebSocketState.connecting()); + connect(_ref); + }); + } + + void manualReconnect() { + talker.info('[WebSocket] Manual reconnect triggered by user'); _reconnectTimer?.cancel(); _reconnectTimer = Timer(const Duration(milliseconds: 500), () { _statusStreamController.sink.add(WebSocketState.connecting()); @@ -204,4 +238,9 @@ class WebSocketStateNotifier extends Notifier { _reconnectTimer?.cancel(); state = const WebSocketState.disconnected(); } + + void manualReconnect() { + final service = ref.read(websocketProvider); + service.manualReconnect(); + } } diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index db3bcd1a..ff86136b 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -530,6 +530,7 @@ class _WebSocketIndicator extends HookConsumerWidget { Color indicatorColor; String indicatorText; + bool isInteractive = false; if (websocketState == WebSocketState.connected()) { indicatorColor = Colors.green; @@ -537,12 +538,16 @@ class _WebSocketIndicator extends HookConsumerWidget { } else if (websocketState == WebSocketState.connecting()) { indicatorColor = Colors.teal; indicatorText = 'connectionReconnecting'; + } else if (websocketState == WebSocketState.serverDown()) { + indicatorColor = Colors.red; + indicatorText = 'connectionServerDown'; + isInteractive = true; } else { indicatorColor = Colors.red; indicatorText = 'connectionDisconnected'; } - return AnimatedPositioned( + final widget = AnimatedPositioned( duration: Duration(milliseconds: 1850), top: user.value == null || @@ -554,15 +559,20 @@ class _WebSocketIndicator extends HookConsumerWidget { left: 0, right: 0, height: indicatorHeight, - child: IgnorePointer( - child: Material( - elevation: - user.value == null || websocketState == WebSocketState.connected() - ? 0 - : 4, - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - color: indicatorColor, + child: Material( + elevation: + user.value == null || websocketState == WebSocketState.connected() + ? 0 + : 4, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + color: indicatorColor, + child: InkWell( + onTap: isInteractive + ? () { + ref.read(websocketStateProvider.notifier).manualReconnect(); + } + : null, child: Center( child: Text( indicatorText, @@ -573,5 +583,7 @@ class _WebSocketIndicator extends HookConsumerWidget { ), ), ); + + return isInteractive ? widget : IgnorePointer(child: widget); } }