Registry that tracks pending client command requests, assigns unique CommandRequestId, times out or cancels them, and completes associated TaskCompletionSource when a response or cancellation occurs. It enforces a maximum pending limit and completes all pending commands with a failure when disposed.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;
namespace Hexagon.V2.Client;
public readonly record struct PendingClientCommand(
CommandRequestId RequestId,
ValueTask<OperationResult> Completion );
/// <summary>
/// Correlates one-way RPC messages into truthful client command completions.
/// The registry owns timeout and cancellation races; exactly one terminal result
/// wins and every pending task is completed when the transport is disposed.
/// </summary>
public sealed class PendingCommandRegistry : IDisposable
{
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds( 15 );
public const int DefaultMaximumPending = 64;
private readonly object _sync = new();
private readonly Dictionary<CommandRequestId, PendingState> _pending = new();
private readonly TimeSpan _timeout;
private readonly int _maximumPending;
private bool _disposed;
public PendingCommandRegistry(
TimeSpan? timeout = null,
int maximumPending = DefaultMaximumPending )
{
_timeout = timeout ?? DefaultTimeout;
if ( _timeout <= TimeSpan.Zero ) throw new ArgumentOutOfRangeException( nameof(timeout) );
if ( maximumPending <= 0 ) throw new ArgumentOutOfRangeException( nameof(maximumPending) );
_maximumPending = maximumPending;
}
public int Count
{
get { lock ( _sync ) return _pending.Count; }
}
public OperationResult<PendingClientCommand> Register( CancellationToken cancellationToken = default )
{
cancellationToken.ThrowIfCancellationRequested();
PendingState state;
lock ( _sync )
{
if ( _disposed )
return OperationResult<PendingClientCommand>.Failure( ErrorCode.InternalError, "Command transport is disposed." );
if ( _pending.Count >= _maximumPending )
return OperationResult<PendingClientCommand>.Failure( ErrorCode.Conflict, "Too many commands are awaiting a host response." );
CommandRequestId requestId;
do requestId = CommandRequestId.New(); while ( _pending.ContainsKey( requestId ) );
state = new PendingState( this, requestId, cancellationToken, _timeout );
_pending.Add( requestId, state );
state.Start();
}
return OperationResult<PendingClientCommand>.Success(
new PendingClientCommand( state.RequestId, new ValueTask<OperationResult>( state.Task ) ) );
}
public bool IsPending( CommandRequestId requestId )
{
lock ( _sync ) return _pending.ContainsKey( requestId );
}
public bool Complete( CommandRequestId requestId, OperationResult result )
{
var state = Remove( requestId );
if ( state is null ) return false;
state.Complete( result );
return true;
}
public void Dispose()
{
PendingState[] pending;
lock ( _sync )
{
if ( _disposed ) return;
_disposed = true;
pending = _pending.Values.ToArray();
_pending.Clear();
}
var failure = OperationResult.Failure( ErrorCode.InternalError, "Command transport closed before the host responded." );
foreach ( var state in pending ) state.Complete( failure );
}
private PendingState? Remove( CommandRequestId requestId )
{
lock ( _sync )
{
if ( !_pending.Remove( requestId, out var state ) ) return null;
return state;
}
}
private void Cancel( PendingState candidate )
{
var state = Remove( candidate.RequestId );
if ( !ReferenceEquals( state, candidate ) ) return;
state.Cancel();
}
private void Timeout( PendingState candidate )
{
var state = Remove( candidate.RequestId );
if ( !ReferenceEquals( state, candidate ) ) return;
state.Complete( OperationResult.Failure( ErrorCode.InternalError, "Host command response timed out." ) );
}
private sealed class PendingState
{
private readonly PendingCommandRegistry _owner;
private readonly CancellationToken _externalToken;
private readonly TimeSpan _timeout;
private readonly TaskCompletionSource<OperationResult> _completion =
new( TaskCreationOptions.RunContinuationsAsynchronously );
private readonly CancellationTokenSource _timeoutCancellation = new();
private CancellationTokenRegistration _externalRegistration;
private CancellationTokenRegistration _timeoutRegistration;
public PendingState(
PendingCommandRegistry owner,
CommandRequestId requestId,
CancellationToken externalToken,
TimeSpan timeout )
{
_owner = owner;
RequestId = requestId;
_externalToken = externalToken;
_timeout = timeout;
}
public CommandRequestId RequestId { get; }
public Task<OperationResult> Task => _completion.Task;
public void Start()
{
_timeoutRegistration = _timeoutCancellation.Token.Register(
static value => ((PendingState)value!)._owner.Timeout( (PendingState)value ), this );
_timeoutCancellation.CancelAfter( _timeout );
var externalRegistration = _externalToken.Register(
static value => ((PendingState)value!)._owner.Cancel( (PendingState)value ), this );
_externalRegistration = externalRegistration;
if ( !_owner.IsPending( RequestId ) ) externalRegistration.Dispose();
}
public void Complete( OperationResult result )
{
Cleanup();
_completion.TrySetResult( result );
}
public void Cancel()
{
Cleanup();
_completion.TrySetCanceled( _externalToken );
}
private void Cleanup()
{
_timeoutCancellation.Dispose();
_timeoutRegistration.Dispose();
_externalRegistration.Dispose();
}
}
}