This article / idea really refactors two things out of some IO code<p>- the event loop<p>- the state machine of data states that occur<p>But async rust is already a state machine, so the stun binding could be expressed as a 3 line async function that is fairly close to sans-io (if you don't consider relying on abstractions like Stream and Sink to be IO).<p><pre><code> async fn stun(
server: SocketAddr,
mut socket: impl Sink<(BindingRequest, SocketAddr), Error = color_eyre::Report>
+ Stream<Item = Result<(BindingResponse, SocketAddr), color_eyre::Report>>
+ Unpin
+ Send
+ 'static,
) -> Result<SocketAddr> {
socket.send((BindingRequest, server)).await?;
let (message, _server) = socket.next().await.ok_or_eyre("No response")??;
Ok(message.address)
}
</code></pre>
If you look at how the underlying async primitives are implemented, they look pretty similar to what you;ve implemented. sink.send is just a future for Option<SomeMessage>, a future is just something that can be polled at some later point, which is exactly equivalent to your event loop constructing the StunBinding and then calling poll_transmit to get the next message. And the same goes with the stream.next call, it's the same as setting up a state machine that only proceeds when there is a next item that is being fed to it. The Tokio runtime is your event loop, but just generalized.<p>Restated simply: stun function above returns a future that that combines the same methods you have with a contract about how that interacts with a standard async event loop.<p>The above is testable without hitting the network. Just construct the test Stream / Sink yourself. It also easily composes to add timeouts etc. To make it work with the network instead pass in a UdpFramed (and implement codecs to convert the messages to / from bytes).<p>Adding timeout can be either composed from the outside caller if it's a timeout imposed by the application, or inside the function if it's a timeout you want to configure on the call. This can be tested using tokio test-utils and pausing / advancing the time in your tests.<p>---<p>The problem with the approach suggested in the article is that it splits the flow (event loop) and logic (statemachine) from places where the flow is the logic (send a stun binding request, get an answer).<p>Yes, there's arguments to be made about not wanting to use async await, but when you effectively create your own custom copy of async await, just without the syntactic sugar, and without the various benefits (threading, composability, ...), it's worth considering whether you could use async instead.