proto_channel
Copy MarkdownTyped Protobuf layer over Phoenix.Channel.
Two independently usable pieces that compose:
ProtoChannel— a macro that lets a channel declareevent ⇄ Protobuf messagepairs at compile time, so handlers exchange typed structs instead of raw{:binary, bytes}payloads.ProtoChannel.Serializer— aPhoenix.Socket.Serializerthat frames every socket frame as a protobufEnvelope(kinds:PUSH,REPLY,BROADCAST).
Goals
- A typed-struct boundary inside channels: pattern-match request and reply
structs at the edge, get compile-time field-name safety, and let dialyzer
type-check
handle_proto/3end-to-end. - An all-binary wire format — no JSON, no per-event ad-hoc encoding.
- Stay small: each piece is opt-in and can be paired with hand-written counterparts.
Installation
def deps do
[
{:proto_channel, "~> 0.1.0"}
]
endDocs: 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
endproto_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:
{:reply, {:ok | :error, %Reply{}}, socket}{:noreply, socket}/{:noreply, socket, timeout | :hibernate}{:stop, reason, socket}/{:stop, reason, {:ok | :error, %Reply{}}, socket}
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: falseEvery 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.