using Avalonia.Threading; using CommunityToolkit.Mvvm.Messaging; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace OTSSignsOrchestrator.Desktop.Services; /// /// Singleton service managing the persistent SignalR connection to the server's FleetHub. /// All handlers dispatch to the UI thread and republish via . /// public sealed class ServerSignalRService : IAsyncDisposable { private readonly HubConnection _connection; private readonly ILogger _logger; public ServerSignalRService( TokenStoreService tokenStore, IConfiguration config, ILogger logger) { _logger = logger; var baseUrl = config["ServerBaseUrl"] ?? "https://localhost:5001"; _connection = new HubConnectionBuilder() .WithUrl($"{baseUrl}/hubs/fleet", options => { options.AccessTokenProvider = () => Task.FromResult(tokenStore.GetJwt()); }) .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(30) }) .Build(); RegisterHandlers(); _connection.Reconnecting += ex => { _logger.LogWarning(ex, "SignalR reconnecting..."); return Task.CompletedTask; }; _connection.Reconnected += connectionId => { _logger.LogInformation("SignalR reconnected (connId={ConnectionId})", connectionId); return Task.CompletedTask; }; _connection.Closed += ex => { _logger.LogWarning(ex, "SignalR connection closed"); return Task.CompletedTask; }; } /// /// Starts the SignalR connection. Call from App.OnFrameworkInitializationCompleted. /// Failures are logged but do not throw — automatic reconnect will retry. /// public async Task StartAsync() { try { await _connection.StartAsync(); _logger.LogInformation("SignalR connected to FleetHub"); } catch (Exception ex) { _logger.LogWarning(ex, "SignalR initial connection failed — will retry via automatic reconnect"); } } public async Task StopAsync() { try { await _connection.StopAsync(); } catch (Exception ex) { _logger.LogWarning(ex, "Error stopping SignalR connection"); } } public HubConnectionState State => _connection.State; private void RegisterHandlers() { _connection.On("SendJobCreated", (jobId, abbrev, jobType) => Dispatcher.UIThread.InvokeAsync(() => WeakReferenceMessenger.Default.Send( new JobCreatedMessage(new(jobId, abbrev, jobType))))); _connection.On("SendJobProgressUpdate", (jobId, stepName, pct, logLine) => Dispatcher.UIThread.InvokeAsync(() => WeakReferenceMessenger.Default.Send( new JobProgressUpdateMessage(new(jobId, stepName, pct, logLine))))); _connection.On("SendJobCompleted", (jobId, success, summary) => Dispatcher.UIThread.InvokeAsync(() => WeakReferenceMessenger.Default.Send( new JobCompletedMessage(new(jobId, success, summary))))); _connection.On("SendInstanceStatusChanged", (customerId, status) => Dispatcher.UIThread.InvokeAsync(() => WeakReferenceMessenger.Default.Send( new InstanceStatusChangedMessage(new(customerId, status))))); _connection.On("SendAlertRaised", (severity, message) => Dispatcher.UIThread.InvokeAsync(() => WeakReferenceMessenger.Default.Send( new AlertRaisedMessage(new(severity, message))))); } public async ValueTask DisposeAsync() { await _connection.DisposeAsync(); } }