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'; class SignalRService { // Singleton instance - DO NOT instantiate directly, use DI (Provider) static final SignalRService _instance = SignalRService._internal(); factory SignalRService() => _instance; SignalRService._internal() { log('🏗️ [SIGNALR] SignalRService singleton created (hashCode: ${hashCode})', name: 'SignalRService'); } // Single HubConnection instance - ONLY created and managed by this service HubConnection? _hubConnection; // Connection state management bool _isInitializing = false; Completer? _initializationCompleter; // Authentication data String? _userId; String? _authToken; String? _currentConversationId; // Event handler registrations for re-registration after reconnect final Map?)>> _eventHandlers = {}; // Connection state stream for reactive updates final StreamController _connectionStateController = StreamController.broadcast(); /// Stream of connection state changes for consumers to listen Stream get connectionStateStream => _connectionStateController.stream; /// Get the current HubConnection (read-only access) HubConnection? get hubConnection => _hubConnection; /// Check if connected bool get isConnected => _hubConnection?.state == HubConnectionState.Connected; /// Get current connection state HubConnectionState? get connectionState => _hubConnection?.state; /// Get connection ID (for debugging and verification) String? get connectionId => _hubConnection?.connectionId; /// Initialize SignalR connection with authentication /// Thread-safe: Multiple callers will wait for the same connection task Future initialize({ required String userId, required String authToken, String? conversationId, }) async { log('═══════════════════════════════════════════', name: 'SignalRService'); log('🔌 [SIGNALR] initialize() called', name: 'SignalRService'); log(' Instance hashCode: ${hashCode}', name: 'SignalRService'); log(' User ID: $userId', name: 'SignalRService'); log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService'); // THREAD SAFETY: If already initializing, wait for current initialization if (_isInitializing && _initializationCompleter != null) { log('⏳ [SIGNALR] Already initializing, waiting for completion...', name: 'SignalRService'); return await _initializationCompleter!.future; } // If already connected with same credentials, reuse connection if (isConnected && _userId == userId && _authToken == authToken) { log('✅ [SIGNALR] Already connected with same credentials', name: 'SignalRService'); log(' Connection ID: ${_hubConnection?.connectionId}', name: 'SignalRService'); // Join new conversation if provided and different if (conversationId != null && conversationId != _currentConversationId) { await _joinConversation(conversationId); } log('═══════════════════════════════════════════', name: 'SignalRService'); return true; } // Start new initialization _isInitializing = true; _initializationCompleter = Completer(); try { // Store credentials _userId = userId; _authToken = authToken; _currentConversationId = conversationId; // Dispose existing connection if any await _disposeConnection(); // Create new connection log('🔧 [SIGNALR] Creating new HubConnection...', name: 'SignalRService'); log(' URL: ${URLs.chatHubUrlChat}', name: 'SignalRService'); final httpOp = HttpConnectionOptions( skipNegotiation: false, logMessageContent: kDebugMode, transport: HttpTransportType.WebSockets, // Use WebSockets requestTimeout: 30000, // 30 seconds timeout ); _hubConnection = HubConnectionBuilder() .withUrl( "${URLs.chatHubUrlChat}?UserId=$userId&source=Desktop&access_token=$authToken", options: httpOp, ) .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]) .build(); log('✅ [SIGNALR] HubConnection created (hashCode: ${_hubConnection.hashCode})', name: 'SignalRService'); // Setup reconnection handlers BEFORE starting connection _setupReconnectionHandlers(); // Start connection with timeout log('🔌 [SIGNALR] Starting connection...', name: 'SignalRService'); try { final startFuture = _hubConnection!.start(); if (startFuture != null) { await startFuture.timeout( const Duration(seconds: 30), onTimeout: () { throw TimeoutException('SignalR connection timeout after 30 seconds'); }, ); } else { log('⚠️ [SIGNALR] start() returned null, checking state...', name: 'SignalRService'); } } catch (e) { log('❌ [SIGNALR] Connection start failed: $e', name: 'SignalRService'); rethrow; } // Verify connection state if (_hubConnection!.state != HubConnectionState.Connected) { throw Exception('SignalR failed to connect. State: ${_hubConnection!.state}'); } log('✅ [SIGNALR] Connection established', name: 'SignalRService'); log(' Connection ID: ${_hubConnection!.connectionId}', name: 'SignalRService'); log(' Connection State: ${_hubConnection!.state}', name: 'SignalRService'); // Emit connected state _connectionStateController.add(HubConnectionState.Connected); // Join conversation if provided if (conversationId != null) { await _joinConversation(conversationId); } // Re-register all event handlers _reregisterAllHandlers(); _isInitializing = false; _initializationCompleter?.complete(true); log('═══════════════════════════════════════════', name: 'SignalRService'); return true; } catch (e, stackTrace) { log('❌ [SIGNALR] Error initializing connection: $e', name: 'SignalRService', error: e, stackTrace: stackTrace); log(' User ID: $userId', name: 'SignalRService'); log(' URL: ${URLs.chatHubUrlChat}', name: 'SignalRService'); _isInitializing = false; _initializationCompleter?.complete(false); log('═══════════════════════════════════════════', name: 'SignalRService'); return false; } } /// Setup reconnection handlers void _setupReconnectionHandlers() { if (_hubConnection == null) return; _hubConnection!.onclose(({Exception? error}) { log('🔴 [SIGNALR] Connection closed: $error', name: 'SignalRService'); _connectionStateController.add(HubConnectionState.Disconnected); }); _hubConnection!.onreconnecting(({Exception? error}) { log('🟡 [SIGNALR] Reconnecting: $error', name: 'SignalRService'); _connectionStateController.add(HubConnectionState.Reconnecting); }); _hubConnection!.onreconnected(({String? connectionId}) async { log('🟢 [SIGNALR] Reconnected: $connectionId', name: 'SignalRService'); _connectionStateController.add(HubConnectionState.Connected); // Rejoin conversation if we had one if (_currentConversationId != null) { await _joinConversation(_currentConversationId!); } // Re-register all event handlers _reregisterAllHandlers(); log('✅ [SIGNALR] Reconnection complete', name: 'SignalRService'); }); } /// 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 /// Events are stored and automatically re-registered after reconnect 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] = []; } // Check for duplicate handler if (_eventHandlers[eventName]!.contains(handler)) { log('⚠️ [SIGNALR] Handler already registered for: $eventName', name: 'SignalRService'); return; } _eventHandlers[eventName]!.add(handler); // Register with SignalR if connected if (_hubConnection != null) { _hubConnection!.on(eventName, handler); log('✅ [SIGNALR] Handler registered for: $eventName', name: 'SignalRService'); } else { log('⚠️ [SIGNALR] Handler stored but not registered (not connected): $eventName', name: 'SignalRService'); } } /// 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 types...', name: 'SignalRService'); int totalHandlers = 0; for (final entry in _eventHandlers.entries) { final eventName = entry.key; final handlers = entry.value; for (final handler in handlers) { _hubConnection!.on(eventName, handler); totalHandlers++; } } log('✅ [SIGNALR] $totalHandlers 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'); if (kDebugMode && args != null && args.isNotEmpty) { log(' Args: ${args.take(3)}${args.length > 3 ? "..." : ""}', name: 'SignalRService'); } return await _hubConnection!.invoke(methodName, args: args); } /// Ensure connection is ready (connect if needed) /// Thread-safe: Multiple callers will wait for the same connection task Future ensureConnected() async { if (isConnected) { return true; } if (_userId != null && _authToken != null) { log('🔄 [SIGNALR] Not connected, attempting to reconnect...', name: 'SignalRService'); 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) { log('🔌 [SIGNALR] Disposing existing connection...', name: 'SignalRService'); 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('═══════════════════════════════════════════', name: 'SignalRService'); log('🔄 [SIGNALR] Resetting service...', name: 'SignalRService'); await _disposeConnection(); _eventHandlers.clear(); _userId = null; _authToken = null; _currentConversationId = null; _isInitializing = false; _initializationCompleter = null; log('✅ [SIGNALR] Service reset complete', name: 'SignalRService'); log('═══════════════════════════════════════════', name: 'SignalRService'); } /// Disconnect from SignalR (alias for reset) Future disconnect() async { await reset(); } /// Dispose stream controller (called when app is terminating) void dispose() { _connectionStateController.close(); } }