Phoenix Channels

Elixir Evening Club 1

Alex Rozumii

Why?

How to?

Generate project

$ phx.new elixirclubchat
* creating elixirclubchat/config/config.exs
* creating elixirclubchat/config/dev.exs
* creating elixirclubchat/config/prod.exs
* creating elixirclubchat/config/prod.secret.exs
.......

Generate channel

$ mix phx.gen.channel lobby
* creating lib/elixirclubchat/web/channels/lobby_channel.ex
* creating test/elixirclubchat/web/channels/lobby_channel_test.exs

Add the channel to your `lib/elixirclubchat/web/channels/user_socket.ex` handler, for example:

    channel "lobby:lobby", Elixirclubchat.Web.LobbyChannel

Add simple HTML

<!-- lib/elixirclubchat/web/templates/page/index.html.eex --> 

<div id='message-list' class='row'>
</div>

<div class='row form-group'>
  <div class='col-md-3'>
    <input type='text' id='name' placeholder='Name' />
  </div>
  <div class='col-md-9'>
    <input type='text' id='message' placeholder='Message' />
  </div>
</div>

Add channel JS

import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {}})
socket.connect()

let channel = socket.channel("lobby:lobby", {})
channel.join()

Add posting JS

let messageInput = document.getElementById("message")
let nameInput = document.getElementById("name")

messageInput.addEventListener("keypress", (e) => {
  if (e.keyCode == 13 && messageInput.value != "") {
    channel.push('shout', { 
        name: nameInput.value, 
        text: messageInput.value 
    });
    messageInput.value = ""
  }
})

Add polling JS

let messageList = document.getElementById("message-list")

let renderMessage = (message) => {
  let messageElement = document.createElement("p")
  messageElement.innerHTML = `<b>${message.name}</b> ${message.text}`
  messageList.appendChild(messageElement)
}

channel.on("shout", message => renderMessage(message))

Bonus: Add CSS

#message-list {
  border: 1px solid #777;
  height: 400px;
  padding: 10px;
  overflow: scroll;
  margin-bottom: 50px;
}

That's it!

We need to go deeper

# lib/elixirclubchat/application.ex

defmodule Elixirclubchat.Application do
    ....
    children = [
      supervisor(Elixirclubchat.Web.Endpoint, []),
    ]

    opts = [strategy: :one_for_one, name: Elixirclubchat.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
# lib/elixirclubchat/web/endpoint.ex

defmodule Elixirclubchat.Web.Endpoint do
  use Phoenix.Endpoint, otp_app: :elixirclubchat

  socket "/socket", Elixirclubchat.Web.UserSocket

  plug Plug.Static

  plug Plug.Logger

  plug Elixirclubchat.Web.Router
end
defmodule Elixirclubchat.Web.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel "lobby:lobby", Elixirclubchat.Web.LobbyChannel

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket
  # transport :longpoll, Phoenix.Transports.LongPoll

  def connect(_params, socket) do
    {:ok, socket}
  end

  def id(_socket), do: nil
end
defmodule Elixirclubchat.Web.LobbyChannel do
  use Elixirclubchat.Web, :channel

  def join("lobby:lobby", payload, socket) do
    if authorized?(payload) do
      {:ok, socket}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  # It is also common to receive messages from the client and
  # broadcast to everyone in the current topic (lobby:lobby).
  def handle_in("shout", payload, socket) do
    broadcast socket, "shout", payload
    {:noreply, socket}
  end

  defp authorized?(_payload) do
    true
  end
end

Cowboy

# Is called when the websocket connection is initiated
defcallback init

# Is called to handle messages from the client
defcallback websocket_handle

# Is called to handle Erlang/Elixir messages
defcallback websocket_info

# Is called when the socket connection terminates
defcallback websocket_terminate  

Transport

  # Phoenix.Socket.Transport

  ## The transport behaviour

  The transport requires one function:

    * `default_config/0` - returns the default transport configuration
      to be merged when the transport is declared in the socket module

(App) Socket

# lib/elixirclubchat/web/channels/user_socket.ex

defmodule Elixirclubchat.Web.UserSocket do
  use Phoenix.Socket

  ## Channels
  # channel "room:*", Elixirclubchat.Web.RoomChannel
  channel "lobby:lobby", Elixirclubchat.Web.LobbyChannel

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket
  # transport :longpoll, Phoenix.Transports.LongPoll
  def channel do
    quote do
      use Phoenix.Channel
    end
  end
  # deps/phoenix/lib/phoenix/channel.ex

  @doc """
  Broadcast an event to all subscribers of the socket topic.

  """

  def broadcast(socket, event, message) do
    %{pubsub_server: pubsub_server, topic: topic} = assert_joined!(socket)
    Server.broadcast pubsub_server, topic, event, message
  end
  # deps/phoenix/lib/phoenix/channel/server.ex

  @doc """
  Broadcasts on the given pubsub server
  """

  def broadcast!(pubsub_server, topic, event, payload) do
    PubSub.broadcast! pubsub_server, topic, %Broadcast{
      topic: topic,
      event: event,
      payload: payload
    }
  end

PubSub

# Configures the endpoint
config :elixirclubchat, Elixirclubchat.Web.Endpoint,
  pubsub: [name: Elixirclubchat.PubSub,
           adapter: Phoenix.PubSub.PG2]

PubSub

  def init({server_name, pool_size}) do
    pg2_group = pg2_namespace(server_name)
    :ok = :pg2.create(pg2_group)
    :ok = :pg2.join(pg2_group, self())

    {:ok, %{name: server_name, pool_size: pool_size}}
  end

  defp get_members(server_name) do
    :pg2.get_members(pg2_namespace(server_name))
  end

PubSub

  defp do_broadcast(pids, fastlane, server_name, 
    pool_size, from_pid, topic, msg) do

    Enum.each(pids, fn
      pid when is_pid(pid) and node(pid) == node() ->
        Local.broadcast(fastlane, server_name, 
            pool_size, from_pid, topic, msg)
      .....
    end)
    :ok
  end

PubSub

  defp do_broadcast(nil, pubsub_server, shard, 
       from, topic, msg) do

    pubsub_server
    |> subscribers_with_fastlanes(topic, shard)
    |> Enum.each(fn
      {pid, _} when pid == from -> :noop
      {pid, _} -> send(pid, msg)
    end)
  end

RePG2

Registry: Elixir 1.4

Thanks!

Ask me some questions!

Made with Slides.com