Making Elm Lang Realtime with PusherJS

elm_realtime.jpg

Creating realtime applications with PusherJS and Elm.

Introduction

We’re big fans of Elm Lang at Pusher, a functional reactive language that compiles to JavaScript. We recently attended an Elm hacknight in London and ever since we’ve been thinking about how to integrate Pusher into an Elm application. Longer term we’d love to build a native Elm library for Pusher but in the mean time you can successfully integrate by using PusherJS and Elm’s interopability with ports together.

We’ve put our proof of concept onto GitHub if you’re keen to check the code out locally and play around with it. Remember that you’ll need a small server running, such as our Sinatra Pusher Server. Please note that we’re assuming a familiarity with Elm for the rest of this post; if you’re not familiar the Elm docs are a great place to start. We also won’t cover every single bit of the application in this tutorial, I recommend running the application locally if you’d like to really explore the full application.

Interopability with Ports

The typical Pusher integration for a client is as follows:

  • the client makes POST requests to your server with data
  • the server takes that data and triggers Pusher events
  • PusherJS listens for Pusher events and deals with them as the server triggers them

We’re able to use elm-http to make requests from Elm, so we’re able to keep that part native Elm. Once our server triggers events though we’ll use a small bit of JavaScript.

PusherJS and Elm Ports

The JavaScript will fetch our events and then send them back into our Elm application using Elm ports. Ports were introduced in Elm 0.11 and are designed exactly for this purpose; making it easy to communicate between JavaScript and Elm. Although we won’t cover it in this post you’re also able to send data from your Elm application to JavaScript through ports too.

Here’s all the JavaScript we’ll need to write:

1var myApp = Elm.fullscreen(Elm.PusherApp, {
2  newMessage: ''
3});
4
5var pusher = new Pusher('APP_KEY');
6
7var channel = pusher.subscribe('messages');
8
9channel.bind('new_message', function(data) {
10  myApp.ports.newMessage.send(data.text);
11});

First we initialise our Elm application in fullscreen mode. For each port that we need (we just have the one, newMessage) we have to give it an initial value. In our case we just use the empty string. Finally, we then use the PusherJS library to subscribe to any new_message events on the messages channel. When we get an event we grab the text property from the object and send that through to the newMessage port.

Ports in Elm

From the Elm side, a port is just a signal of data, where the data is what’s fed in from the JavaScript side. In our case this will be a Signal String, because we’re just sending through the text from our event. There’s no reason we couldn’t send an entire object though if we wanted to.

All we have to do is define the newMessage port:

1port newMessage : Signal String

And that’s it! We now have a newMessage signal that we can get data from. You can treat this signal just like any other in Elm; there’s no additional special casing because it’s coming from a port. It’s as much a signal as any of the signals Elm provides out of the box for you.

Actions

Because we’re following the Elm Architecture, we have an update method that expects to take an action, model and return the new model. In our case the user actions are as follows:

1type Action
2    = NoOp
3    | NewMessage String
4    | SendMessage
5    | UpdateField String

NewMessage String is the action we want to generate when we get a new value from the newMessage signal. UpdateField String is how we’ll keep track of the value of the input box the user can type their new message in. SendMessage is triggered when the user clicks the button to send their message.

Normally we’d define our update function as follows:

1update : Action -> Model -> Model

But in this case one of the actions has a side effect; the SendMessage action will trigger an HTTP request to our server. Asynchronous tasks like this are represented using the Task module. We use Tasks to represent asynchronous actions that may succeed or fail, such as HTTP requests. At this point I should also thank Peter Damoc, whose answer to my question on the Elm Discuss Mailing List really helped me with this.

The type annotation for our update method looks like so:

1update : Action -> (Model, Task () ()) -> (Model, Task () ())

Our update will be called with an Action and then a tuple. The first item will be our model, and the second will be a Task that will succeed or fail with an empty tuple. Note that in this tutorial we’re not going to discuss error handling, and presume that our requests will always succeed. update is now expected to return the new model and then a task that will succeed or fail. For most of our actions we won’t need to return an actual task to be executed, so we can return Task.succeed (), a Task that when run will immediately succeed with the given value.

Let’s take a look at our full update function:

