👔 No longer rapid websocket reconnect (stops after 5 times in a minute)

This commit is contained in:
2025-12-24 22:15:33 +08:00
parent 2e3e988125
commit d655840e85
2 changed files with 63 additions and 12 deletions

View File

@@ -52,6 +52,11 @@ class WebSocketService {
DateTime? _heartbeatAt; DateTime? _heartbeatAt;
Duration? heartbeatDelay; Duration? heartbeatDelay;
// Reconnection tracking
int _reconnectCount = 0;
DateTime? _reconnectWindowStart;
static const int _maxReconnectsPerMinute = 5;
Stream<WebSocketPacket> get dataStream => _streamController.stream; Stream<WebSocketPacket> get dataStream => _streamController.stream;
Stream<WebSocketState> get statusStream => _statusStreamController.stream; Stream<WebSocketState> get statusStream => _statusStreamController.stream;
@@ -79,8 +84,9 @@ class WebSocketService {
_scheduleHeartbeat(); _scheduleHeartbeat();
_channel!.stream.listen( _channel!.stream.listen(
(data) { (data) {
final dataStr = final dataStr = data is Uint8List
data is Uint8List ? utf8.decode(data) : data.toString(); ? utf8.decode(data)
: data.toString();
final packet = WebSocketPacket.fromJson(jsonDecode(dataStr)); final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
if (packet.type == 'error.dupe') { if (packet.type == 'error.dupe') {
_statusStreamController.sink.add(WebSocketState.duplicateDevice()); _statusStreamController.sink.add(WebSocketState.duplicateDevice());
@@ -123,6 +129,34 @@ class WebSocketService {
} }
void _scheduleReconnect() { 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?.cancel();
_reconnectTimer = Timer(const Duration(milliseconds: 500), () { _reconnectTimer = Timer(const Duration(milliseconds: 500), () {
_statusStreamController.sink.add(WebSocketState.connecting()); _statusStreamController.sink.add(WebSocketState.connecting());
@@ -204,4 +238,9 @@ class WebSocketStateNotifier extends Notifier<WebSocketState> {
_reconnectTimer?.cancel(); _reconnectTimer?.cancel();
state = const WebSocketState.disconnected(); state = const WebSocketState.disconnected();
} }
void manualReconnect() {
final service = ref.read(websocketProvider);
service.manualReconnect();
}
} }

View File

@@ -530,6 +530,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
Color indicatorColor; Color indicatorColor;
String indicatorText; String indicatorText;
bool isInteractive = false;
if (websocketState == WebSocketState.connected()) { if (websocketState == WebSocketState.connected()) {
indicatorColor = Colors.green; indicatorColor = Colors.green;
@@ -537,12 +538,16 @@ class _WebSocketIndicator extends HookConsumerWidget {
} else if (websocketState == WebSocketState.connecting()) { } else if (websocketState == WebSocketState.connecting()) {
indicatorColor = Colors.teal; indicatorColor = Colors.teal;
indicatorText = 'connectionReconnecting'; indicatorText = 'connectionReconnecting';
} else if (websocketState == WebSocketState.serverDown()) {
indicatorColor = Colors.red;
indicatorText = 'connectionServerDown';
isInteractive = true;
} else { } else {
indicatorColor = Colors.red; indicatorColor = Colors.red;
indicatorText = 'connectionDisconnected'; indicatorText = 'connectionDisconnected';
} }
return AnimatedPositioned( final widget = AnimatedPositioned(
duration: Duration(milliseconds: 1850), duration: Duration(milliseconds: 1850),
top: top:
user.value == null || user.value == null ||
@@ -554,15 +559,20 @@ class _WebSocketIndicator extends HookConsumerWidget {
left: 0, left: 0,
right: 0, right: 0,
height: indicatorHeight, height: indicatorHeight,
child: IgnorePointer( child: Material(
child: Material( elevation:
elevation: user.value == null || websocketState == WebSocketState.connected()
user.value == null || websocketState == WebSocketState.connected() ? 0
? 0 : 4,
: 4, child: AnimatedContainer(
child: AnimatedContainer( duration: Duration(milliseconds: 300),
duration: Duration(milliseconds: 300), color: indicatorColor,
color: indicatorColor, child: InkWell(
onTap: isInteractive
? () {
ref.read(websocketStateProvider.notifier).manualReconnect();
}
: null,
child: Center( child: Center(
child: Text( child: Text(
indicatorText, indicatorText,
@@ -573,5 +583,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
), ),
), ),
); );
return isInteractive ? widget : IgnorePointer(child: widget);
} }
} }