From 71939cef5bc13ff67233c049bb8d32a2aa51d1d2 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:17:44 +0530 Subject: [PATCH 1/2] feat(socket): improve socket connection handling and reliability --- lib/core/providers/app_providers.dart | 6 ++- lib/core/services/socket_service.dart | 66 +++++++++++++++++++-------- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 13bddef..0d1f681 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -329,6 +329,7 @@ final apiServiceProvider = Provider((ref) { class SocketServiceManager extends _$SocketServiceManager { SocketService? _service; ProviderSubscription? _tokenSubscription; + int _connectToken = 0; @override FutureOr build() async { @@ -390,9 +391,11 @@ class SocketServiceManager extends _$SocketServiceManager { } void _scheduleConnect(SocketService service) { + final token = ++_connectToken; WidgetsBinding.instance.addPostFrameCallback((_) async { - await Future.delayed(const Duration(milliseconds: 150)); if (!ref.mounted) return; + if (_connectToken != token) return; + if (!identical(_service, service)) return; try { unawaited(service.connect()); } catch (_) {} @@ -400,6 +403,7 @@ class SocketServiceManager extends _$SocketServiceManager { } void _disposeService() { + _connectToken++; if (_service == null) return; try { _service!.dispose(); diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index 57af6ab..ffce55a 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -25,6 +25,7 @@ class SocketService with WidgetsBindingObserver { final bool allowWebsocketUpgrade; io.Socket? _socket; String? _authToken; + bool _isConnecting = false; bool _isAppForeground = true; Timer? _heartbeatTimer; @@ -69,6 +70,9 @@ class SocketService with WidgetsBindingObserver { Future connect({bool force = false}) async { if (_socket != null && _socket!.connected && !force) return; + if (_isConnecting && !force) return; + + _isConnecting = true; DebugLogger.log( 'Connecting to socket', @@ -80,7 +84,11 @@ class SocketService with WidgetsBindingObserver { _stopHeartbeat(); try { - _socket?.dispose(); + final existing = _socket; + if (existing != null) { + _unbindCoreSocketHandlers(existing); + existing.dispose(); + } } catch (_) {} String base = serverConfig.url.replaceFirst(RegExp(r'/+$'), ''); @@ -154,13 +162,17 @@ class SocketService with WidgetsBindingObserver { builder.setExtraHeaders(extraHeaders); } - _socket = createSocketWithOptionalBadCertOverride( - base, - builder, - serverConfig, - ); - - _bindCoreSocketHandlers(); + try { + _socket = createSocketWithOptionalBadCertOverride( + base, + builder, + serverConfig, + ); + _bindCoreSocketHandlers(); + } catch (_) { + _isConnecting = false; + rethrow; + } } /// Update the auth token used by the socket service. @@ -298,7 +310,11 @@ class SocketService with WidgetsBindingObserver { void dispose() { _stopHeartbeat(); try { - _socket?.dispose(); + final existing = _socket; + if (existing != null) { + _unbindCoreSocketHandlers(existing); + existing.dispose(); + } } catch (_) {} _socket = null; WidgetsBinding.instance.removeObserver(this); @@ -327,17 +343,7 @@ class SocketService with WidgetsBindingObserver { final socket = _socket; if (socket == null) return; - socket - ..off('events', _handleChatEvent) - ..off('chat-events', _handleChatEvent) - ..off('events:channel', _handleChannelEvent) - ..off('channel-events', _handleChannelEvent) - ..off('connect', _handleConnect) - ..off('connect_error', _handleConnectError) - ..off('reconnect_attempt', _handleReconnectAttempt) - ..off('reconnect', _handleReconnect) - ..off('reconnect_failed', _handleReconnectFailed) - ..off('disconnect', _handleDisconnect); + _unbindCoreSocketHandlers(socket); socket ..on('events', _handleChatEvent) @@ -352,7 +358,22 @@ class SocketService with WidgetsBindingObserver { ..on('disconnect', _handleDisconnect); } + void _unbindCoreSocketHandlers(io.Socket socket) { + socket + ..off('events', _handleChatEvent) + ..off('chat-events', _handleChatEvent) + ..off('events:channel', _handleChannelEvent) + ..off('channel-events', _handleChannelEvent) + ..off('connect', _handleConnect) + ..off('connect_error', _handleConnectError) + ..off('reconnect_attempt', _handleReconnectAttempt) + ..off('reconnect', _handleReconnect) + ..off('reconnect_failed', _handleReconnectFailed) + ..off('disconnect', _handleDisconnect); + } + void _handleConnect(dynamic _) { + _isConnecting = false; DebugLogger.log( 'Socket connected', scope: 'socket', @@ -370,6 +391,7 @@ class SocketService with WidgetsBindingObserver { } void _handleReconnectAttempt(dynamic attempt) { + _isConnecting = true; DebugLogger.log( 'Socket reconnection attempt', scope: 'socket', @@ -378,6 +400,7 @@ class SocketService with WidgetsBindingObserver { } void _handleReconnect(dynamic attempt) { + _isConnecting = false; DebugLogger.log( 'Socket reconnected', scope: 'socket', @@ -400,6 +423,7 @@ class SocketService with WidgetsBindingObserver { } void _handleConnectError(dynamic err) { + _isConnecting = false; DebugLogger.error( 'Socket connection error', scope: 'socket', @@ -409,6 +433,7 @@ class SocketService with WidgetsBindingObserver { } void _handleReconnectFailed(dynamic _) { + _isConnecting = false; DebugLogger.error( 'Socket reconnection failed after all attempts', scope: 'socket', @@ -417,6 +442,7 @@ class SocketService with WidgetsBindingObserver { } void _handleDisconnect(dynamic reason) { + _isConnecting = false; DebugLogger.warning( 'Socket disconnected', scope: 'socket', From 6a5bd37e7ef938fca6c19a9df7c3a3196968d6dd Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:07:50 +0530 Subject: [PATCH 2/2] feat(socket): improve WebSocket connection fallback mechanism --- lib/core/services/socket_service.dart | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index ffce55a..756fb36 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -28,6 +28,7 @@ class SocketService with WidgetsBindingObserver { bool _isConnecting = false; bool _isAppForeground = true; Timer? _heartbeatTimer; + bool _forcePollingFallback = false; /// Heartbeat interval matching OpenWebUI's 30-second interval. static const Duration _heartbeatInterval = Duration(seconds: 30); @@ -102,8 +103,10 @@ class SocketService with WidgetsBindingObserver { } catch (_) {} final path = '/ws/socket.io'; - final usePollingOnly = !websocketOnly && !allowWebsocketUpgrade; - final transports = websocketOnly + final usePollingFallback = _forcePollingFallback; + final effectiveWebsocketOnly = websocketOnly && !usePollingFallback; + final usePollingOnly = !effectiveWebsocketOnly && !allowWebsocketUpgrade; + final transports = effectiveWebsocketOnly ? const ['websocket'] : usePollingOnly ? const ['polling'] @@ -112,8 +115,8 @@ class SocketService with WidgetsBindingObserver { final builder = io.OptionBuilder() // Transport selection switches between WebSocket-only and polling fallback .setTransports(transports) - .setRememberUpgrade(!websocketOnly && allowWebsocketUpgrade) - .setUpgrade(!websocketOnly && allowWebsocketUpgrade) + .setRememberUpgrade(!effectiveWebsocketOnly && allowWebsocketUpgrade) + .setUpgrade(!effectiveWebsocketOnly && allowWebsocketUpgrade) // Tune reconnect/backoff and timeouts // Note: In socket_io_client, pass a very large number for "unlimited" attempts. // Using double.maxFinite.toInt() ensures unlimited reconnection attempts. @@ -430,6 +433,18 @@ class SocketService with WidgetsBindingObserver { error: err, data: {'serverUrl': serverConfig.url}, ); + + // If WebSocket-only handshake fails, retry once with polling+websocket + // transports to avoid endless spinners (issue #172). + if (websocketOnly && !_forcePollingFallback) { + _forcePollingFallback = true; + DebugLogger.warning( + 'WebSocket connect failed; retrying with polling fallback', + scope: 'socket', + data: {'reason': err?.toString()}, + ); + unawaited(connect(force: true)); + } } void _handleReconnectFailed(dynamic _) {