-
Notifications
You must be signed in to change notification settings - Fork 79
2020, [piegames]: An idiomatic API
You may want to read the previous design notes first because I'm referring to the code they describe.
The event-driven API design with state machines, as is used in the core
module, did not turn out to work very well in my opinion. The public API that resulted from this involved a lot of code to invoke, with a lot of variants to uphold. (Look at the Python API for how to use its Transit
object for an example of what I mean. Though it's a lot better in Python than in Rust due to the different type systems).
The API style I aim for (and I think is more idiomatic Rust) can be seen in async-tungstenite as an example:
- There is code to set up a connection
- Once a connection is established, it is represented as a struct (
WebsocketStream
) to interact with - Drop it to close the connection, maybe there's an optional explicit
close
method
The key benefit of this separation is that no setup or teardown things need to be handled during the connection.
For Wormhole, this means that events like GotCode
or GotWelcome
need not be handled after the connection has been made. Sadly, the connection setup is not as simple as a method to call. Instead, it is split into two parts. The connect_to_server
method will give you the response of the handshake and a struct on which you can call the second part. The transit
API is built in a similar way.
In core
, I kept the existing state machine / events system mostly intact, but with major modifications to the "outer world communication". First of all, there is no blocking/async/tokio abstraction anymore. At the moment, everything is async_std
down the line. I separated the API and the IO layer (which befire where both combined in the WormholeWrapper
I think) and moved the IO layer into the Wormhole. The API of core is still getting ApiEvent
s and firing ApiAction
s. But instead of making this methods of a struct, it simply exposes a Sender and Reveiver, respectively.
The "sane wrapper" around core
is in the root module itself. Basically, the stream and sink to the API are used to handle all handshake-related events. After this, it wraps both in a layer that only exposes Vec<u8>
– message events. The user can only send and receive messages to the other client and none of the other event types. They are all hidden by the API.
As before, the Wormhole
gets events from the outside and responds with actions to perform. The asynchronicity comes from the IO layer: it may receive messages (and thus produce events) at any time. I see two possible solution to this: either give both the library caller and the IO a Arc<Mutex<Wormhole>>
or the like, so that both can take turns at processing their events. Or to put the Wormhole
in an own task and communicate with both via channels. I chose the former solution because the first one wouldn't solve the problem of the produced actions (API events may produce IO actions and vice versa). Therefore, we run the Wormhole in what is called the "event loop".