$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

More Decks by Sophie DeBenedetto

Other Decks in Programming


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

  2. Introductions 2

  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
  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
  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
  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
  7. Today’s Goals 7

  8. What We’ll Learn ◦ Part 1: • WebSockets • Intro

    to Phoenix Channels • Phoenix PubSub ◦ Part 2: • Phoenix Presence • Complex, Real-Time UI Changes 8
  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
  10. Pointing Party A collaborative ticket estimation tool for your team

  11. 11

  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
  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
  14. Part 1: WebSockets, Channels and PubSub

  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
  16. We Need Real-Time! In the current state of our app,

    users cannot collaborate on ticket estimation. 16
  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
  18. 18 WebSockets

  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
  20. 20 Server Client Request GET https://elixirschool.com Response <h1>Welcome to Elixir

    School!</h1> HTTP Protocol
  21. 21 Server Client Step 1. Request to initiate WS connection

    Client Client GET / Host: elixirschool.com Upgrade: websocket Connection: upgrade
  22. 22 Server Client Step 2. Open and maintain WS connections

    Client Client
  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
  24. Harnessing the Real-Time Web Part 2: Phoenix Presence

  25. 25 We Have A Problem

  26. 26 How can teammates collaborate in a super fun pointing

    party if they can’t see who is participating?
  27. 27 How can we sync and share stateful info, like

    who is present?
  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
  29. Do This • Use Phoenix Presence! 29

  30. 30 Phoenix Presence

  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
  32. 32 Phoenix Presence + Distributed Elixir =

  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
  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
  35. 35 How It Works

  36. 36 Channel topic chats:1 Step 1. User joins the channel

    and registers their presence PubSub Client “user 1 is present”
  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”
  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” </> </> </>
  39. 39 A Closer Look

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

    1. Registering User Presence 40
  41. 41 1. Registering User Presence

  42. 42 Define your app’s Presence module

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

  44. 44 Add your Presence module to the supervision tree

  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
  46. 46 User joins a channel

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

  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
  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
  50. 50 { "sophiemaria" => %{ metas: [%{phx_ref: “xxxx"}] }, “mstalker"

    => %{ metas:[%{ phx_ref: "xxxx"}] } }
  51. Roadmap 2. Broadcasting Presence Events 3. Presence on the Front-End

    1. Registering User Presence 51
  52. 52 2. Broadcasting Presence Events

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

  54. 54 This event is broadcast to all channels for a

    given topic, which are being tracked by Presence
  55. Roadmap 2. Broadcasting Presence Events 3. Presence on the Front-End

    1. Registering User Presence 55
  56. 56 3. Presence Events on the Front-End

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

  58. 58 import { Presence } from 'phoenix' const presence =

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

  60. 60 Fetching the updated list of present users

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

  62. 62 Using a listBy function

  63. 63 You can give presence.list() a listBy function to specify

    which metadata to collect for each user
  64. 64 const listBy=(username, {metas: [{points}, ..._rest]}) => ({username, points})

  65. 65 But Wait!

  66. 66 We have another problem

  67. 67 How can we display the list of already-present users

    to anyone who joins the channel?
  68. 68 Fetching present users on channel join

  69. 69 Presence.list(socket)

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

  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
  72. 72 users = Presence.list(socket) push(socket, "presence_state", users)

  73. 73 Putting it all together

  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
  75. 75 But Wait!

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

  77. 77 Do we need to write more code?

  78. 78 Nope!

  79. 79 What happens to a channel process when the user

    navigates away from the page?
  80. 80 It dies

  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
  82. Your Turn!

  83. Feature Roadmap 2. Users see list of present users 3.

    All votes are tallied, displayed 1. User initiates pointing round 83
  84. 84

  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
  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
  87. Harnessing the Real-Time Web Part 3: Complex UI Updated with

    Channels and Presence
  88. Your Turn

  89. Feature Roadmap 2. Users see list of present users 3.

    All votes are tallied, displayed 1. User initiates pointing round 89
  90. 90

  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
  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
  93. Before we jump in...

  94. 94 Gotcha: Updating presence across channels

  95. 95

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

  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
  98. 98 Only ONE user––the driver––sends the “next_card” message to their

    channel to display the next card
  99. 99 This is when we want to clear the votes

    for all users from Presence
  100. 100 But! A channel can only update metadata for its

    own tracked process in Presence
  101. 101 We need to tell all of the subscribing channels

    to clear their votes from Presence when the “new_card” message is broadcast
  102. 102 We need handle_out/3

  103. 103 handle_out/3 allows all subscribing channels to intercept an outgoing

    broadcast and do some work
  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
  105. Okay, now it’s really your turn :)