Phoenix Channels via F#

Premise

I’m an FP cheerleader, and I enjoy both Elixir and F#. Both are lovely, but they are very different (different VMs, different priorities, different sweet-spots). This is what I look for when evaluating my toolbox each year. A set of distinct complementary tools is much better than a box full of hammers.

I’m hopeful this F# Advent Calendar post will spark ideas on how you might use the two languages together. My goal isn’t to present a polished, client library, but more to trick inspire you to write one.

Background on Phoenix Channels

If you are unfamiliar with Elixir…[info info info]… Phoenix is the primary web framework for Elixir. …[info info info]… Channels is a mechanism for topic based communication.

Prerequisites

We’re going to need:

  • F#
  • Elixir
  • A Phoenix application to test against

Setting up a sample Phoenix app

To see everything in action we will use a sample Phoenix chat application was written by Chris McCord (the creator of Phoenix). You can find the sample here: // https://github.com/chrismccord/phoenix_chat_example)

Playing with Channels from the F# REPL

Phoenix channels supports pluggable transports. The two in-the-box transports are WebSockets and LongPolling. In our F# client we will talk to Phoenix via WebSockets and we will piggyback on WebSocketSharp to handle to communication bits that aren’t specific Phoenix channels.

// #r @"..\packages\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll"
open WebSocketSharp

Next we will create a WebSocket pointing to our Phoenix channel route.

let ws = new WebSocket("ws://localhost:4000/socket/websocket")

So we’re not flying blind we will hook up logging to the WebSocketSharp events.

let log msg = printfn "%s" msg

ws.OnOpen.Add(fun args -> log "Open")
ws.OnClose.Add(fun args -> log "Close")
ws.OnMessage.Add(fun args -> log("Msg: " + args.Data))
ws.OnError.Add(fun args -> log("Error: " + args.Message))

Next we will connect.

ws.Connect()

Now we get to the first Phoenix-specific bits. We will join a channel called “rooms:lobby”

ws.Send("{\"ref\":\"1\", \"event\":\"phx_join\", \"topic\":\"rooms:lobby\",
\"payload\":{\"user\":\"boo\"}}")

Other users will see that we have joined the channel, and we will see logged (from ws.OnMessage above) any messages from other users.

ws.Send("{\"ref\":\"2\", \"event\":\"new:msg\", \"topic\":\"rooms:lobby\",
\"payload\":{\"user\":\"boo\", \"body\":\"howdy\"}}")

ws.Send("{\"ref\":\"3\", \"event\":\"new:msg\", \"topic\":\"rooms:lobby\",
 \"payload\":{\"user\":\"boo\", \"body\":\"Woohoo!\"}}")

All good things must come to an end, so here’s how we leave the channel.

ws.Send("{\"ref\":\"4\", \"event\":\"phx_leave\", \"topic\":\"rooms:lobby\",
\"payload\":{\"user\":\"boo\"}}")

A bit less metal, please

So we have hacked our way into the party. None of the other members in the channel suspect we’re blasting raw messages. That’s fun, but it won’t win any design prizes. How do we go from bare metal to something that feels nice, idiomatic, and functional.

We could take a peek at Scott Wlaschin’s “13 Ways of Looking at a Turtle” series and try out different APIs (I highly recommend that). Since Elixir is based on the Actor model, I like the idea of “Way #5: an API in front of an agent”. Lets see how we it all maps.

Phoenix channels via an F# Agent-backed API

First, what does our API need to let us do?

  • Connect to a Socket: We will create a socket connection by pointing our websocket library at a Phoenix endpoint (Uri).
  • Join Channel: Phoenix channels are topic based conversations that many users can join simultaneously. From our single socket connection we can join many channels.
  • Receive: After we join a channel we want to be able to lurk and hear what’s being sent on the channel.
  • Send: We also want the ability to send our own messages to other to the channel.
  • Leave Channel: When we are no longer interested in sending or receiving messages on a given topic we should be able to leave that channel without affecting our other channels.
  • Diconnect the socket: When the party comes to an end we don’t want F# to be “that guy” who doesn’t know it’s time to leave.

Message formats

In our REPL adventure above, we saw the structure of the messages that Phoenix expects from us. Let’s get away from our imperative low-level strings and move forward to F# types.

type Message = {
    topic:string    // The string topic or topic:subtopic pair namespace, for example "messages", "messages:123"
    event:string    // The string event name, for example "phx_join"
    payload:string  // The message payload
    ref:string}     // The unique string ref