using Microsoft.AspNetCore.SignalR; using PlotLine.Hubs; namespace PlotLine.Services; public sealed class WordCompanionPresenceMonitor( IWordCompanionPresenceService presence, IManuscriptScanPreviewStore scanStore, IHubContext hub) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); try { while (await timer.WaitForNextTickAsync(stoppingToken)) { var offlineStatuses = await presence.MarkStaleOfflineAsync(); foreach (var status in offlineStatuses) { await hub.Clients .Group(PresenceGroup(status.UserID)) .SendAsync("WordCompanionPresenceChanged", status, stoppingToken); var failedScans = await scanStore.FailRunningForUserAsync( status.UserID, "The Word Companion disconnected before the scan finished. Reopen Word and try again."); foreach (var failedScan in failedScans) { await hub.Clients .Group(PresenceGroup(status.UserID)) .SendAsync("OnboardingScanFailed", failedScan, stoppingToken); } } } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { } } private static string PresenceGroup(int userId) => $"word-companion-presence:{userId}"; }