using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Tests.TestSupport; /// Coordinates a fake worker's incoming frames and event emission gate. internal sealed class GatedWorkerEnvelopeReader( Func> read, SemaphoreSlim gate, CancellationToken cancellationToken) : IAsyncDisposable { private readonly CancellationTokenSource _lifetime = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); private Task? _pendingRead; private Task? _pendingGate; /// Reads an envelope, or returns null when emission should be checked. /// Whether the worker is currently subscribed and emitting. /// The next complete envelope, or null for an emission signal. public async Task ReadOrGateAsync(bool watchGate = true) { // A gate wakeup must never cancel or replace a partially consumed frame. _pendingRead ??= read(_lifetime.Token); if (watchGate) { _pendingGate ??= gate.WaitAsync(_lifetime.Token); await Task.WhenAny(_pendingRead, _pendingGate).ConfigureAwait(false); if (!_pendingRead.IsCompleted) { await _pendingGate.ConfigureAwait(false); _pendingGate = null; return null; } } WorkerEnvelope envelope = await _pendingRead.ConfigureAwait(false); _pendingRead = null; return envelope; } /// public async ValueTask DisposeAsync() { await _lifetime.CancelAsync().ConfigureAwait(false); try { if (_pendingRead is not null) { await _pendingRead.ConfigureAwait(false); } } catch (OperationCanceledException) when (_lifetime.IsCancellationRequested) { } finally { try { if (_pendingGate is not null) { await _pendingGate.ConfigureAwait(false); } } catch (OperationCanceledException) when (_lifetime.IsCancellationRequested) { } _lifetime.Dispose(); } } }