|
|
|
|
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<bool>? _initializationCompleter;
|
|
|
|
|
|
|
|
|
|
// Authentication data
|
|
|
|
|
String? _userId;
|
|
|
|
|
String? _authToken;
|
|
|
|
|
String? _currentConversationId;
|
|
|
|
|
|
|
|
|
|
// Event handler registrations for re-registration after reconnect
|
|
|
|
|
final Map<String, List<Function(List<Object?>?)>> _eventHandlers = {};
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
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<bool> 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<bool>();
|
|
|
|
|
|
|
|
|
|
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: <int>[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<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
|
|
|
|
|
/// Events are stored and automatically re-registered after reconnect
|
|
|
|
|
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] = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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<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 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<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');
|
|
|
|
|
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<bool> 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<void> _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<void> 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<void> disconnect() async {
|
|
|
|
|
await reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Dispose stream controller (called when app is terminating)
|
|
|
|
|
void dispose() {
|
|
|
|
|
_connectionStateController.close();
|
|
|
|
|
}
|
|
|
|
|
}
|