Files
OpenQuery/Services/StatusReporter.cs
TomiEckert b28d8998f7 feat: parallel async processing and compact output mode
Major performance improvements:
- Parallel search execution across all queries
- Parallel article fetching with 10 concurrent limit
- Parallel embeddings with rate limiting (4 concurrent)
- Polly integration for retry resilience

New features:
- Add -v/--verbose flag for detailed output
- Compact single-line status mode with braille spinner
- StatusReporter service for unified output handling
- Query generation and errors hidden in compact mode
- ANSI escape codes for clean line updates

New files:
- Services/RateLimiter.cs - Semaphore-based concurrency control
- Services/StatusReporter.cs - Verbose/compact output handler
- Models/ParallelOptions.cs - Parallel processing configuration

All changes maintain Native AOT compatibility.
2026-03-18 22:16:28 +01:00

129 lines
3.2 KiB
C#

using System.Threading.Channels;
namespace OpenQuery.Services;
public class StatusReporter : IDisposable
{
private readonly bool _verbose;
private readonly char[] _spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
private string? _currentMessage;
private CancellationTokenSource? _spinnerCts;
private Task? _spinnerTask;
private readonly Channel<string> _statusChannel;
private readonly Task _statusProcessor;
public StatusReporter(bool verbose)
{
_verbose = verbose;
_statusChannel = Channel.CreateUnbounded<string>();
_statusProcessor = ProcessStatusUpdatesAsync();
}
private async Task ProcessStatusUpdatesAsync()
{
await foreach (var message in _statusChannel.Reader.ReadAllAsync())
{
if (_verbose)
{
Console.WriteLine(message);
continue;
}
// Clear current line using ANSI escape code
Console.Write("\r\x1b[K");
// Write new status with spinner (use first spinner char for static updates)
Console.Write($"{_spinnerChars[0]} {message}");
_currentMessage = message;
}
}
public void UpdateStatus(string message)
{
_statusChannel.Writer.TryWrite(message);
}
public void ClearStatus()
{
if (_verbose) return;
Console.Write("\r\x1b[K");
_currentMessage = null;
}
public void WriteFinal(string text)
{
if (_verbose)
{
Console.WriteLine(text);
return;
}
StopSpinner();
Console.Write("\r\x1b[K");
Console.Write(text);
Console.WriteLine();
}
public void StartSpinner()
{
if (_verbose || _spinnerCts != null) return;
_spinnerCts = new CancellationTokenSource();
_spinnerTask = Task.Run(async () =>
{
var spinner = _spinnerChars;
var index = 0;
while (_spinnerCts is { Token.IsCancellationRequested: false })
{
if (_currentMessage != null)
{
Console.Write("\r\x1b[K");
var charIndex = index++ % spinner.Length;
Console.Write($"{spinner[charIndex]} {_currentMessage}");
}
try
{
await Task.Delay(100, _spinnerCts.Token);
}
catch (TaskCanceledException)
{
break;
}
}
}, _spinnerCts.Token);
}
public void StopSpinner()
{
if (_spinnerCts == null) return;
_spinnerCts.Cancel();
_spinnerTask?.GetAwaiter().GetResult();
_spinnerCts = null;
_spinnerTask = null;
}
public void WriteLine(string text)
{
if (_verbose)
{
Console.WriteLine(text);
return;
}
StopSpinner();
ClearStatus();
Console.WriteLine(text);
}
public void Dispose()
{
_statusChannel.Writer.Complete();
_statusProcessor.GetAwaiter().GetResult();
StopSpinner();
}
}