proto_channel

Typed Protobuf layer over Phoenix.Channel.

Two independently usable pieces that compose:

Goals

Installation

def deps do
  [
    {:proto_channel, "~> 0.1.0"}
  ]
end

Docs: https://hexdocs.pm/proto_channel.

Channel usage

defmodule MyAppWeb.MyChannel do
  use ProtoChannel

  alias MyApp.{Request, Response, Notice}

  proto_message "ping", request: Request, reply: Response
  proto_push "notice", Notice
  proto_broadcast "notice", Notice

  @impl Phoenix.Channel
  def join("room:" <> _, _payload, socket), do: {:ok, socket}

  @impl ProtoChannel
  def handle_proto("ping", %Request{} = req, socket) do
    push(socket, "notice", %Notice{text: req.text})
    broadcast(socket, "notice", %Notice{text: req.text})
    {:reply, {:ok, %Response{text: req.text}}, socket}
  end
end

proto_message generates the handle_in/3 clause that decodes the inbound bytes into a %Request{}, dispatches to handle_proto/3, and encodes the reply back to bytes. proto_push and proto_broadcast generate typed wrappers around Phoenix.Channel.push/3, broadcast/3, broadcast!/3, broadcast_from/3, and broadcast_from!/3 — each declared event accepts only its declared struct. To bypass the wrappers, call Phoenix.Channel.push/3 etc. directly.

handle_proto/3 callback

@callback handle_proto(
            event :: String.t(),
            request :: struct(),
            socket :: Phoenix.Socket.t()
          ) :: result()

Supported return shapes:

Compile-time validation

Duplicate event names within the same macro family raise ArgumentError. Every referenced module must use Protobuf (verified via __message_props__/0), so typos and accidentally pointing at a plain struct fail at compile time rather than at runtime over the wire.

Serializer usage

socket "/socket", MyAppWeb.UserSocket,
  websocket: [serializer: [{ProtoChannel.Serializer, "~> 2.0.0"}]],
  longpoll: false

Every frame is wrapped in a protobuf Envelope (defined in priv/proto/wire.proto). Payloads must be {:binary, bytes}; empty maps are tolerated for Phoenix's join acks.