# proto_channel
Typed Protobuf layer over `Phoenix.Channel`.
Two independently usable pieces that compose:
- **`ProtoChannel`** — a macro that lets a channel declare `event ⇄ Protobuf
message` pairs at compile time, so handlers exchange typed structs instead of
raw `{:binary, bytes}` payloads.
- **`ProtoChannel.Serializer`** — a `Phoenix.Socket.Serializer` that frames
every socket frame as a protobuf `Envelope` (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/3` end-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
```elixir
def deps do
[
{:proto_channel, "~> 0.1.0"}
]
end
```
Docs: [https://hexdocs.pm/proto_channel](https://hexdocs.pm/proto_channel).
## Channel usage
```elixir
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
```elixir
@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
```elixir
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.