using Collaboration.Application.Interfaces; using Collaboration.Application.Services; using Collaboration.Hubs; using Collaboration.Infrastructure.Persistence; using Collaboration.Infrastructure.Repositories; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Collaboration.Api; public static class CollabModule { /// /// Register all Collaboration module services. /// Call from your main application's Program.cs: /// builder.Services.AddCollaborationModule(builder.Configuration); /// public static IServiceCollection AddCollaborationModule( this IServiceCollection services, IConfiguration configuration) { // DbContext — use separate connection string or share your main one services.AddDbContext(options => options.UseSqlServer( configuration.GetConnectionString("CollabConnection") ?? configuration.GetConnectionString("DefaultConnection"), sql => sql.MigrationsHistoryTable("__CollabMigrationsHistory", "collab") )); // Repositories services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Application services services.AddScoped(); services.AddScoped(); // SignalR services.AddSignalR(options => { options.EnableDetailedErrors = false; // Set true in Development options.KeepAliveInterval = TimeSpan.FromSeconds(15); options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); options.HandshakeTimeout = TimeSpan.FromSeconds(10); options.MaximumReceiveMessageSize = 32 * 1024; // 32KB — enough for payloads }); // Background service for session cleanup services.AddHostedService(); return services; } /// /// Map Collaboration module endpoints. /// Call from your main application's Program.cs after app.UseAuthorization(): /// app.UseCollaborationModule(); /// public static WebApplication UseCollaborationModule(this WebApplication app) { app.MapHub("/hubs/collab", options => { options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets | Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling; }); return app; } } // ── Background cleanup service ─────────────────────────────────────────────── /// /// Runs every 5 minutes to auto-abandon timed-out sessions. /// Configurable timeout per org is respected. /// public class CollabSessionCleanupService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; private static readonly TimeSpan Interval = TimeSpan.FromMinutes(5); public CollabSessionCleanupService( IServiceScopeFactory scopeFactory, ILogger logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await Task.Delay(Interval, stoppingToken); await RunCleanupAsync(stoppingToken); } } private async Task RunCleanupAsync(CancellationToken ct) { using var scope = _scopeFactory.CreateScope(); var sessions = scope.ServiceProvider.GetRequiredService(); var hub = scope.ServiceProvider.GetRequiredService< IHubContext>(); try { // Use default timeout of 30 mins for the scan — // fine-grained per-org timeout can be added when needed var candidates = await sessions.GetAbandonedCandidatesAsync(timeoutMinutes: 30, ct); foreach (var session in candidates) { session.End(Guid.Empty, isAbandoned: true); await hub.Clients.Group($"session:{session.SessionId}") .SessionEnded("Session ended due to inactivity."); } if (candidates.Count > 0) { await sessions.SaveChangesAsync(ct); _logger.LogInformation("Abandoned {Count} inactive sessions", candidates.Count); } } catch (Exception ex) { _logger.LogError(ex, "Error during session cleanup"); } } }