using System.Collections.Concurrent; namespace XFEToolBox.Client.Core.Console; /// /// A thread-safe, single-consumer buffer that preserves console Write/WriteLine semantics. /// /// Metadata needed by the output renderer. public sealed class ConsoleOutputBuffer { private readonly ConcurrentQueue> queue = new(); private readonly object enqueueLock = new(); private bool lastLineIsEnded = true; private long generation; private int count; /// /// Gets the number of entries waiting to be consumed. /// public int Count => Volatile.Read(ref count); /// /// Gets whether no entries are waiting to be consumed. /// public bool IsEmpty => Count == 0; /// /// Gets the current buffer generation. Clearing the buffer starts a new generation. /// public long Generation => Volatile.Read(ref generation); /// /// Enqueues output and atomically determines whether it starts a new logical line. /// public BufferedConsoleOutput Enqueue( string text, TMetadata metadata, bool isLineEnd, bool forceNewLine = false) => Enqueue(_ => text, metadata, isLineEnd, forceNewLine); /// /// Enqueues output using a factory that can add a prefix only when a new line starts. /// public BufferedConsoleOutput Enqueue( Func textFactory, TMetadata metadata, bool isLineEnd, bool forceNewLine = false) { ArgumentNullException.ThrowIfNull(textFactory); lock (enqueueLock) { var startsNewLine = forceNewLine || lastLineIsEnded; var output = new BufferedConsoleOutput( textFactory(startsNewLine), metadata, startsNewLine, isLineEnd, generation); queue.Enqueue(output); Interlocked.Increment(ref count); lastLineIsEnded = isLineEnd; return output; } } /// /// Attempts to read the next entry. This method is intended for a single consumer. /// public bool TryDequeue(out BufferedConsoleOutput output) { if (!queue.TryDequeue(out output)) return false; Interlocked.Decrement(ref count); return true; } /// /// Returns whether an entry belongs to the current, non-cleared generation. /// public bool IsCurrent(BufferedConsoleOutput output) => output.Generation == Generation; /// /// Clears pending output, resets Write/WriteLine state, and returns discarded entries. /// public IReadOnlyList> Clear() { var discarded = new List>(); lock (enqueueLock) { Interlocked.Increment(ref generation); lastLineIsEnded = true; while (queue.TryDequeue(out var output)) { Interlocked.Decrement(ref count); discarded.Add(output); } } return discarded; } } /// /// One buffered console write operation. /// public readonly record struct BufferedConsoleOutput( string Text, TMetadata Metadata, bool StartsNewLine, bool IsLineEnd, long Generation);