Files
Nexus.Reader/src/NexusReader.UI.Shared/Services/SyncService.cs
T

123 lines
3.9 KiB
C#

using FluentResults;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using NexusReader.Application.Abstractions.Services;
namespace NexusReader.UI.Shared.Services;
public class SyncService : ISyncService, IAsyncDisposable
{
private readonly HttpClient _httpClient;
private readonly INativeStorageService _storageService;
private readonly IPlatformService _platformService;
private readonly ILogger<SyncService> _logger;
private HubConnection? _hubConnection;
private bool _isInitialized;
private CancellationTokenSource? _debounceCts;
public event Func<string, DateTime, Task>? OnProgressReceived;
public event Func<string, double, Task>? OnIngestionProgressReceived;
public SyncService(
HttpClient httpClient,
INativeStorageService storageService,
IPlatformService platformService,
ILogger<SyncService> logger)
{
_httpClient = httpClient;
_storageService = storageService;
_platformService = platformService;
_logger = logger;
}
public async Task<Result> InitializeAsync()
{
if (_isInitialized) return Result.Ok();
var tokenResult = await _storageService.GetSecureString("nexus_auth_token");
if (tokenResult.IsFailed) return Result.Fail("Not authenticated");
var baseUrl = _httpClient.BaseAddress?.ToString() ?? "http://localhost:5000/";
var hubUrl = new Uri(new Uri(baseUrl), "synchub").ToString();
_hubConnection = new HubConnectionBuilder()
.WithUrl(hubUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult<string?>(tokenResult.Value);
})
.WithAutomaticReconnect()
.Build();
_hubConnection.On<string, DateTime>("ProgressUpdated", async (pageId, timestamp) =>
{
// Note: In the future we might want to receive ebookId and progress here too
if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp);
});
_hubConnection.On<string, double>("IngestionProgress", async (message, progress) =>
{
if (OnIngestionProgressReceived != null) await OnIngestionProgressReceived(message, progress);
});
try
{
await _hubConnection.StartAsync();
_isInitialized = true;
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}
private string? _lastSentPageId;
public async Task<Result> UpdateProgressAsync(string pageId, Guid ebookId, double progress, string? chapterTitle, int chapterIndex)
{
if (pageId == _lastSentPageId) return Result.Ok();
// Proper trailing-edge debounce
_debounceCts?.Cancel();
_debounceCts = new CancellationTokenSource();
var token = _debounceCts.Token;
_ = Task.Run(async () =>
{
try
{
await Task.Delay(2000, token);
if (!_isInitialized) await InitializeAsync();
if (_hubConnection?.State == HubConnectionState.Connected)
{
await _hubConnection.SendAsync("UpdateProgress", pageId, ebookId, progress, chapterTitle, token);
_lastSentPageId = pageId;
}
}
catch (TaskCanceledException) { /* Ignored, user kept scrolling */ }
catch (Exception ex)
{
_logger.LogError(ex, "[SyncService] Error sending reading progress for page {PageId}.", pageId);
}
}, token);
return Result.Ok();
}
public async Task DisposeAsync()
{
_debounceCts?.Cancel();
if (_hubConnection != null)
{
await _hubConnection.DisposeAsync();
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
await DisposeAsync();
}
}