I’m looking for some help in how best to avoid writing extra code on the javascript-side of a port. I would love a general purpose strategy to take forward, but becuase I can’t explain very clearly what I mean (and because I might just be attempting something stupid!) I’m going to give a specific example of something I’m doing where it came up…
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 Broker
…
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 Connection
to a specific peer goes down it should try to establish a new one for the user, and automatically accept any reconnection attempts from that peer, etc. But in doing all this I found myself contemplating writing more and more javascript on the other side of the port to make it happen.
Now I’ve never learned javascript, I muddle through it based on experience in other languages. I came to Elm specifically because I didn’t want to have to learn all the eccentricities of javascript. And so I couldn’t help thinking, surely it would be safer, easier and more pleasant to be doing all this on the elm side of the port…
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 ->
...
I eventually came across the idea of writing an effects manager to let me deal with these messages without any fear of the user forgetting to, but it seems that approach is now frowned upon and won’t be available in 0.19. So is there any other way I can ensure that these messages get handled (other than handling it all on the javascript side of the port)?. Or am I just being paranoid about how careless my future self (or others if I ever decide to share it) will be when using the module?
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!