I’m trying to write a ports module to access a subset of PeerJS’s functionality, just for personal use at the moment. Specifically I want to be able to send data between different users. PeerJS provides a broker service that assigns clients an id to find each other and then helps set up a web-RTC connection between them.
However, I’m a bit of a perfectionist and even though the code is just for me I’d like to do it “the right way”, especially as I may end up using the module in multiple projects.
With this in mind I definitely don’t want to just “wrap” the PeerJS functions I need, but instead produce something more Elm-like. After much thinking I decided I wanted an API along the following lines…
A user of the module must first create a type for the data they will be exchanging and create JSON encoders and decoders for it.
They pass these encoders and decoders to a module function to create a RegistrationRequest.
register : (Json.Decode.Decoder data) -> (data -> Json.Encode.Value) -> RegistrationRequest data
Once they have a RegistrationRequest they can use helper functions to add any additional options they may require (e.g. requesting a specific peer id with
requireId) and then
submitRegistration on the request and
waitForCompletion to get a response. On successful registration they get a
Broker data back. This is the only way to create a
Broker, to prevent accidentally trying to start peer communication before everything is set up.
requireId : String -> RegistrationRequest data -> RegistrationRequest data submitRegistration : RegistrationRequest data -> Cmd msg waitForCompletion : RegistrationRequest data -> (Result String (Broker data) -> msg) -> Sub msg
Once the module user has a
Broker specific to their data type they can start to connect to other clients, wait for incoming connections and send data across existing connections. This is all done by calling functions on the
Broker data they hold which return a
Cmd msg. They subscribe to the responses to all these commands by using the subscription they get by invoking
responses on their
responses : Broker data -> (Event data -> msg) -> Sub msg
which will return to them an
Event everytime something happens:
type Event data = Error | NewConnection (Connection data) | DataReceived Connection (Result String data) | ConnectionError (Connection data) String
Note that the
Event is specialised according to their chosen data type. There is no need to fill their code with manually calling coders/decoders but rather they get the the result of trying to decode the received data using the decoder they specified at registration.
I was very happy with all this (but feel free to tell me if you think it’s a rubbish API!). However, the problems came when I actually started trying to implement it…
I decided I wanted the module to be as user-friendly as possible. If the connection to the broker drops it should try to reconnect automatically and only issue an
Error if it fails. If a
But that’s where I hit a problem. The only way I could think of to implement such things on the elm side was to have some kind of event/update loop within the module. I would have to add an extra possibility to my
Event type to deal with internal messages like so:
Event data = InternalMsg | ...
and then mandate that the user call
handleInternalMsg : Broker data -> InternalMsg -> ( Broker data, Cmd msg )
and update their
Broker and cmd’s accordingly.
But what is to stop a user (or more importantly me, six months later when I want to use the module for something new) from just quickly trying to knock something up in their update loop and forgetting this? E.g.:
update: Msg -> Model -> ( Model, Cmd msg ) update msg model = case msg of BrokerMsg event -> case event of DataReceived conn result -> ... _ -> -- Oops! Don't handle InternalMsg ( model, Cmd.none ) OtherMsgs -> ...
Any advice on any of this would be greatly appreciated. Sorry for such a long post, but I wanted to follow the advice about the “XY problem” and being specific about what one is trying to achieve!