$30 off During Our Annual Pro Sale. View Details »

Harnessing the Real-Time Web with Phoenix Channels + Presence

Harnessing the Real-Time Web with Phoenix Channels + Presence

Elixir School's Day 1 workshop at ElixirConf 2019

Sophie DeBenedetto

August 27, 2019
Tweet

More Decks by Sophie DeBenedetto

Other Decks in Programming

Transcript

  1. Harnessing the
    Real-Time Web
    With Phoenix Channels & Presence

    View Slide

  2. Introductions
    2

    View Slide

  3. Hello!
    I’m Sophie
    I’m an engineer and sometimes teacher at The
    Flatiron School where I build education and
    business tools in Elixir and Ruby.
    @sm_debenedetto
    3
    *and this is my
    dog

    View Slide

  4. And...
    I’m Michael
    I’m a lead engineer at RentPath, where we build
    websites to help people find a place to live.
    @michaelstalker
    4
    *and this is my
    family

    View Slide

  5. Elixir School
    A free, open-source Elixir
    curriculum
    Through the hard work of many volunteers
    around the world, we’ve developed and
    translated content covering everything from
    intro topics to deep dives.
    5

    View Slide

  6. Get Involved!
    Help us grow the Elixir School
    community
    Write a lesson, contribute a TIL (or longer!) blog
    post or provide a translation. Open an issue here
    describing your idea or simply open a PR with
    your work to get started.
    6

    View Slide

  7. Today’s Goals
    7

    View Slide

  8. What We’ll Learn
    ○ Part 1:
    ● WebSockets
    ● Intro to Phoenix Channels
    ● Phoenix PubSub
    ○ Part 2:
    ● Phoenix Presence
    ● Complex, Real-Time UI Changes
    8

    View Slide

  9. What We’ll Build
    A real-time ticket estimation tool
    ○ Users can see other users in the “estimation room” in
    real-time
    ○ Users can vote on tickets
    ○ Users can see the winning vote tallied in real-time and move
    on to the next ticket
    9

    View Slide

  10. Pointing Party
    A collaborative ticket estimation tool for your team
    10

    View Slide

  11. 11

    View Slide

  12. User Starts Estimation Round
    When I click “start pointing!”
    Then the first card for estimation
    appears for all users in the room
    And I become the “driving user”
    Features
    Real-Time User Presence
    When a new user joins/leaves
    Then I see the list of present
    users update in the UI
    12

    View Slide

  13. Displaying the Next Card
    When the “driving” user picks a
    clicks the “next card” button
    Then the next card to be
    estimated is displayed for all users
    Features
    Winner is Calculated
    When the last user submits a vote
    Then all users see the winning
    vote
    And the “driving” user can click
    “next card”
    13

    View Slide

  14. Part 1: WebSockets,
    Channels and
    PubSub

    View Slide

  15. Application Starting State
    Log In with a username
    We built some dummy
    authorization flows
    See the static ticket estimation
    page
    Oh no you can’t estimate a ticket!
    15

    View Slide

  16. We Need Real-Time!
    In the current state of our app,
    users cannot collaborate on ticket
    estimation.
    16

    View Slide

  17. What is the Real-Time Web?
    Users receive new information from the server as soon as it is
    available—no request required
    This represents a significant departure from traditional HTTP
    communication
    Made possible by WebSockets
    17

    View Slide

  18. 18
    WebSockets

    View Slide

  19. What are WebSockets?
    A protocol built on top of TCP that allows for bi-directional,
    "full-duplex" communication between the client and the server by
    creating a persistent connection between the two.
    19

    View Slide

  20. 20
    Server
    Client
    Request
    GET https://elixirschool.com
    Response
    Welcome to Elixir School!
    HTTP Protocol

    View Slide

  21. 21
    Server
    Client
    Step 1.
    Request to
    initiate WS
    connection
    Client Client
    GET /
    Host: elixirschool.com
    Upgrade: websocket
    Connection: upgrade

    View Slide

  22. 22
    Server
    Client
    Step 2.
    Open and
    maintain WS
    connections
    Client Client

    View Slide

  23. 23
    We’ll use WebSockets to maintain a persistent, bi-directional and stateful
    connection between our ticket estimation clients and the server with the
    help of Phoenix Channels

    View Slide

  24. Harnessing the
    Real-Time Web
    Part 2: Phoenix Presence

    View Slide

  25. 25
    We Have A Problem

    View Slide

  26. 26
    How can teammates collaborate in a
    super fun pointing party if they can’t
    see who is participating?

    View Slide

  27. 27
    How can we sync and share stateful
    info, like who is present?

    View Slide

  28. Don’t Do This:
    ● Store present user info in the DB
    ● Roll your own user presence data store with
    Agent
    ● Replicate socket state across channels and keep
    it in sync
    ● Leverage Mnesia
    28

    View Slide

  29. Do This
    ● Use Phoenix Presence!
    29

    View Slide

  30. 30
    Phoenix Presence

    View Slide

  31. What is Phoenix Presence?
    The Phoenix.Presence module allows us to:
    ● Register and expose topic-specific info to all of the channel
    processes subscribing to that topic.
    ● Store that info in a decentralized and resilient data store.
    ● Broadcast presence-related events and handle them on the
    front-end with ease.
    31

    View Slide

  32. 32
    Phoenix Presence +
    Distributed Elixir =

    View Slide

  33. Phoenix Presence Uses a CRDT
    A CRDT (Conflict-free Replicated Data Type) backs
    Phoenix Presence. What’s so special about a CRDT?
    Unlike centralized data stores like Redis, Phoenix
    Presence…
    33

    View Slide


  34. …gives you high availability and performance
    because we don't have to send all data through a
    central server. It also gives you resilience to failure
    because the system automatically recovers from
    failures.
    - Chris McCord
    34

    View Slide

  35. 35
    How It Works

    View Slide

  36. 36
    Channel
    topic chats:1
    Step 1. User
    joins the
    channel and
    registers their
    presence
    PubSub
    Client
    “user 1 is present”

    View Slide

  37. 37
    Channel
    topic chats:1
    Step 2.
    User presence
    broadcast to
    subscribers
    Channel
    topic chats:1
    Channel
    topic chats:1
    PubSub
    Client Client Client
    “user 1 is present”

    View Slide

  38. 38
    Channel
    topic chats:1
    Step 3.
    Client handles
    broadcast and
    updates UI
    Channel
    topic chats:1
    Channel
    topic chats:1
    PubSub
    Client Client Client
    “user 1 is present”
    > > >

    View Slide

  39. 39
    A Closer Look

    View Slide

  40. Roadmap
    2.
    Broadcasting
    Presence Events
    3.
    Presence on the
    Front-End
    1.
    Registering User
    Presence
    40

    View Slide

  41. 41
    1. Registering User
    Presence

    View Slide

  42. 42
    Define your app’s
    Presence module

    View Slide

  43. 43
    defmodule PointingPartyWeb.Presence do
    use Phoenix.Presence,
    otp_app: :pointing_party,
    pubsub_server: PointingParty.PubSub
    end

    View Slide

  44. 44
    Add your Presence
    module to the supervision
    tree

    View Slide

  45. 45
    # application.ex
    def start(_type, _args) do
    children = [
    PointingPartyWeb.Endpoint,
    PointingPartyWeb.Presence
    ]
    opts = [
    strategy: :one_for_one,
    name: PointingParty.Supervisor]
    Supervisor.start_link(children, opts)
    end

    View Slide

  46. 46
    User joins a channel

    View Slide

  47. 47
    Track the user presence in
    Presence’s data store

    View Slide

  48. 48
    def join("room:lobby", _payload, socket) do
    send(self(), :after_join)
    {:ok, socket}
    end
    def handle_info(:after_join, socket) do
    username = socket.assigns.username
    {:ok, _} = Presence.track(socket,username, %{})
    {:noreply, socket}
    end

    View Slide

  49. The Presence.Tracker Behavior
    Presence.track/4 is used to register the channel's process as a
    presence for the socket's username, with a map of metadata.
    49

    View Slide

  50. 50
    {
    "sophiemaria" => %{
    metas: [%{phx_ref: “xxxx"}]
    },
    “mstalker" => %{
    metas:[%{ phx_ref: "xxxx"}]
    }
    }

    View Slide

  51. Roadmap
    2.
    Broadcasting
    Presence Events
    3.
    Presence on the
    Front-End
    1.
    Registering User
    Presence
    51

    View Slide

  52. 52
    2. Broadcasting
    Presence Events

    View Slide

  53. 53
    Tracking user presence
    triggers the “presence_diff”
    event

    View Slide

  54. 54
    This event is broadcast to all
    channels for a given topic,
    which are being tracked by
    Presence

    View Slide

  55. Roadmap
    2.
    Broadcasting
    Presence Events
    3.
    Presence on the
    Front-End
    1.
    Registering User
    Presence
    55

    View Slide

  56. 56
    3. Presence Events
    on the Front-End

    View Slide

  57. 57
    Connecting to the Presence
    on the front-end

    View Slide

  58. 58
    import { Presence } from 'phoenix'
    const presence = new Presence(channel)

    View Slide

  59. 59
    presence.onSync(() => {
    // coming soon!
    })

    View Slide

  60. 60
    Fetching the updated list of
    present users

    View Slide

  61. 61
    presence.onSync(() => {
    renderUsers(presence.list(listBy))
    })

    View Slide

  62. 62
    Using a listBy function

    View Slide

  63. 63
    You can give presence.list() a
    listBy function to specify which
    metadata to collect for each user

    View Slide

  64. 64
    const listBy=(username, {metas: [{points}, ..._rest]}) => ({username, points})

    View Slide

  65. 65
    But Wait!

    View Slide

  66. 66
    We have another problem

    View Slide

  67. 67
    How can we display the list of
    already-present users to anyone
    who joins the channel?

    View Slide

  68. 68
    Fetching present users on
    channel join

    View Slide

  69. 69
    Presence.list(socket)

    View Slide

  70. 70
    Pushing present users to the
    newly-joined client

    View Slide

  71. push vs. broadcast
    Broadcasting messages sends messages to all channel processes
    that share a given topic, triggering a callback on each channel’s
    front-end.
    Pushing a message sends that message only that channel’s socket,
    triggering a callback for on that channel’s front-end only.
    71

    View Slide

  72. 72
    users = Presence.list(socket)
    push(socket, "presence_state", users)

    View Slide

  73. 73
    Putting it all together

    View Slide

  74. 74
    # room_channel.ex
    def handle_info(:after_join, socket) do
    users = Presence.list(socket)
    push(socket, "presence_state", users)
    username = socket.assigns.username
    {:ok, _} = Presence.track(socket, username, %{})
    {:noreply, socket}
    end

    View Slide

  75. 75
    But Wait!

    View Slide

  76. 76
    What happens when a user
    leaves the pointing party?

    View Slide

  77. 77
    Do we need to write more
    code?

    View Slide

  78. 78
    Nope!

    View Slide

  79. 79
    What happens to a channel
    process when the user navigates
    away from the page?

    View Slide

  80. 80
    It dies

    View Slide

  81. Phoenix Presence handles user
    leaving for free!
    ● When a user navigates away from the webpage, their channel
    process terminates
    ● Presence knows that one of its tracked processes terminated
    ● Presence updates list of present users and broadcasts the
    “presence_diff” event
    ● Still-alive channel subscribers already know how to handle
    that event via presence.onSync on the front-end
    81

    View Slide

  82. Your Turn!

    View Slide

  83. Feature Roadmap
    2.
    Users see list of
    present users
    3.
    All votes are
    tallied, displayed
    1.
    User initiates
    pointing round
    83

    View Slide

  84. 84

    View Slide

  85. Users see list of present users
    85
    Sync present users on
    the front-end
    ● Use onSync and
    presence.list() to
    display lists of users
    Display existing users for
    new user
    ● In your after_join
    function, fetch the list of
    present users and push
    them down to the client
    Register user presence on
    channel join
    ● Use Presence.track/3
    to register the user’s
    presence

    View Slide

  86. We provided
    86
    RoomChannel starter
    code
    ● Some hints in the
    RoomChannel module
    about where and how to
    leverage Presence
    user.js file
    ● With some JS starter-code
    you can use to update
    page with presence data

    View Slide

  87. Harnessing the
    Real-Time Web
    Part 3: Complex UI Updated with
    Channels and Presence

    View Slide

  88. Your Turn

    View Slide

  89. Feature Roadmap
    2.
    Users see list of
    present users
    3.
    All votes are
    tallied, displayed
    1.
    User initiates
    pointing round
    89

    View Slide

  90. 90

    View Slide

  91. More features!
    91
    Tally All Votes
    ● Once all of the present
    users have voted,
    calculate the winning
    vote
    ● Broadcast and display
    the winning vote
    Update to the next card
    ● Once the winning vote is
    displayed, the “driver” can
    select “new card”
    ● UI updates to show the
    new card for voting
    Users can cast votes
    ● User casts a vote
    ● Store their vote in
    Presence state
    ● Broadcast a message and
    update the UI to indicate a
    given user voted

    View Slide

  92. We provided
    92
    socket.js and user.js
    files
    ● Some JS starter-code
    you can use to update
    the page with the
    winning/tied votes
    ● Some JS starter-code
    you can user to update
    user votes on the page
    VoteCalculator module
    ● A module that contains the
    vote calculation logic.
    The RoomChannel
    ● With some start-code to
    indicate where and how to
    hook into certain events

    View Slide

  93. Before we
    jump in...

    View Slide

  94. 94
    Gotcha: Updating
    presence across
    channels

    View Slide

  95. 95

    View Slide

  96. 96
    The Presence list stores each
    user’s vote

    View Slide

  97. 97
    When all the users have voted, and we
    want to display the next card, then we
    need to clear out these votes from
    Presence

    View Slide

  98. 98
    Only ONE user––the driver––sends the
    “next_card” message to their
    channel to display the next card

    View Slide

  99. 99
    This is when we want to clear the
    votes for all users from Presence

    View Slide

  100. 100
    But! A channel can only
    update metadata for its own
    tracked process in Presence

    View Slide

  101. 101
    We need to tell all of the subscribing
    channels to clear their votes from
    Presence when the “new_card”
    message is broadcast

    View Slide

  102. 102
    We need handle_out/3

    View Slide

  103. 103
    handle_out/3 allows all subscribing
    channels to intercept an outgoing
    broadcast and do some work

    View Slide

  104. 104
    # room_channel.ex
    intercept ["new_card"]
    def handle_out("new_card", payload, socket) do
    Presence.update(
    socket,
    socket.assigns.username,
    %{points: nil})
    push(socket, "new_card", payload)
    {:noreply, socket}
    end

    View Slide

  105. Okay, now it’s
    really your turn :)

    View Slide