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

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