using Microsoft.AspNetCore.SignalR; using PlotLine.Hubs; using PlotLine.Models; namespace PlotLine.Services; public sealed class WordCompanionPresenceMonitor( IWordCompanionPresenceService presence, IWritingWorkspacePresenceService writingPresence, 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 workspaceStatus = await writingPresence.GetStatusAsync(status.UserID, new WritingWorkspaceScope { ProjectID = status.LinkedProjectID, BookID = status.LinkedBookID }, status); await hub.Clients .Group(PresenceGroup(status.UserID)) .SendAsync("WritingWorkspacePresenceChanged", workspaceStatus, 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); } } } var staleWorkspaceStatuses = await writingPresence.MarkStaleOfflineAsync(presence.GetStatusAsync); foreach (var workspaceStatus in staleWorkspaceStatuses) { await hub.Clients .Group(PresenceGroup(workspaceStatus.UserID)) .SendAsync("WritingWorkspacePresenceChanged", workspaceStatus, stoppingToken); } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { } } private static string PresenceGroup(int userId) => $"word-companion-presence:{userId}"; }