@@ -13,7 +13,7 @@ namespace DotNext.Net.Multiplexing;
1313[ Experimental ( "DOTNEXT001" ) ]
1414public abstract partial class MultiplexedClient : Disposable , IAsyncDisposable
1515{
16- private readonly TaskCompletionSource readiness ;
16+ private volatile TaskCompletionSource ? readiness ;
1717 private Task dispatcher ;
1818
1919 [ SuppressMessage ( "Usage" , "CA2213" , Justification = "False positive" ) ]
@@ -46,6 +46,9 @@ protected MultiplexedClient(Options configuration)
4646 output = input . CreateOutput ( GC . AllocateArray < byte > ( configuration . BufferCapacity , pinned : true ) , configuration . Timeout ) ;
4747 }
4848
49+ private Task WaitForConnectionCoreAsync ( CancellationToken token )
50+ => readiness ? . Task . WaitAsync ( token ) ?? Task . CompletedTask ;
51+
4952 /// <summary>
5053 /// Connects to the server and starts the dispatching loop.
5154 /// </summary>
@@ -54,14 +57,27 @@ protected MultiplexedClient(Options configuration)
5457 /// <exception cref="ObjectDisposedException">The client is disposed.</exception>
5558 public ValueTask StartAsync ( CancellationToken token = default )
5659 {
60+ var task = WaitForConnectionCoreAsync ( token ) ;
5761 if ( ReferenceEquals ( dispatcher , Task . CompletedTask ) )
5862 {
5963 dispatcher = DispatchAsync ( ) ;
6064 }
6165
62- return new ( readiness . Task . WaitAsync ( token ) ) ;
66+ return new ( task ) ;
6367 }
6468
69+ /// <summary>
70+ /// Waits for the connection to be established.
71+ /// </summary>
72+ /// <remarks>
73+ /// The method can be called to ensure that the connection to the server is established successfully.
74+ /// This is useful when the underlying connection is lost to prevent inflation of the stream IDs.
75+ /// </remarks>
76+ /// <param name="token">The token that can be used to cancel the operation.</param>
77+ /// <returns>The task representing connection state.</returns>
78+ public ValueTask WaitForConnectionAsync ( CancellationToken token = default )
79+ => new ( WaitForConnectionCoreAsync ( token ) ) ;
80+
6581 /// <summary>
6682 /// Creates a new multiplexed client stream.
6783 /// </summary>
@@ -84,11 +100,16 @@ public ValueTask StartAsync(CancellationToken token = default)
84100 /// <returns>A duplex pipe for data input/output.</returns>
85101 /// <seealso cref="DotNext.IO.Pipelines.DuplexStream"/>
86102 public ValueTask < IDuplexPipe > OpenStreamAsync ( CancellationToken token = default )
87- => readiness . Task . IsCompletedSuccessfully ? new ( OpenStream ( ) ) : OpenStreamCoreAsync ( token ) ;
103+ {
104+ var readinessCopy = readiness ;
105+ return readinessCopy is null or { Task . IsCompletedSuccessfully : true }
106+ ? new ( OpenStream ( ) )
107+ : OpenStreamCoreAsync ( readinessCopy . Task , token ) ;
108+ }
88109
89- private async ValueTask < IDuplexPipe > OpenStreamCoreAsync ( CancellationToken token )
110+ private async ValueTask < IDuplexPipe > OpenStreamCoreAsync ( Task readinessTask , CancellationToken token )
90111 {
91- await readiness . Task . WaitAsync ( token ) . ConfigureAwait ( false ) ;
112+ await readinessTask . WaitAsync ( token ) . ConfigureAwait ( false ) ;
92113 return OpenStream ( ) ;
93114 }
94115
@@ -130,7 +151,6 @@ private void Cancel()
130151 {
131152 if ( Interlocked . Exchange ( ref lifetimeTokenSource , null ) is { } cts )
132153 {
133- readiness . TrySetException ( new ObjectDisposedException ( GetType ( ) . Name ) ) ;
134154 using ( cts )
135155 {
136156 cts . Cancel ( ) ;
@@ -143,6 +163,8 @@ protected override async ValueTask DisposeAsyncCore()
143163 {
144164 Cancel ( ) ;
145165 await dispatcher . ConfigureAwait ( ConfigureAwaitOptions . SuppressThrowing ) ;
166+ ReportDisposed ( ) ;
167+
146168 await input . DisposeAsync ( ) . ConfigureAwait ( false ) ;
147169 await output . DisposeAsync ( ) . ConfigureAwait ( false ) ;
148170 writeSignal . Dispose ( ) ;
0 commit comments