1update : Action -> ( Model, Task () () ) -> ( Model, Task () () )
2update action ( model, _ ) =
3    case action of
4        NewMessage string ->
5            ( { model | messages = string :: model.messages }
6            , Task.succeed ()
7            )
8
9        UpdateField string ->
10            ( { model | field = string }, Task.succeed () )
11
12        NoOp ->
13            ( model, Task.succeed () )
14
15        SendMessage ->
16            ( { model | field = "" }, postJson model.field )

Note that all of the actions return Task.succeed () (which I tend to think of as a “blank task”), but SendMessage returns postJson model.field, which will return a task.

Dealing with Tasks

You’ll remember earlier that I mentioned we won’t deal with errors in this tutorial, and additionally we don’t need to deal with the return responses of HTTP requests. The tasks returned by Elm-HTTP don’t match the type we need of Task () () but instead return Task Http.RawError Http.Response. We need to map that into a Task () (); this can be done with a silenceTask method:

1silenceTask : Task x a -> Task () ()
2silenceTask task =
3    task
4        |> Task.map (\_ -> ())
5        |> Task.mapError (\_ -> ())

Thanks again to Peter Damoc for suggesting this implementation on the Elm mailing list. In a real application we’d definitely want to deal with errors, and we’ll look more at error handling in a future post.

Now we have a way to take any Task x a and change it into a task that will succeed or fail with (), we can use this in our postJson method:

1postJson : String -> Task () ()
2postJson str =
3    silenceTask
4        <| Http.send
5            Http.defaultSettings
6            { verb = "POST"
7            , headers =
8                [ ( "Content-Type", "application/json" )
9                , ( "Accept", "application/json" )
10                ]
11            , url = "http://localhost:5000/messages"
12            , body = Http.string (jsonBody str)
13            }

This method uses Http.send to create a custom HTTP request – whilst Elm HTTP does provide Http.post, it doesn’t allow us yet to customise the headers, and in our case we need more control over the request. We take the response of this and pass it into silenceTask to transform the Task returned into the type that we need. We encode the body using jsonBody, which uses Elm’s JSON.Encode module, which I’ve imported as JSEncode:

1jsonBody : String -> String
2jsonBody str =
3    JSEncode.encode
4        0
5        (JSEncode.object
6            [ ( "text", JSEncode.string str ) ]
7        )

With this we’re now able to fill in the text field and get the data sent to the server.

Executing Tasks

Now our update method can give us back the new model along with a task to run, we need to actually run them! To do this in Elm we give a task to a port, which will cause it to execute. A port can also take a signal of tasks, which is what we’re going to do here.

First, let’s take a look at our signals in the app:

1newMessageSignal : Signal Action
2newMessageSignal =
3    Signal.map (\str -> (NewMessage str)) newMessage
4
5inputSignal : Signal Action
6inputSignal =
7    Signal.mergeMany [ actions.signal, newMessageSignal ]
8
9modelAndTask : Signal ( Model, Task () () )
10modelAndTask =
11    Signal.foldp update ( initialModel, Task.succeed () ) inputSignal
12
13modelSignal : Signal Model
14modelSignal =
15    Signal.map fst modelAndTask
16
17tasksSignal : Signal (Task () ())
18tasksSignal =
19    Signal.map snd modelAndTask

modelAndTask is the typical method you’ll see in nearly all Elm applications, it maintains the state of the application using Signal.foldp. We have inputSignal as a signal of all the user inputs, which includes the inputs from the newMessage port (mapped to create NewMessage actions) and any from actions, which tracks user events such as mouse clicks.

Once we have modelAndTask which is a signal of our model and the current task we can then create tasksSignal by applying snd to modelAndTask. This creates a signal of tasks that change over time. All we now need to do is pass this to a port to have them run:

1port tasks : Signal (Task () ())
2port tasks =
3    tasksSignal

Conclusion

Elm’s signals, ports and tasks are confusing at first and I’ll happily confess to a lot of head bashing whilst working on this application and blog post. However, once things begin to click they become very powerful and understandable. I’d urge you to pull down this repo, have a play and get a feel for how signals and tasks operate. I’d love to hear how you get on with Elm and any thoughts you might have – feel free to tweet me and let me know.