using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Channels; using Inspectron.Epson.PrintServer.ConfigurationSources; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; namespace Inspectron.Epson.PrintServer.JobSources; public class SignalRPrintJobSource: IPrintJobSource { private readonly EpsonPrintServiceConfiguration _groupConfiguration; private readonly ILogger _logger; private readonly HubConnection _connection; private readonly Channel _printJobChannel = Channel.CreateUnbounded(); public SignalRPrintJobSource(EpsonPrintServiceConfiguration groupConfiguration, ILogger logger) { _groupConfiguration = groupConfiguration; _logger = logger; _connection = new HubConnectionBuilder() .WithUrl("https://api.dev.gastrojames.ch/hubs/printer-servers") .WithAutomaticReconnect(new InfiniteRetryPolicy()) .Build(); // Register handlers BEFORE starting connection _connection.On("PrintJob", OnPrintJobReceived); _connection.Reconnecting += OnReconnecting; _connection.Reconnected += OnReconnected; _connection.Closed += OnClosed; _ = InitializeConnectionAsync(); } private async Task InitializeConnectionAsync() { try { await _connection.StartAsync(); await _connection.InvokeAsync("JoinGroup", _groupConfiguration.GroupId); _logger.LogInformation("SignalR connection started and joined group {GroupId}.", _groupConfiguration.GroupId); } catch (Exception ex) { _logger.LogError(ex, "Failed to initialize SignalR connection."); } } private Task OnReconnecting(Exception? exception) { _logger.LogWarning(exception, "SignalR connection lost. Reconnecting..."); return Task.CompletedTask; } private async Task OnReconnected(string? connectionId) { _logger.LogInformation("SignalR reconnected with connection ID: {ConnectionId}. Rejoining group...", connectionId); try { await _connection.InvokeAsync("JoinGroup", _groupConfiguration.GroupId); _logger.LogInformation("Rejoined SignalR group {GroupId} after reconnection.", _groupConfiguration.GroupId); } catch (Exception ex) { _logger.LogError(ex, "Failed to rejoin group after reconnection."); } } private async Task OnClosed(Exception? exception) { _logger.LogError(exception, "SignalR connection closed. Attempting manual restart..."); // Manual reconnection loop if automatic reconnection exhausted (shouldn't happen with infinite retry) await Task.Delay(5000); // Wait before retry try { await _connection.StartAsync(); await _connection.InvokeAsync("JoinGroup", _groupConfiguration.GroupId); _logger.LogInformation("Manually reconnected to SignalR."); } catch (Exception ex) { _logger.LogError(ex, "Manual reconnection failed. Will retry on next connection closed event."); } } private void OnPrintJobReceived(PrintJobFromSignalR args) { _logger.LogInformation("Received print job for working area: {PrintJob}", JsonSerializer.Serialize(args)); _printJobChannel.Writer.TryWrite(new PrintJob { IP = args.PrinterIp, Document = args, }); } public async Task GetNextJobAsync(CancellationToken cancellationToken) { return await _printJobChannel.Reader.ReadAsync(cancellationToken); } public class PrintJobFromSignalR { [JsonPropertyName("printerIp")] public string PrinterIp { get; set; } [JsonPropertyName("logoUrl")] public string? LogoUrl { get; set; } [JsonPropertyName("receiptType")] public int ReceiptType { get; set; } [JsonPropertyName("content")] public string Content { get; set; } } private class InfiniteRetryPolicy : IRetryPolicy { public TimeSpan? NextRetryDelay(RetryContext retryContext) { // Exponential backoff with a cap at 30 seconds var delay = Math.Min(Math.Pow(2, retryContext.PreviousRetryCount), 30); return TimeSpan.FromSeconds(delay); } } }