You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudsolutions-atoms/lib/modules/cx_module/chat/services/signalr_service.dart

278 lines
8.9 KiB
Dart

1 month ago
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:signalr_netcore/hub_connection.dart';
import 'package:signalr_netcore/signalr_client.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
/// Singleton SignalR service that manages the single HubConnection
/// Both ChatProvider and CallManager use this service
class SignalRService {
static final SignalRService _instance = SignalRService._internal();
factory SignalRService() => _instance;
SignalRService._internal();
// Single HubConnection instance
HubConnection? _hubConnection;
// Connection state
bool _isInitializing = false;
// Authentication data
String? _userId;
String? _authToken;
String? _currentConversationId;
// Event handler registrations
final Map<String, List<Function(List<Object?>?)>> _eventHandlers = {};
/// Get the current HubConnection
HubConnection? get hubConnection => _hubConnection;
/// Check if connected
bool get isConnected => _hubConnection?.state == HubConnectionState.Connected;
/// Get current connection state
HubConnectionState? get connectionState => _hubConnection?.state;
/// Initialize SignalR connection with authentication
Future<bool> initialize({
required String userId,
required String authToken,
String? conversationId,
}) async {
try {
log('═══════════════════════════════════════════', name: 'SignalRService');
log('🔌 [SIGNALR] Initializing SignalR connection...', name: 'SignalRService');
log(' User ID: $userId', name: 'SignalRService');
log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService');
// Prevent multiple simultaneous initializations
if (_isInitializing) {
log('⏳ [SIGNALR] Already initializing, waiting...', name: 'SignalRService');
// Wait for current initialization to complete
final timeout = DateTime.now().add(const Duration(seconds: 5));
while (_isInitializing && DateTime.now().isBefore(timeout)) {
await Future.delayed(const Duration(milliseconds: 100));
}
return isConnected;
}
// If already connected with same credentials, reuse connection
if (isConnected && _userId == userId && _authToken == authToken) {
log('✅ [SIGNALR] Already connected with same credentials, reusing connection', name: 'SignalRService');
// Join new conversation if provided and different
if (conversationId != null && conversationId != _currentConversationId) {
await _joinConversation(conversationId);
}
return true;
}
_isInitializing = true;
// Store credentials
_userId = userId;
_authToken = authToken;
_currentConversationId = conversationId;
// Dispose existing connection if any
await _disposeConnection();
// Create new connection
final httpOp = HttpConnectionOptions(
skipNegotiation: false,
logMessageContent: kDebugMode,
);
_hubConnection = HubConnectionBuilder()
.withUrl(
"${URLs.chatHubUrlChat}?UserId=$userId&source=Desktop&access_token=$authToken",
options: httpOp,
)
.withAutomaticReconnect(retryDelays: <int>[2000, 5000, 10000, 20000])
.build();
// Setup reconnection handlers
_setupReconnectionHandlers();
// Start connection
await _hubConnection!.start();
log('✅ [SIGNALR] Connection established', name: 'SignalRService');
log(' Connection ID: ${_hubConnection!.connectionId}', name: 'SignalRService');
// Join conversation if provided
if (conversationId != null) {
await _joinConversation(conversationId);
}
// Re-register all event handlers
_reregisterAllHandlers();
_isInitializing = false;
log('═══════════════════════════════════════════', name: 'SignalRService');
return true;
} catch (e, stackTrace) {
log('❌ [SIGNALR] Error initializing connection: $e',
name: 'SignalRService', error: e, stackTrace: stackTrace);
_isInitializing = false;
return false;
}
}
/// Setup reconnection handlers
void _setupReconnectionHandlers() {
if (_hubConnection == null) return;
_hubConnection!.onclose(({Exception? error}) {
log('🔴 [SIGNALR] Connection closed: $error', name: 'SignalRService');
});
_hubConnection!.onreconnecting(({Exception? error}) {
log('🟡 [SIGNALR] Reconnecting: $error', name: 'SignalRService');
});
_hubConnection!.onreconnected(({String? connectionId}) async {
log('🟢 [SIGNALR] Reconnected: $connectionId', name: 'SignalRService');
// Rejoin conversation if we had one
if (_currentConversationId != null) {
await _joinConversation(_currentConversationId!);
}
// Re-register all event handlers
_reregisterAllHandlers();
});
}
/// Join a conversation
Future<void> _joinConversation(String conversationId) async {
try {
if (_hubConnection?.state != HubConnectionState.Connected) {
log('⚠️ [SIGNALR] Cannot join conversation - not connected', name: 'SignalRService');
return;
}
await _hubConnection!.invoke("JoinConversation", args: [conversationId]);
_currentConversationId = conversationId;
log('✅ [SIGNALR] Joined conversation: $conversationId', name: 'SignalRService');
} catch (e) {
log('❌ [SIGNALR] Error joining conversation: $e', name: 'SignalRService');
}
}
/// Register an event handler
void on(String eventName, Function(List<Object?>?) handler) {
log('🔧 [SIGNALR] Registering handler for: $eventName', name: 'SignalRService');
// Store handler for re-registration after reconnect
if (!_eventHandlers.containsKey(eventName)) {
_eventHandlers[eventName] = [];
}
_eventHandlers[eventName]!.add(handler);
// Register with SignalR if connected
if (_hubConnection != null) {
_hubConnection!.on(eventName, handler);
}
}
/// Unregister an event handler
void off(String eventName, [Function(List<Object?>?)? handler]) {
log('🔧 [SIGNALR] Unregistering handler for: $eventName', name: 'SignalRService');
if (handler != null) {
_eventHandlers[eventName]?.remove(handler);
if (_eventHandlers[eventName]?.isEmpty ?? false) {
_eventHandlers.remove(eventName);
}
} else {
_eventHandlers.remove(eventName);
}
// Unregister from SignalR if connected
if (_hubConnection != null) {
_hubConnection!.off(eventName, method: handler);
}
}
/// Re-register all event handlers (after reconnection)
void _reregisterAllHandlers() {
if (_hubConnection == null) return;
log('🔄 [SIGNALR] Re-registering ${_eventHandlers.length} event handlers...', name: 'SignalRService');
for (final entry in _eventHandlers.entries) {
final eventName = entry.key;
final handlers = entry.value;
for (final handler in handlers) {
_hubConnection!.on(eventName, handler);
}
}
log('✅ [SIGNALR] All event handlers re-registered', name: 'SignalRService');
}
/// Invoke a SignalR method
Future<Object?> invoke(String methodName, {List<Object>? args}) async {
if (_hubConnection?.state != HubConnectionState.Connected) {
throw Exception('SignalR not connected. Current state: ${_hubConnection?.state}');
}
log('📤 [SIGNALR] Invoking: $methodName', name: 'SignalRService');
return await _hubConnection!.invoke(methodName, args: args);
}
/// Ensure connection is ready (connect if needed)
Future<bool> ensureConnected() async {
if (isConnected) {
return true;
}
if (_userId != null && _authToken != null) {
return await initialize(
userId: _userId!,
authToken: _authToken!,
conversationId: _currentConversationId,
);
}
log('❌ [SIGNALR] Cannot reconnect - no credentials stored', name: 'SignalRService');
return false;
}
/// Dispose connection
Future<void> _disposeConnection() async {
try {
if (_hubConnection != null) {
await _hubConnection!.stop();
_hubConnection = null;
log('✅ [SIGNALR] Connection disposed', name: 'SignalRService');
}
} catch (e) {
log('⚠️ [SIGNALR] Error disposing connection: $e', name: 'SignalRService');
}
}
/// Reset service (for logout)
Future<void> reset() async {
log('🔄 [SIGNALR] Resetting service...', name: 'SignalRService');
await _disposeConnection();
_eventHandlers.clear();
_userId = null;
_authToken = null;
_currentConversationId = null;
_isInitializing = false;
log('✅ [SIGNALR] Service reset complete', name: 'SignalRService');
}
}