Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Real-Time applications in Phoenix

Real-Time applications in Phoenix

A talk about building real time applications in phoenix and elixir with examples for both the client side and server side.

Chris Keathley

January 08, 2016
Tweet

More Decks by Chris Keathley

Other Decks in Programming

Transcript

  1. Types iex> 1 # integer iex> 0x1F # integer iex>

    1.0 # float iex> true # boolean iex> :atom # atom / symbol iex> "elixir" # string iex> [1, 2, 3] # list iex> {1, 2, 3} # tuple iex> %{:a => 1, 2 => :b} # map
  2. Pattern Matching iex(4)> %{a: a, b: b} = %{a: "hello",

    b: "world"} %{a: "hello", b: "world"} iex(5)> a "hello" iex(6)> b "world" iex(7)>
  3. Processes iex(9)> pid = spawn fn -> 1 + 2

    end #PID<0.68.0> iex(10)> self() #PID<0.57.0>
  4. Processes iex> parent = self() #PID<0.41.0> iex> spawn fn ->

    send(parent, {:hello, self()}) end #PID<0.48.0> iex> receive do ...> {:hello, pid} -> "Got hello from #{inspect pid}" ...> end "Got hello from #PID<0.48.0>"
  5. Controllers <!DOCTYPE html> <html lang="en"> <head> <title>Conflicted</title> </head> <body> <%=

    render Conflicted.PageView, "index.html" %> <%= if Mix.env == :dev do %> <script src="http://localhost:4001/bundle.js"></script> <% else %> <script src="<%= static_path(@conn, "/js/bundle.js") %>"></script> <% end %> </body> </html>
  6. Sockets defmodule Conflicted.UserSocket do use Phoenix.Socket ## Channels channel "tweets:*",

    Conflicted.TweetChannel ## 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
  7. defmodule Conflicted.TweetChannel do use Phoenix.Channel def join("tweets:stream", _message, socket) do

    tweets = Conflicted.Repo.all(Conflicted.Tweet) {:ok, tweets, socket} end def join("tweets:" <> _private_room_id, _params, _socket) do {:error, %{reason: "unauthorized"}} end # … end
  8. def join("tweets:stream", _message, socket) do When a client “joins” the

    channel Pattern matching on the channel name The client socket connection
  9. def join("tweets:stream", _message, socket) do tweets = Repo.all(Tweet) {:ok, tweets,

    socket} end Retrieves all “tweets” from the database
  10. def join("tweets:stream", _message, socket) do tweets = Repo.all(Tweet) {:ok, tweets,

    socket} end Retrieves all “tweets” from the database Return a tuple including the socket
  11. defmodule Conflicted.TweetChannel do use Phoenix.Channel def join("tweets:stream", _message, socket) do

    tweets = Repo.all(Tweet) {:ok, tweets, socket} end def join("tweets:" <> _private_room_id, _params, _socket) do {:error, %{reason: "unauthorized"}} end # … end Return an error if the client tries to join any other channel
  12. defmodule Conflicted.Tweet do use Conflicted.Web, :model schema "tweets" do field

    :author, :string field :content, :string field :source_url, :string field :image_url, :string field :likes, :integer, default: 0 timestamps end end
  13. query = from t in Tweet, select: [t.text, t.likes], where:

    [t.author == “@chriskeathley”], order_by: [desc: t.inserted_at], limit: 30 tweets = Conflicted.Repo.all(query)
  14. defmodule Conflicted.TweetChannel do use Phoenix.Channel def join("tweets:stream", _message, socket) do

    tweets = Repo.all(Tweet) {:ok, tweets, socket} end def join("tweets:" <> _private_room_id, _params, _socket) do {:error, %{reason: "unauthorized"}} end # … end
  15. import { Socket } from 'phoenix-socket' let socket = new

    Socket("/socket") socket.connect() let channel = socket.channel("tweets:stream", {}) channel.join() .receive("ok", resp => { store.dispatch(setState(resp)) }) .receive("error", resp => { console.log("Unable to join", resp); })
  16. defmodule Conflicted do use Application def start(_type, _args) do import

    Supervisor.Spec, warn: false children = [ supervisor(Conflicted.Endpoint, []), worker(Conflicted.Repo, []), worker(Task, [fn -> stream_task("elixirfriends") end]) ] opts = [strategy: :one_for_one, name: Conflicted.Supervisor] Supervisor.start_link(children, opts) end defp stream_task(term) do Conflicted.TweetStreamer.stream(term) |> Enum.to_list end end
  17. defmodule Conflicted.TweetStreamer do alias Conflicted.Repo import Ecto.Query, only: [from: 2]

    def stream(search_term) do ExTwitter.stream_filter(track: search_term) |> Stream.filter(&has_images?/1) |> Stream.filter(&new_tweet?/1) |> Stream.map(&store_tweet/1) end defp store_tweet(raw_tweet) do tweet = raw_tweet |> new_tweet |> Repo.insert! |> IO.inspect Conflicted.Endpoint.broadcast!("tweets:stream", "state", tweet) end # … end
  18. def stream(search_term) do ExTwitter.stream_filter(track: search_term) |> Stream.filter(&has_images?/1) |> Stream.filter(&new_tweet?/1) |>

    Stream.map(&store_tweet/1) end defp store_tweet(raw_tweet) do tweet = raw_tweet |> new_tweet |> Repo.insert! |> IO.inspect Conflicted.Endpoint.broadcast!("tweets:stream", "state", tweet) end
  19. export function setState(state) { return { type: SET_STATE, state }

    } export function likeTweet(id) { return { meta: {remote: true}, type: LIKE_TWEET, id } }
  20. export default channel => store => next => action =>

    { if (action.meta && action.meta.remote) channel.push('action', action) return next(action) }
  21. import { Map } from 'immutable' import { SET_STATE, LIKE_TWEET

    } from '../actions' const setState = (state, newState) => { let tweets = newState.map((tweet) => [tweet.id, tweet]) return state.merge(tweets) } const likeTweet = (state, id) => { return state.updateIn([id, 'likes'], val => val + 1) } export default function tweetsReducer(state=Map({}), action) { switch(action.type) { case SET_STATE: return setState(state, action.state) case LIKE_TWEET: return likeTweet(state, action.id) default: return state } }
  22. def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do

    changeset = Tweet |> Repo.get!(id) |> Tweet.changeset(%{"likes" => tweet.likes+1}) case Repo.update(changeset) do {:ok, tweet} -> broadcast!(socket, "state", tweet) {:error, changeset} -> IO.puts "SOMETHING WENT WRONG" IO.inspect changeset end {:noreply, socket} end
  23. Pattern match on the action def handle_in("action", %{"type" => "LIKE_TWEET",

    "id" => id}, socket) do changeset = Tweet |> Repo.get!(id) |> Tweet.changeset(%{"likes" => tweet.likes+1}) case Repo.update(changeset) do {:ok, tweet} -> broadcast!(socket, "state", tweet) {:error, changeset} -> IO.puts "SOMETHING WENT WRONG" IO.inspect changeset end {:noreply, socket} end
  24. Increment the likes by 1 Pattern match on the action

    def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do changeset = Tweet |> Repo.get!(id) |> Tweet.changeset(%{"likes" => tweet.likes+1}) case Repo.update(changeset) do {:ok, tweet} -> broadcast!(socket, "state", tweet) {:error, changeset} -> IO.puts "SOMETHING WENT WRONG" IO.inspect changeset end {:noreply, socket} end
  25. Increment the likes by 1 Send a message back to

    all of the clients Pattern match on the action def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do changeset = Tweet |> Repo.get!(id) |> Tweet.changeset(%{"likes" => tweet.likes+1}) case Repo.update(changeset) do {:ok, tweet} -> broadcast!(socket, "state", tweet) {:error, changeset} -> IO.puts "SOMETHING WENT WRONG" IO.inspect changeset end {:noreply, socket} end
  26. defmodule Conflicted.Tweet do use Conflicted.Web, :model schema "tweets" do field

    :author, :string field :content, :string field :source_url, :string field :image_url, :string field :likes, :integer, default: 0 timestamps end @required_fields ~w(author content source_url image_url) @optional_fields ~w(likes) def changeset(model, params \\ []) do model |> cast(params, @required_fields, @optional_fields) |> validate_number(:likes, greater_than_or_equal_to: 0) end end
  27. @required_fields ~w(author content source_url image_url) @optional_fields ~w(likes) def changeset(model, params

    \\ []) do model |> cast(params, @required_fields, @optional_fields) |> validate_number(:likes, greater_than_or_equal_to: 0) end Transform into changeset
  28. @required_fields ~w(author content source_url image_url) @optional_fields ~w(likes) def changeset(model, params

    \\ []) do model |> cast(params, @required_fields, @optional_fields) |> validate_number(:likes, greater_than_or_equal_to: 0) end Transform into changeset Provide some validation
  29. def handle_in("action", %{"type" => "LIKE_TWEET", "id" => id}, socket) do

    changeset = Tweet |> Repo.get!(id) |> Tweet.changeset(%{"likes" => tweet.likes+1}) case Repo.update(changeset) do {:ok, tweet} -> broadcast!(socket, "state", tweet) {:error, changeset} -> IO.puts "SOMETHING WENT WRONG" IO.inspect changeset end {:noreply, socket} end
  30. Create a changeset def handle_in("action", %{"type" => "LIKE_TWEET", "id" =>

    id}, socket) do changeset = Tweet |> Repo.get!(id) |> Tweet.changeset(%{"likes" => tweet.likes+1}) case Repo.update(changeset) do {:ok, tweet} -> broadcast!(socket, "state", tweet) {:error, changeset} -> IO.puts "SOMETHING WENT WRONG" IO.inspect changeset end {:noreply, socket} end Execute the change
  31. Create a changeset def handle_in("action", %{"type" => "LIKE_TWEET", "id" =>

    id}, socket) do changeset = Tweet |> Repo.get!(id) |> Tweet.changeset(%{"likes" => tweet.likes+1}) case Repo.update(changeset) do {:ok, tweet} -> broadcast!(socket, "state", tweet) {:error, changeset} -> IO.puts "SOMETHING WENT WRONG" IO.inspect changeset end {:noreply, socket} end Execute the change Handle the response