The Phoenix channel protocol NervesHub speaks, as a pure state machine.
No processes, no timers, no socket. Every function takes a state and returns a new state plus a list of actions for the caller to perform:
{send, Binary} %% write this to the socket
{event, Term} %% tell the application this happened
Keeping it pure is what lets the protocol be tested against a real NervesHub from a desktop, over any WebSocket client, before any of it runs on a device.
Phoenix's v2 serializer is a five element array rather than an object:
[JoinRef, Ref, Topic, Event, Payload]
Two details are easy to get wrong and fail quietly:
<<"device">>, unqualified. NervesHub's
serializer rewrites it to device:<id> on the way in.<<"phoenix">> with a null join reference, not to
the device topic.connected/1 must be called
on every connection, including reconnections, and it starts a fresh join with
a new join reference. A client that reconnects the socket without rejoining
looks healthy and silently stops receiving updates.
action() = {send, binary()} | {event, term()}
abstract datatype: state()
| add_topic/3 | Register another topic to join on this socket. |
| connected/1 | Called on every connection, including reconnections. |
| disconnected/1 | The socket went away. |
| handle_text/2 | Handle a text frame from the socket. |
| heartbeat/1 | A heartbeat frame. |
| joined/1 | Whether the join has been acknowledged. |
| joined/2 | Whether a named topic's join has been acknowledged. |
| new/1 | A channel that has not connected yet. |
| push/3 | Push an event to the device channel. |
| push/4 | Push an event to a named topic. |
| topics/1 | Every topic registered, in join order. |
Register another topic to join on this socket.
One socket carries several channels — the device topic, and console when
the application wants one. Each is joined separately and has its own join
reference, but they share the socket's reference counter: Phoenix correlates
a reply by reference, and two channels numbering from 1 apiece would produce
two different frames with the same reference.
Called on every connection, including reconnections.
The socket went away.
Only the join is lost. The parameters and the reference counter are kept: a fresh channel would restart references at 1, and a late reply from the old connection could then be mistaken for a reply to the new join.Handle a text frame from the socket.
A heartbeat frame. NervesHub closes a socket that stops sending these.
joined(State::state()) -> boolean()
Whether the join has been acknowledged.
joined(Topic::binary(), State::state()) -> boolean()
Whether a named topic's join has been acknowledged.
new(Params::map()) -> state()
A channel that has not connected yet.
Params is the join payload: what the device reports about itself.
Push an event to the device channel.
Push an event to a named topic.
topics(X1::state()) -> [binary()]
Every topic registered, in join order.
Generated by EDoc