using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; namespace DiunaBI.UI.Shared.Services; public class EntityChangeHubService : IAsyncDisposable { private readonly string _hubUrl; private readonly ILogger _logger; private HubConnection? _hubConnection; private bool _isInitialized; private readonly SemaphoreSlim _initializationLock = new SemaphoreSlim(1, 1); private static int _instanceCounter = 0; private readonly int _instanceId; // Events that components can subscribe to public event Action? EntityChanged; public EntityChangeHubService( string apiBaseUrl, IServiceProvider serviceProvider, ILogger logger) { _instanceId = Interlocked.Increment(ref _instanceCounter); // Convert HTTP URL to SignalR hub URL var baseUrl = apiBaseUrl.TrimEnd('/'); _hubUrl = baseUrl + "/hubs/entitychanges"; _logger = logger; _logger.LogInformation("🏗️ EntityChangeHubService instance #{InstanceId} created. Hub URL: {HubUrl}", _instanceId, _hubUrl); Console.WriteLine($"🏗️ EntityChangeHubService instance #{_instanceId} created. Hub URL: {_hubUrl}, _isInitialized = {_isInitialized}"); } public async Task InitializeAsync() { _logger.LogInformation("🔍 Instance #{InstanceId} InitializeAsync called. _isInitialized = {IsInitialized}, _hubConnection null? {IsNull}", _instanceId, _isInitialized, _hubConnection == null); Console.WriteLine($"🔍 Instance #{_instanceId} InitializeAsync called. _isInitialized = {_isInitialized}, _hubConnection null? {_hubConnection == null}"); if (_isInitialized) { _logger.LogInformation("⏭️ Instance #{InstanceId} SignalR already initialized, skipping", _instanceId); Console.WriteLine($"⏭️ Instance #{_instanceId} SignalR already initialized, skipping"); return; } await _initializationLock.WaitAsync(); try { // Double-check after acquiring lock if (_isInitialized) { Console.WriteLine($"⏭️ SignalR already initialized (after lock), skipping"); return; } _logger.LogInformation("🔌 Initializing SignalR connection to {HubUrl}", _hubUrl); Console.WriteLine($"🔌 Initializing SignalR connection to {_hubUrl}"); _hubConnection = new HubConnectionBuilder() .WithUrl(_hubUrl) .WithAutomaticReconnect() .Build(); // Subscribe to EntityChanged messages _hubConnection.On("EntityChanged", (data) => { Console.WriteLine($"🔔 RAW SignalR message received at {DateTime.Now:HH:mm:ss.fff}"); Console.WriteLine($"🔔 Data type: {data?.GetType().FullName}"); try { // Parse the anonymous object var json = System.Text.Json.JsonSerializer.Serialize(data); Console.WriteLine($"📨 Received SignalR message: {json}"); // Use case-insensitive deserialization (backend sends camelCase: module, id, operation) var options = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var change = System.Text.Json.JsonSerializer.Deserialize(json, options); if (change != null) { _logger.LogInformation("📨 Received entity change: {Module} {Id} {Operation}", change.Module, change.Id, change.Operation); Console.WriteLine($"📨 Entity change: {change.Module} {change.Id} {change.Operation}"); // Notify all subscribers Console.WriteLine($"🔔 Invoking EntityChanged event, subscribers: {EntityChanged?.GetInvocationList().Length ?? 0}"); EntityChanged?.Invoke(change.Module, change.Id, change.Operation); Console.WriteLine($"🔔 EntityChanged event invoked successfully"); } else { Console.WriteLine($"⚠️ Deserialized change is null"); } } catch (Exception ex) { _logger.LogError(ex, "❌ Error processing entity change message"); Console.WriteLine($"❌ Error processing message: {ex.Message}"); Console.WriteLine($"❌ Stack trace: {ex.StackTrace}"); } }); _hubConnection.Reconnecting += (error) => { _logger.LogWarning("SignalR reconnecting: {Error}", error?.Message); Console.WriteLine($"⚠️ SignalR reconnecting: {error?.Message}"); return Task.CompletedTask; }; _hubConnection.Reconnected += (connectionId) => { _logger.LogInformation("✅ SignalR reconnected: {ConnectionId}", connectionId); Console.WriteLine($"✅ SignalR reconnected: {connectionId}"); return Task.CompletedTask; }; _hubConnection.Closed += (error) => { _logger.LogError(error, "❌ SignalR connection closed"); Console.WriteLine($"❌ SignalR connection closed: {error?.Message}"); return Task.CompletedTask; }; await StartConnectionAsync(); _isInitialized = true; _logger.LogInformation("✅ Instance #{InstanceId} _isInitialized set to true", _instanceId); Console.WriteLine($"✅ Instance #{_instanceId} _isInitialized set to true"); } catch (Exception ex) { _logger.LogError(ex, "❌ Instance #{InstanceId} Failed to initialize SignalR connection", _instanceId); Console.WriteLine($"❌ Instance #{_instanceId} Failed to initialize SignalR: {ex.Message}"); } finally { _initializationLock.Release(); } } private async Task StartConnectionAsync() { if (_hubConnection == null) { _logger.LogWarning("Hub connection is null, cannot start"); return; } try { Console.WriteLine($"🔌 Starting SignalR connection..."); await _hubConnection.StartAsync(); _logger.LogInformation("✅ SignalR connected successfully"); Console.WriteLine($"✅ SignalR connected successfully to {_hubUrl}"); } catch (Exception ex) { _logger.LogError(ex, "❌ Failed to start SignalR connection"); Console.WriteLine($"❌ Failed to start SignalR: {ex.Message}\n{ex.StackTrace}"); } } public async ValueTask DisposeAsync() { if (_hubConnection != null) { try { await _hubConnection.StopAsync(); await _hubConnection.DisposeAsync(); } catch (Exception ex) { _logger.LogError(ex, "Error disposing SignalR connection"); } } _initializationLock?.Dispose(); } } public class EntityChangeMessage { public string Module { get; set; } = string.Empty; public string Id { get; set; } = string.Empty; public string Operation { get; set; } = string.Empty; }