Editor/Server/QueueLoop.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace HumanoidMocapHost;

/// <summary>Runs queue processing and periodic cleanup without a framework host, so the
/// ASP.NET worker and an editor-embedded service share one loop.</summary>
internal static class QueueLoop
{
    static readonly TimeSpan CleanupInterval = TimeSpan.FromMinutes(1);

    /// <summary>Processes jobs until <paramref name="stopping"/> is cancelled. Processor failures
    /// go to <paramref name="reportFailure"/>; a failure of the loop itself propagates.</summary>
    internal static async Task RunAsync(CaptureQueue queue, Action<Guid, Exception> reportFailure, CancellationToken stopping)
    {
        using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(stopping);
        var processing = queue.RunAsync(reportFailure, lifetime.Token);
        var cleanup = Cleanup(queue, lifetime.Token);
        await Task.WhenAny(processing, cleanup);
        await lifetime.CancelAsync();
        try { await Task.WhenAll(processing, cleanup); }
        catch (OperationCanceledException) when (stopping.IsCancellationRequested) { }
    }

    static async Task Cleanup(CaptureQueue queue, CancellationToken cancellation)
    {
        using var timer = new PeriodicTimer(CleanupInterval);
        while (await timer.WaitForNextTickAsync(cancellation)) queue.Prune();
    }
}