|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
/// Use an existing HubConnection (from ChatProvider)
|
|
|
|
|
/// This prevents creating duplicate connections
|
|
|
|
|
void useExistingConnection(HubConnection existingConnection, {
|
|
|
|
|
String? userId,
|
|
|
|
|
String? authToken,
|
|
|
|
|
String? conversationId,
|
|
|
|
|
}) {
|
|
|
|
|
log('═══════════════════════════════════════════', name: 'SignalRService');
|
|
|
|
|
log('🔌 [SIGNALR] Using existing HubConnection', name: 'SignalRService');
|
|
|
|
|
log(' Connection ID: ${existingConnection.connectionId}', name: 'SignalRService');
|
|
|
|
|
log(' User ID: ${userId ?? "not set"}', name: 'SignalRService');
|
|
|
|
|
log(' Conversation ID: ${conversationId ?? "not set"}', name: 'SignalRService');
|
|
|
|
|
log('═══════════════════════════════════════════', name: 'SignalRService');
|
|
|
|
|
|
|
|
|
|
_hubConnection = existingConnection;
|
|
|
|
|
_userId = userId;
|
|
|
|
|
_authToken = authToken;
|
|
|
|
|
_currentConversationId = conversationId;
|
|
|
|
|
|
|
|
|
|
// Re-register all stored event handlers on this connection
|
|
|
|
|
_reregisterAllHandlers();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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');
|
|
|
|
|
}
|
|
|
|
|
}
|