ProtoChannel behaviour (ProtoChannel v0.1.1)

Copy Markdown

A typed Protobuf layer over Phoenix.Channel.

A use ProtoChannel channel declares its event ⇄ Protobuf-message pairs at compile time and exchanges typed structs with handlers instead of raw {:binary, bytes} payloads. Pattern-matching the structs at the boundary gives compile-time field-name safety, and the handle_proto/3 spec gives dialyzer full value-type checking.

Example

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

What the macros generate

  • proto_message/2 — one handle_in/3 clause per declared event that decodes the inbound bytes into the request struct, dispatches to handle_proto/3, and encodes the reply struct back to bytes.
  • proto_push/2 and proto_broadcast/2 — 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; anything else is a function-clause mismatch at the call site.

The unqualified push/3, broadcast/3, ... names inside your channel resolve to the generated wrappers — the macro imports Phoenix.Channel with those five names excluded. To bypass the wrappers, call Phoenix.Channel.push/3 etc. directly.

Compile-time validation

  • Duplicate event names within the same macro family (proto_message, proto_push, or proto_broadcast) raise ArgumentError at compile time.
  • Every referenced module must use Protobuf — the macro checks for __message_props__/0 and raises if missing, so typos and stray plain structs are caught up-front.

Wire format

The macro only produces {:binary, bytes} payloads. To frame those over the socket as protobuf, pair this with ProtoChannel.Serializer.

Summary

Types

Return shapes accepted by handle_proto/3.

A reply payload from handle_proto/3: the status atom plus a Protobuf struct that the macro will encode to bytes.

Callbacks

Handles a decoded request struct for a proto_message/2-declared event.

Functions

Generates typed wrappers for an outbound broadcast event.

Declares an RPC-style inbound event.

Generates a typed push/3 wrapper for an outbound event.

Types

handle_proto_result()

@type handle_proto_result() ::
  {:reply, reply(), Phoenix.Socket.t()}
  | {:noreply, Phoenix.Socket.t()}
  | {:noreply, Phoenix.Socket.t(), timeout() | :hibernate}
  | {:stop, reason :: term(), Phoenix.Socket.t()}
  | {:stop, reason :: term(), reply(), Phoenix.Socket.t()}

Return shapes accepted by handle_proto/3.

The reply form mirrors Phoenix.Channel.handle_in/3, but the reply struct is encoded to {:binary, bytes} by the macro before being handed back to Phoenix. :noreply and bare :stop variants pass through unchanged.

reply()

@type reply() :: {:ok | :error, struct()}

A reply payload from handle_proto/3: the status atom plus a Protobuf struct that the macro will encode to bytes.

Callbacks

handle_proto(event, request, socket)

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

Handles a decoded request struct for a proto_message/2-declared event.

Invoked from the generated handle_in/3 clause after the inbound bytes have been decoded into the declared request struct. The returned reply struct (if any) is encoded back to bytes before being handed to Phoenix.

See handle_proto_result/0 for the supported return shapes.

Functions

proto_broadcast(event, module)

(macro)

Generates typed wrappers for an outbound broadcast event.

Generates clauses for broadcast/3, broadcast!/3, broadcast_from/3, and broadcast_from!/3. Each encodes the struct and forwards to the matching Phoenix.Channel function as {:binary, bytes}.

Example

proto_broadcast "notice", MyApp.Notice
# ...
broadcast(socket, "notice", %MyApp.Notice{text: "hi"})

proto_message(event, opts)

(macro)

Declares an RPC-style inbound event.

The channel receives event as a {:binary, bytes} payload, decodes the bytes into a request struct, dispatches to handle_proto/3, and encodes the returned reply struct back to bytes.

Options

  • :request — module returned by use Protobuf; the request struct type.
  • :reply — module returned by use Protobuf; the reply struct type.

Both keys are required. Each module is resolved and checked for __message_props__/0 at compile time.

Example

proto_message "ping", request: MyApp.Ping, reply: MyApp.Pong

proto_push(event, module)

(macro)

Generates a typed push/3 wrapper for an outbound event.

Inside the channel, push(socket, event, %module{} = msg) encodes msg and forwards to Phoenix.Channel.push/3 as {:binary, bytes}. Other event names still resolve to the unwrapped Phoenix.Channel.push/3.

Example

proto_push "notice", MyApp.Notice
# ...
push(socket, "notice", %MyApp.Notice{text: "hi"})