United Kingdom: +44 (0)208 088 8978

Elmish Bridge

In this post, we share our experience with Elmish Bridge, a nifty library that brings WebSockets to your Elmish apps

We're hiring Software Developers

Click here to find out more

Every couple of months, the developers of CIT get together for a Hack Day where we get together and build an application of choice; the main goal here is to get our hands dirty with a technology we are unfamiliar with. During one of these Hack Days, we built a chat app using Elmish Bridge, a library that allows you to use WebSockets in an Elmish Application.

A standard SAFE app uses Fable Remoting to communicate between the client and server. Fable Remoting uses plain old HTTP and blends in very nicely into the Elmish workflow by allowing us provide a message dispatcher as a success callback; take a look at how the standard SAFE todo list uses this to deal with adding a todo to the todo list:

| AddTodo msg ->
    match msg with
    | Start() ->
        let todo = Todo.create model.Input
        let cmd = Cmd.OfAsync.perform todosApi.addTodo todo (Finished >> AddTodo)
        { model with Input = "" }, cmd
    | Finished todo ->
        {
            model with
                Todos = model.Todos.Map(fun todos -> todos @ [ todo ])
        },
        Cmd.none

Very elegant!

For a lot of applications this pattern will do: you send a message down to the server, and deal with the result. But what if the server wants to take initiative? WebSockets to the rescue! When using WebSockets, you establish a lasting connection between the client and server, so the server also has the possibility to send messages to the client, without any client initiative apart from establishing the initial connection.

The Elmish.Bridge library provides a nice way to use WebSockets in an Elmish Application, and it's not even hard to set up! The only thing we initially missed was adding the proxy to vite.config.mts

...
server: {
    port: 8080,
    proxy: {
        // redirect requests that start with /api/ to the server on port 5000
        "/api/": {
            target: proxyTarget,
           changeOrigin: true,
        },
        "/bridge": {
            target: "ws://localhost:" + proxyPort,
            ws: true,
        },
    }
}
...

API

Once you have that set up, the rest is easy. Begin with defining an API in the Shared project. You need 2 types: One describing the messages that are sent from the server to the client, and one describing messages from the client to the server.

Our chat app has a "malicious" functionality that allows you to execute JS on the receiver's device. Obviously not something you want to do in a production app!

 module Bridge =
    type ClientToServerMsg =
        | PostMessage of userName:string * message:string
        | PostJs of userName:string option * script: string
    type ServerToClientMsg =
        | MessagePosted of ChatMessage
        | ExecuteJs of userName:string option * script: string
    let endpoint = "/bridge"

Server Code

Elmish Bridge uses an Elmish loop on the server side to deal with the messages. We add an Init and Update Message, and hook that into the bridge. We also add a hub: The hub allows you to broadcast messages to all users.

let hub =
    ServerHub()
        .RegisterServer(FromClient)
        .RegisterClient(MessagePosted)

let init (clientDispatch: Dispatch) () =
    (), Cmd.none
let update (clientDispatch: Dispatch) (msg: ServerMsg) (model: ServerModel) =
    match msg with
    | FromClient msg ->
        match msg with
        | PostMessage (userName, message) ->
            {
                Content = message
                Id = Guid.NewGuid()
                Sender = userName
                Date = DateTime.UtcNow
            }
            |> MessagePosted
            |> hub.BroadcastClient
        | PostJs (userName, script) ->
            (userName, script)
            |> ExecuteJs
            |> hub.BroadcastClient

    model, Cmd.none

let bridge =
    Bridge.mkServer Bridge.endpoint init update
    |> Bridge.withServerHub hub
    |> Bridge.run Giraffe.server            

You also need to hook the app into the router, and enable WebSockets

let router = choose [
    bridge
    remotingApi
]

let app = application {
    use_router router
    memory_cache
    use_static "public"
    use_gzip
    app_config Giraffe.useWebSockets
}

Client code

On the client side you'll also need to hook up the bridge in `App.fs`.

Program.mkProgram Index.init Index.update Index.view
|> Program.withBridgeConfig (
   Bridge.endpoint Shared.Bridge.endpoint
   |> Bridge.withMapping Index.Msg.FromServer )

Now you can start receiving from and start sending messages to the server, in a way that's very similar to how you'd deal with regular Elmish messages!

let update (msg: ClientMsg) (model: Model) : Model * Cmd =
    match msg with
    | SetInput value -> { model with Input = value }, Cmd.none
    | SendMessage ->
        match model.User with
        | Some user ->
            let cmd =
                ...
                    Cmd.bridgeSend (PostMessage (user, model.Input))

            { model with Input = "" }, cmd
        | None ->
            model, Cmd.none
    | FromServer msg ->
        match msg with
        | MessagePosted message ->
            async {
                do! Async.Sleep 100 // delay to allow rendering of the new message
                scrollToBottom()
            }
            |> Async.StartImmediate

            { model with Messages = Array.append model.Messages [| message |] }, Cmd.none
...

As you can see, getting Elmish bridge running is really quite easy and makes using WebSockets a breeze. Elmish bridge has a couple more tricks up its sleeve, such as being able to target messages to specific users. Use their docs to learn all about it!

While we had fun implementing Elmish bridge in this app, we are not convinced we should start using it in production environments; the library has not seen new releases in a bit over a year. We also felt like the Elmish bridge has a hard time recovering from errors, which is quite important in the case of Websockets, where a connection is used over an extended period of time.