Phoenix v1.2.0-rc.1 Phoenix.Channel behaviour

Defines a Phoenix Channel.

Channels provide a means for bidirectional communication from clients that integrate with the Phoenix.PubSub layer for soft-realtime functionality.

Topics & Callbacks

Everytime you join a channel, you need to choose which particular topic you want to listen to. The topic is just an identifier, but by convention it is often made of two parts: "topic:subtopic". Using the "topic:subtopic" approach pairs nicely with the Phoenix.Socket.channel/2 allowing you to match on all topics starting with a given prefix:

channel "room:*", MyApp.RoomChannel

Any topic coming into the router with the "room:" prefix would dispatch to MyApp.RoomChannel in the above example. Topics can also be pattern matched in your channels’ join/3 callback to pluck out the scoped pattern:

# handles the special `"lobby"` subtopic
def join("room:lobby", _auth_message, socket) do
  {:ok, socket}
end

# handles any other subtopic as the room ID, for example `"room:12"`, `"room:34"`
def join("room:" <> room_id, auth_message, socket) do
  {:ok, socket}
end

Authorization

Clients must join a channel to send and receive PubSub events on that channel. Your channels must implement a join/3 callback that authorizes the socket for the given topic. For example, you could check if the user is allowed to join that particular room.

To authorize a socket in join/3, return {:ok, socket}. To refuse authorization in join/3, return {:error, reply}.

Incoming Events

After a client has successfully joined a channel, incoming events from the client are routed through the channel’s handle_in/3 callbacks. Within these callbacks, you can perform any action. Typically you’ll either forward a message to all listeners with broadcast!/3, or push a message directly down the socket with push/3. Incoming callbacks must return the socket to maintain ephemeral state.

Here’s an example of receiving an incoming "new_msg" event from one client, and broadcasting the message to all topic subscribers for this socket.

def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
  broadcast! socket, "new_msg", %{uid: uid, body: body}
  {:noreply, socket}
end

You can also push a message directly down the socket:

# client asks for their current rank, push sent directly as a new event.
def handle_in("current_rank", socket) do
  push socket, "current_rank", %{val: Game.get_rank(socket.assigns[:user])}
  {:noreply, socket}
end

Replies

In addition to pushing messages out when you receive a handle_in event, you can also reply directly to a client event for request/response style messaging. This is useful when a client must know the result of an operation or to simply ack messages.

For example, imagine creating a resource and replying with the created record:

