using System.Diagnostics;
namespace EnvelopeRenderer.Cli.Progress;
///
/// Writes PROGRESS lines to the given (stdout in production). Only the
/// high-frequency `render` event is throttled — to at least once per second, per the "Emit
/// machine-readable progress during render" story — because `startup`, `complete`, and `failure`
/// each happen exactly once per run and must never be dropped. The elapsed-time source is
/// injected (rather than reading directly) so tests can drive throttling
/// deterministically without real `Thread.Sleep` calls; wires up a
/// real stopwatch for production use.
///
public sealed class ConsoleProgressReporter : IProgressReporter
{
private readonly TextWriter _output;
private readonly Func _elapsedMillisecondsProvider;
private readonly long _minIntervalMs;
private long? _lastEmittedAtMs;
public ConsoleProgressReporter(
TextWriter output, Func elapsedMillisecondsProvider, long minIntervalMs = 1000)
{
_output = output;
_elapsedMillisecondsProvider = elapsedMillisecondsProvider;
_minIntervalMs = minIntervalMs;
}
/// Production factory: throttles against a real, freshly-started stopwatch.
public static ConsoleProgressReporter CreateDefault(TextWriter output)
{
var stopwatch = Stopwatch.StartNew();
return new ConsoleProgressReporter(output, () => stopwatch.ElapsedMilliseconds);
}
public void Startup() => Write("startup", 0);
public void ReportRenderProgress(int completed)
{
var elapsed = _elapsedMillisecondsProvider();
// Always emit the first call immediately (so a slow run shows *something* right away),
// then suppress further calls until at least _minIntervalMs has passed since the last
// one actually written — this is the "at least once per second, not once per row" rule.
if (_lastEmittedAtMs is not null && elapsed - _lastEmittedAtMs.Value < _minIntervalMs)
{
return;
}
_lastEmittedAtMs = elapsed;
Write("render", completed);
}
public void Complete(int completed) => Write("complete", completed);
public void Failure(int completed, string reason) => Write("failure", completed, reason);
private void Write(string kind, int completed, string? reason = null)
{
_output.WriteLine(ProgressEventFormatter.Format(kind, _elapsedMillisecondsProvider(), completed, reason));
}
}