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?)>> _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 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: [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 _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?) 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?)? 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 invoke(String methodName, {List? 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 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 _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 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'); } }