From 311da69e048abb1c36e8f9f4035a9b7b9694158c Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:13:24 +0530 Subject: [PATCH] feat(socket): Improve socket connection reliability and logging --- lib/core/services/socket_service.dart | 87 +++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index 8bfa03b..57af6ab 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:socket_io_client/socket_io_client.dart' as io; import '../models/server_config.dart'; +import '../utils/debug_logger.dart'; import 'socket_tls_override.dart'; typedef SocketChatEventHandler = @@ -25,6 +26,10 @@ class SocketService with WidgetsBindingObserver { io.Socket? _socket; String? _authToken; bool _isAppForeground = true; + Timer? _heartbeatTimer; + + /// Heartbeat interval matching OpenWebUI's 30-second interval. + static const Duration _heartbeatInterval = Duration(seconds: 30); final Map _chatEventHandlers = {}; final Map _channelEventHandlers = {}; @@ -65,6 +70,15 @@ class SocketService with WidgetsBindingObserver { Future connect({bool force = false}) async { if (_socket != null && _socket!.connected && !force) return; + DebugLogger.log( + 'Connecting to socket', + scope: 'socket', + data: {'force': force, 'serverUrl': serverConfig.url}, + ); + + // Stop any existing heartbeat before disposing old socket + _stopHeartbeat(); + try { _socket?.dispose(); } catch (_) {} @@ -93,7 +107,9 @@ class SocketService with WidgetsBindingObserver { .setRememberUpgrade(!websocketOnly && allowWebsocketUpgrade) .setUpgrade(!websocketOnly && allowWebsocketUpgrade) // Tune reconnect/backoff and timeouts - .setReconnectionAttempts(0) // 0/Infinity semantics: unlimited attempts + // Note: In socket_io_client, pass a very large number for "unlimited" attempts. + // Using double.maxFinite.toInt() ensures unlimited reconnection attempts. + .setReconnectionAttempts(double.maxFinite.toInt()) .setReconnectionDelay(1000) .setReconnectionDelayMax(5000) .setRandomizationFactor(0.5) @@ -280,6 +296,7 @@ class SocketService with WidgetsBindingObserver { } void dispose() { + _stopHeartbeat(); try { _socket?.dispose(); } catch (_) {} @@ -336,35 +353,95 @@ class SocketService with WidgetsBindingObserver { } void _handleConnect(dynamic _) { + DebugLogger.log( + 'Socket connected', + scope: 'socket', + data: {'sessionId': _socket?.id}, + ); + if (_authToken != null && _authToken!.isNotEmpty) { _socket?.emit('user-join', { 'auth': {'token': _authToken}, }); } + + // Start heartbeat timer to keep connection alive + _startHeartbeat(); } void _handleReconnectAttempt(dynamic attempt) { - // Silent reconnection attempt + DebugLogger.log( + 'Socket reconnection attempt', + scope: 'socket', + data: {'attempt': attempt}, + ); } void _handleReconnect(dynamic attempt) { + DebugLogger.log( + 'Socket reconnected', + scope: 'socket', + data: {'attempt': attempt, 'sessionId': _socket?.id}, + ); + if (_authToken != null && _authToken!.isNotEmpty) { _socket?.emit('user-join', { 'auth': {'token': _authToken}, }); } + + // Restart heartbeat after reconnection + _startHeartbeat(); + // Notify listeners that a reconnection occurred so they can refresh state if (!_reconnectController.isClosed) { _reconnectController.add(null); } } - void _handleConnectError(dynamic err) {} + void _handleConnectError(dynamic err) { + DebugLogger.error( + 'Socket connection error', + scope: 'socket', + error: err, + data: {'serverUrl': serverConfig.url}, + ); + } - void _handleReconnectFailed(dynamic _) {} + void _handleReconnectFailed(dynamic _) { + DebugLogger.error( + 'Socket reconnection failed after all attempts', + scope: 'socket', + data: {'serverUrl': serverConfig.url}, + ); + } void _handleDisconnect(dynamic reason) { - // Silent disconnect + DebugLogger.warning( + 'Socket disconnected', + scope: 'socket', + data: {'reason': reason?.toString()}, + ); + + // Stop heartbeat when disconnected + _stopHeartbeat(); + } + + /// Starts the heartbeat timer to keep the connection alive. + /// Sends a heartbeat event every 30 seconds matching OpenWebUI's behavior. + void _startHeartbeat() { + _stopHeartbeat(); + _heartbeatTimer = Timer.periodic(_heartbeatInterval, (_) { + if (_socket?.connected == true) { + _socket?.emit('heartbeat', {}); + } + }); + } + + /// Stops the heartbeat timer. + void _stopHeartbeat() { + _heartbeatTimer?.cancel(); + _heartbeatTimer = null; } void _handleChatEvent(dynamic data, [dynamic ack]) {