192 lines
7.6 KiB
C#
192 lines
7.6 KiB
C#
|
|
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<EntityChangeHubService> _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<string, string, string>? EntityChanged;
|
||
|
|
|
||
|
|
public EntityChangeHubService(
|
||
|
|
string apiBaseUrl,
|
||
|
|
IServiceProvider serviceProvider,
|
||
|
|
ILogger<EntityChangeHubService> 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<object>("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<EntityChangeMessage>(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;
|
||
|
|
}
|