Skip to main content

README.md

# 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.1"}
  ]
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.