Slide 1

Slide 1 text

Harnessing the Real-Time Web With Phoenix Channels & Presence

Slide 2

Slide 2 text

Introductions 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Today’s Goals 7

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Pointing Party A collaborative ticket estimation tool for your team 10

Slide 11

Slide 11 text

11

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Part 1: WebSockets, Channels and PubSub

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

18 WebSockets

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

20 Server Client Request GET https://elixirschool.com Response

Welcome to Elixir School!

HTTP Protocol

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Harnessing the Real-Time Web Part 2: Phoenix Presence

Slide 25

Slide 25 text

25 We Have A Problem

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Do This ● Use Phoenix Presence! 29

Slide 30

Slide 30 text

30 Phoenix Presence

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

32 Phoenix Presence + Distributed Elixir =

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

“ …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

Slide 35

Slide 35 text

35 How It Works

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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”

Slide 38

Slide 38 text

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” > > >

Slide 39

Slide 39 text

39 A Closer Look

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

41 1. Registering User Presence

Slide 42

Slide 42 text

42 Define your app’s Presence module

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

44 Add your Presence module to the supervision tree

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

46 User joins a channel

Slide 47

Slide 47 text

47 Track the user presence in Presence’s data store

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

52 2. Broadcasting Presence Events

Slide 53

Slide 53 text

53 Tracking user presence triggers the “presence_diff” event

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

56 3. Presence Events on the Front-End

Slide 57

Slide 57 text

57 Connecting to the Presence on the front-end

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

60 Fetching the updated list of present users

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

62 Using a listBy function

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

65 But Wait!

Slide 66

Slide 66 text

66 We have another problem

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

68 Fetching present users on channel join

Slide 69

Slide 69 text

69 Presence.list(socket)

Slide 70

Slide 70 text

70 Pushing present users to the newly-joined client

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

73 Putting it all together

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

75 But Wait!

Slide 76

Slide 76 text

76 What happens when a user leaves the pointing party?

Slide 77

Slide 77 text

77 Do we need to write more code?

Slide 78

Slide 78 text

78 Nope!

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

80 It dies

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Your Turn!

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

84

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

Your Turn

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

90

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

Before we jump in...

Slide 94

Slide 94 text

94 Gotcha: Updating presence across channels

Slide 95

Slide 95 text

95

Slide 96

Slide 96 text

96 The Presence list stores each user’s vote

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

102 We need handle_out/3

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

Okay, now it’s really your turn :)