Merge pull request #213 from cogwheel0/socket-connection-reliability-improvement
feat(socket): Improve socket connection reliability and logging
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'package:socket_io_client/socket_io_client.dart' as io;
|
import 'package:socket_io_client/socket_io_client.dart' as io;
|
||||||
|
|
||||||
import '../models/server_config.dart';
|
import '../models/server_config.dart';
|
||||||
|
import '../utils/debug_logger.dart';
|
||||||
import 'socket_tls_override.dart';
|
import 'socket_tls_override.dart';
|
||||||
|
|
||||||
typedef SocketChatEventHandler =
|
typedef SocketChatEventHandler =
|
||||||
@@ -25,6 +26,10 @@ class SocketService with WidgetsBindingObserver {
|
|||||||
io.Socket? _socket;
|
io.Socket? _socket;
|
||||||
String? _authToken;
|
String? _authToken;
|
||||||
bool _isAppForeground = true;
|
bool _isAppForeground = true;
|
||||||
|
Timer? _heartbeatTimer;
|
||||||
|
|
||||||
|
/// Heartbeat interval matching OpenWebUI's 30-second interval.
|
||||||
|
static const Duration _heartbeatInterval = Duration(seconds: 30);
|
||||||
|
|
||||||
final Map<String, _ChatEventRegistration> _chatEventHandlers = {};
|
final Map<String, _ChatEventRegistration> _chatEventHandlers = {};
|
||||||
final Map<String, _ChannelEventRegistration> _channelEventHandlers = {};
|
final Map<String, _ChannelEventRegistration> _channelEventHandlers = {};
|
||||||
@@ -65,6 +70,15 @@ class SocketService with WidgetsBindingObserver {
|
|||||||
Future<void> connect({bool force = false}) async {
|
Future<void> connect({bool force = false}) async {
|
||||||
if (_socket != null && _socket!.connected && !force) return;
|
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 {
|
try {
|
||||||
_socket?.dispose();
|
_socket?.dispose();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -93,7 +107,9 @@ class SocketService with WidgetsBindingObserver {
|
|||||||
.setRememberUpgrade(!websocketOnly && allowWebsocketUpgrade)
|
.setRememberUpgrade(!websocketOnly && allowWebsocketUpgrade)
|
||||||
.setUpgrade(!websocketOnly && allowWebsocketUpgrade)
|
.setUpgrade(!websocketOnly && allowWebsocketUpgrade)
|
||||||
// Tune reconnect/backoff and timeouts
|
// 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)
|
.setReconnectionDelay(1000)
|
||||||
.setReconnectionDelayMax(5000)
|
.setReconnectionDelayMax(5000)
|
||||||
.setRandomizationFactor(0.5)
|
.setRandomizationFactor(0.5)
|
||||||
@@ -280,6 +296,7 @@ class SocketService with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_stopHeartbeat();
|
||||||
try {
|
try {
|
||||||
_socket?.dispose();
|
_socket?.dispose();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -336,35 +353,95 @@ class SocketService with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleConnect(dynamic _) {
|
void _handleConnect(dynamic _) {
|
||||||
|
DebugLogger.log(
|
||||||
|
'Socket connected',
|
||||||
|
scope: 'socket',
|
||||||
|
data: {'sessionId': _socket?.id},
|
||||||
|
);
|
||||||
|
|
||||||
if (_authToken != null && _authToken!.isNotEmpty) {
|
if (_authToken != null && _authToken!.isNotEmpty) {
|
||||||
_socket?.emit('user-join', {
|
_socket?.emit('user-join', {
|
||||||
'auth': {'token': _authToken},
|
'auth': {'token': _authToken},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start heartbeat timer to keep connection alive
|
||||||
|
_startHeartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleReconnectAttempt(dynamic attempt) {
|
void _handleReconnectAttempt(dynamic attempt) {
|
||||||
// Silent reconnection attempt
|
DebugLogger.log(
|
||||||
|
'Socket reconnection attempt',
|
||||||
|
scope: 'socket',
|
||||||
|
data: {'attempt': attempt},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleReconnect(dynamic attempt) {
|
void _handleReconnect(dynamic attempt) {
|
||||||
|
DebugLogger.log(
|
||||||
|
'Socket reconnected',
|
||||||
|
scope: 'socket',
|
||||||
|
data: {'attempt': attempt, 'sessionId': _socket?.id},
|
||||||
|
);
|
||||||
|
|
||||||
if (_authToken != null && _authToken!.isNotEmpty) {
|
if (_authToken != null && _authToken!.isNotEmpty) {
|
||||||
_socket?.emit('user-join', {
|
_socket?.emit('user-join', {
|
||||||
'auth': {'token': _authToken},
|
'auth': {'token': _authToken},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart heartbeat after reconnection
|
||||||
|
_startHeartbeat();
|
||||||
|
|
||||||
// Notify listeners that a reconnection occurred so they can refresh state
|
// Notify listeners that a reconnection occurred so they can refresh state
|
||||||
if (!_reconnectController.isClosed) {
|
if (!_reconnectController.isClosed) {
|
||||||
_reconnectController.add(null);
|
_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) {
|
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', <String, dynamic>{});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops the heartbeat timer.
|
||||||
|
void _stopHeartbeat() {
|
||||||
|
_heartbeatTimer?.cancel();
|
||||||
|
_heartbeatTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleChatEvent(dynamic data, [dynamic ack]) {
|
void _handleChatEvent(dynamic data, [dynamic ack]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user