def handle_in("create:post", attrs, socket) do
  changeset = Post.changeset(%Post{}, attrs)

  if changeset.valid? do
    Repo.insert!(changeset)
    {:reply, {:ok, changeset}, socket}
  else
    {:reply,{:error, MyApp.ChangesetView.render("errors.json",
      %{changeset: changeset}), socket}
  end
end

Alternatively, you may just want to ack the status of the operation:

def handle_in("create:post", attrs, socket) do
  changeset = Post.changeset(%Post{}, attrs)

  if changeset.valid? do
    Repo.insert!(changeset)
    {:reply, :ok, socket}
  else
    {:reply, :error, socket}
  end
end

Intercepting Outgoing Events

When an event is broadcasted with broadcast/3, each channel subscriber can choose to intercept the event and have their handle_out/3 callback triggered. This allows the event’s payload to be customized on a socket by socket basis to append extra information, or conditionally filter the message from being delivered. If the event is not intercepted with Phoenix.Channel.intercept/1, then the message is pushed directly to the client:

intercept ["new_msg", "user_joined"]

# for every socket subscribing to this topic, append an `is_editable`
# value for client metadata.
def handle_out("new_msg", msg, socket) do
  push socket, "new_msg", Map.merge(msg,
    %{is_editable: User.can_edit_message?(socket.assigns[:user], msg)}
  )
  {:noreply, socket}
end

# do not send broadcasted `"user_joined"` events if this socket's user
# is ignoring the user who joined.
def handle_out("user_joined", msg, socket) do
  unless User.ignoring?(socket.assigns[:user], msg.user_id) do
    push socket, "user_joined", msg
  end
  {:noreply, socket}
end

Broadcasting to an external topic

In some cases, you will want to broadcast messages without the context of a socket. This could be for broadcasting from within your channel to an external topic, or broadcasting from elsewhere in your application like a controller or another process. Such can be done via your endpoint:

# within channel
def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
  ...
  broadcast_from! socket, "new_msg", %{uid: uid, body: body}
  MyApp.Endpoint.broadcast_from! self(), "room:superadmin",
    "new_msg", %{uid: uid, body: body}
  {:noreply, socket}
end

# within controller
def create(conn, params) do
  ...
  MyApp.Endpoint.broadcast! "room:" <> rid, "new_msg", %{uid: uid, body: body}
  MyApp.Endpoint.broadcast! "room:superadmin", "new_msg", %{uid: uid, body: body}
  redirect conn, to: "/"
end

Terminate

On termination, the channel callback terminate/2 will be invoked with the error reason and the socket.

If we are terminating because the client left, the reason will be {:shutdown, :left}. Similarly, if we are terminating because the client connection was closed, the reason will be {:shutdown, :closed}.

If any of the callbacks return a :stop tuple, it will also trigger terminate with the reason given in the tuple.

terminate/2, however, won’t be invoked in case of errors nor in case of exits. This is the same behaviour as you find in Elixir abstractions like GenServer and others. Typically speaking, if you want to clean something up, it is better to monitor your channel process and do the clean up from another process. Similar to GenServer, it would also be possible :trap_exit to guarantee that terminate/2 is invoked. This practice is not encouraged though.

Summary

Functions

Broadcast an event to all subscribers of the socket topic

Same as broadcast/3 but raises if broadcast fails

Broadcast event from pid to all subscribers of the socket topic

Same as broadcast_from/3 but raises if broadcast fails

Sends event to the socket

Replies asynchronously to a socket push

Generates a socket_ref for an async reply

Subscribes the socket to an external topic

Unsubscribes the socket from an external topic

Macros

Defines which Channel events to intercept for handle_out/3 callbacks

Types

reply :: status :: atom | {status :: atom, response :: map}
socket_ref :: {transport_pid :: Pid, serializer :: Module.t, topic :: binary, ref :: binary}

Functions

broadcast(socket, event, message)

Broadcast an event to all subscribers of the socket topic.

The event’s message must be a serializable map.

Examples

iex> broadcast socket, "new_message", %{id: 1, content: "hello"}
:ok
broadcast!(socket, event, message)

Same as broadcast/3 but raises if broadcast fails.

broadcast_from(socket, event, message)

Broadcast event from pid to all subscribers of the socket topic.

The channel that owns the socket will not receive the published message. The event’s message must be a serializable map.

Examples

iex> broadcast_from socket, "new_message", %{id: 1, content: "hello"}
:ok
broadcast_from!(socket, event, message)

Same as broadcast_from/3 but raises if broadcast fails.

push(socket, event, message)

Sends event to the socket.

The event’s message must be a serializable map.

Examples

iex> push socket, "new_message", %{id: 1, content: "hello"}
:ok
reply(arg1, arg2)

Specs

reply(socket_ref, reply) :: :ok

Replies asynchronously to a socket push.

Useful when you need to reply to a push that can’t otherwise be handled using the {:reply, {status, payload}, socket} return from your handle_in callbacks. reply/3 will be used in the rare cases you need to perform work in another process and reply when finished by generating a reference to the push with socket_ref/1.

Note: In such cases, a socket_ref should be generated and passed to the external process, so the socket itself is not leaked outside the channel. The socket holds information such as assigns and transport configuration, so it’s important to not copy this information outside of the channel that owns it.

Examples

def handle_in("work", payload, socket) do
  Worker.perform(payload, socket_ref(socket))
  {:noreply, socket}
end

def handle_info({:work_complete, result, ref}, socket) do
  reply ref, {:ok, result}
  {:noreply, socket}
end
socket_ref(socket)

Specs

socket_ref(Phoenix.Socket.t) :: socket_ref

Generates a socket_ref for an async reply.

See reply/2 for example usage.

subscribe(socket, topic)

Subscribes the socket to an external topic.

Useful for circumstances you need to programmatically subscribe a socket to external topics in addition to the the internal socket.topic. For example, imagine you have a bidding system where a remote client dynamically sets preferences on products they want to receiving bidding notifications on. Instead of requiring a unique channel process and topic per preference, a more efficient and simple approach would be to subscribe a single socket to relevant notifications.

Examples

def NotificationChannel do
  use Phoenix.Channel

  def join("notifications:" <> user_id, %{"ids" => ids}, socket) do
    topics = for product_id <- ids, do: "products:#{product_id}"

    {:ok, socket
          |> assign(:topics, [])
          |> put_new_topics(topics)}
  end

  def handle_in("watch", %{"product_id" => id}, socket) do
    {:reply, :ok, put_new_topics(socket, ["products:#{id}"])}
  end

  def handle_in("unwatch", %{"product_id" => id}, socket) do
    {:reply, :ok, unsubscribe(socket, "products:#{id}")}
  end

  defp put_new_topics(socket, topics) do
    Enum.reduce(topics, socket, fn topic, acc ->
      if topic in acc.assigns.topics do
        acc
      else
        :ok = subscribe(acc, topic)
        assign(acc, :topics, [topic | topics])
      end
    end)
  end
end

Note: Like Phoenix.PubSub.subscribe, the caller must be responsible for preventing duplicate subscriptions. After calling Phoenix.Channel.subscribe/2, the same flow applies to normal channel messages. By default, the messages are relayed directly to the client, but the events can be intercepted with intercept/1 causing handle_out/3 callbacks to invoked.

unsubscribe(socket, topic)

Unsubscribes the socket from an external topic.

Examples

iex> unsubscribe(socket, "another:topic")
:ok

Macros

intercept(events)

Defines which Channel events to intercept for handle_out/3 callbacks.

By default, broadcasted events are pushed directly to the client, but intercepting events gives your channel a chance to customize the event for the client to append extra information or filter the message from being delivered.

Note: intercepting events can introduce significantly more overhead if a large number of subscribers must customize a message since the broadcast will be encoded N times instead of a single shared encoding across all subscribers.

Examples

intercept ["new_msg"]

def handle_out("new_msg", payload, socket) do
  push socket, "new_msg", Map.merge(payload,
    is_editable: User.can_edit_message?(socket.assigns[:user], payload)
  )
  {:noreply, socket}
end

handle_out/3 callbacks must return one of:

{:noreply, Socket.t} |
{:stop, reason :: term, Socket.t}

Callbacks

code_change(old_vsn, arg1, extra)

Specs

code_change(old_vsn, Phoenix.Socket.t, extra :: term) ::
  {:ok, Phoenix.Socket.t} |
  {:error, reason :: term} when old_vsn: term | {:down, term}
handle_in(event, msg, arg2)

Specs

handle_in(event :: String.t, msg :: map, Phoenix.Socket.t) ::
  {:noreply, Phoenix.Socket.t} |
  {:reply, reply, Phoenix.Socket.t} |
  {:stop, reason :: term, Phoenix.Socket.t} |
  {:stop, reason :: term, reply, Phoenix.Socket.t}
handle_info(term, arg1)

Specs

handle_info(term, Phoenix.Socket.t) ::
  {:noreply, Phoenix.Socket.t} |
  {:stop, reason :: term, Phoenix.Socket.t}
join(topic, auth_msg, arg2)

Specs

join(topic :: binary, auth_msg :: map, Phoenix.Socket.t) ::
  {:ok, Phoenix.Socket.t} |
  {:ok, map, Phoenix.Socket.t} |
  {:error, map}
terminate(msg, arg1)

Specs

terminate(msg :: map, Phoenix.Socket.t) ::
  {:shutdown, :left | :closed} |
  term