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

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. 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
  2. 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
  3. 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
  4. 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
  5. What We’ll Learn ◦ Part 1: • WebSockets • Intro

    to Phoenix Channels • Phoenix PubSub ◦ Part 2: • Phoenix Presence • Complex, Real-Time UI Changes 8
  6. 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
  7. 11

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

    users cannot collaborate on ticket estimation. 16
  12. 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
  13. 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
  14. 21 Server Client Step 1. Request to initiate WS connection

    Client Client GET / Host: elixirschool.com Upgrade: websocket Connection: upgrade
  15. 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
  16. 26 How can teammates collaborate in a super fun pointing

    party if they can’t see who is participating?
  17. 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
  18. 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
  19. 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
  20. “ …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
  21. 36 Channel topic chats:1 Step 1. User joins the channel

    and registers their presence PubSub Client “user 1 is present”
  22. 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”
  23. 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” </> </> </>
  24. 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
  25. 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
  26. 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
  27. 54 This event is broadcast to all channels for a

    given topic, which are being tracked by Presence
  28. 63 You can give presence.list() a listBy function to specify

    which metadata to collect for each user
  29. 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
  30. 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
  31. 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
  32. Feature Roadmap 2. Users see list of present users 3.

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

  34. 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
  35. 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
  36. Feature Roadmap 2. Users see list of present users 3.

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

  38. 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
  39. 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
  40. 95

  41. 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
  42. 99 This is when we want to clear the votes

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

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

    to clear their votes from Presence when the “new_card” message is broadcast
  45. 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