How to use MessagePack with LiveView instead of JSON

In a quest to optimize the amount of data sent between the server and client I recently decided to try to use MessagePack instead of JSON. The payloads are about 75% of the size in JSON, which is nice. I have not noticed any performance issues related to encoding and decoding.

There is a StackOverflow answer that I used for the basis for this post and @chrismccord helped me with a crucial bit of information. To get LiveUploads to work, a bit of extra code is necessary.

If you spot any improvements or errors, please tell me and I will update the code.

In assets/app.js you import the MessagePack client:

import {Encoder, Decoder} from "@msgpack/msgpack";
const encoder = new Encoder({ ignoreUndefined: true });
const decoder = new Decoder();

....
const liveSocket = new LiveSocket("/live", Socket, {
  // hooks  and params stuff
  encode: (payload, callback) => {
    // We convert binary payloads to a Uint8Array, so MessagePack will send pack it.
    if (payload.payload instanceof ArrayBuffer) {
      payload.payload = new Uint8Array(payload.payload)
    }
    callback(encoder.encode(payload));
  },
  decode: (payload, callback) => callback(decoder.decode(payload)),
})

The serializer looks like this :

defmodule MyAppWeb.MsgpackSerializer do
  @behaviour Phoenix.Socket.Serializer

  alias Phoenix.Socket.Reply
  alias Phoenix.Socket.Message
  alias Phoenix.Socket.Broadcast

  def fastlane!(%Broadcast{} = msg) do
    msg = %Message{topic: msg.topic, event: msg.event, payload: msg.payload}

    {:socket_push, :binary, encode_to_binary(msg)}
  end

  def encode!(%Reply{} = reply) do
    msg = %Message{
      topic: reply.topic,
      event: "phx_reply",
      ref: reply.ref,
      payload: %{status: reply.status, response: reply.payload}
    }

    {:socket_push, :binary, encode_to_binary(msg)}
  end

  def encode!(%Message{} = msg) do
    {:socket_push, :binary, encode_to_binary(msg)}
  end

  defp encode_to_binary(msg) do
    msg |> Map.from_struct() |> Msgpax.pack!()
  end

  def decode!(message, _opts) do
    decoded = Msgpax.unpack!(mesage)
    payload = Map.get(decoded, "payload")

    # LiveUpload sends us binary, but MessagePack is always binary so we cannot use the opcode in the opts.
    if is_binary(payload) and not String.valid?(payload) do
      Map.put(decoded, "payload", {:binary, payload})
    else
      decoded
    end
    |> Phoenix.Socket.Message.from_map!()
  end
end

And then in your Enpoint module use the serializer in the socket:

socket("/live", Phoenix.LiveView.Socket,
  websocket: [
    serializer: [{MyAppWeb.MsgpackSerializer, "2.0.0"}]
  ]
)
12 Likes

Update, the check in serializer does not work when an array of zeroes is sent. It is probably better to check the “event” key in the message directly.

So the decode!/2 function in the serializer should probably use the following check:

if decoded["event"] == "chunk" do
1 Like