Slide 1

Slide 1 text

PRESENCE AIN'T PERFECT Vladimir Dementyev Evil Martians / AnyCable SF Ruby Feb'25

Slide 2

Slide 2 text

Real-Time Web Applications

Slide 3

Slide 3 text

Real-Time Web Applications with Ruby on Rails

Slide 4

Slide 4 text

Real-Time Web Applications with Ruby on Rails Action Cable Hotwire AnyCable Patterns & Pitfalls Not Coming Soon

Slide 5

Slide 5 text

Real-Time Web Applications with Ruby on Rails Not Coming Soon Patterns & Pitfalls Action Cable Hotwire AnyCable

Slide 6

Slide 6 text

Patterns & Pitfalls evilmartians.com/events/the-pitfalls-of-realtime-ification

Slide 7

Slide 7 text

Patterns & Pitfalls evilmartians.com/events/the-pitfalls-of-realtime-ification

Slide 8

Slide 8 text

Patterns & Pitfalls evilmartians.com/events/the-pitfalls-of-realtime-ification

Slide 9

Slide 9 text

Patterns & Pitfalls evilmartians.com/events/the-pitfalls-of-realtime-ification

Slide 10

Slide 10 text

PRESENCE

Slide 11

Slide 11 text

Presence

Slide 12

Slide 12 text

Presence

Slide 13

Slide 13 text

class OnlineChannel < ApplicationCable::Channel def subscribed stream_from "online" broadcast_to( "online", {user:, event: "join"} ) end def unsubscribed broadcast_to( "online", {user:, event: "leave"} ) end end app/channels/online_channel.rb online_channel.rb 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19

Slide 14

Slide 14 text

In the post-ICQ era, multiple sessions (tabs, devices) per user are common

Slide 15

Slide 15 text

class OnlineChannel < ApplicationCable::Channel def subscribed stream_from "online" user.online_sessions.create!(session_id:) broadcast_to("online", {user:, event: "join"}) if user.online_sessions.size.one? end def unsubscribed user.online_sessions.where(session_id:) .delete_all broadcast_to("online", {user:, event: "leave"}) unless user.online_sessions.any? end end app/channels/online_channel.rb online_channel.rb 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20

Slide 16

Slide 16 text

class OnlineChannel < ApplicationCable::Channel def subscribed stream_from "online" user.online_sessions.create!(session_id:) broadcast_to("online", {user:, event: "join"}) if user.online_sessions.size.one? end def unsubscribed user.online_sessions.where(session_id:) .delete_all broadcast_to("online", {user:, event: "leave"}) unless user.online_sessions.any? end end app/channels/online_channel.rb online_channel.rb 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Problem solved?

Slide 17

Slide 17 text

Web Server Connection Client Socket WebSocket Channel Worker Pool HTTP CLOSE #disconnect #unsubscribed The route of #unsubscribed

Slide 18

Slide 18 text

Web Server Connection Client Socket WebSocket Channel Worker Pool CLOSE #disconnect #unsubscribed The route of #unsubscribed: happy path

Slide 19

Slide 19 text

Web Server Connection Client Socket WebSocket Channel Worker Pool WRITE ERROR #disconnect #unsubscribed The route of #unsubscribed: abnormal closure ping retry retry ~15min unless tcp_retries2 is tuned

Slide 20

Slide 20 text

Web Server Connection Client Socket WebSocket Channel Worker Pool HTTP The route of #unsubscribed: server crash / forceful termination

Slide 21

Slide 21 text

WebSocket The route of #unsubscribed: server crash / forceful termination No #disconnect at all!

Slide 22

Slide 22 text

PRESENCE IN THE WILD

Slide 23

Slide 23 text

stanko.io/tracking-online-presence-with-actioncable-ukEi6sUZ98Bz

Slide 24

Slide 24 text

stanko.io/tracking-online-presence-with-actioncable-ukEi6sUZ98Bz

Slide 25

Slide 25 text

stanko.io/tracking-online-presence-with-actioncable-ukEi6sUZ98Bz

Slide 26

Slide 26 text

stanko.io/tracking-online-presence-with-actioncable-ukEi6sUZ98Bz

Slide 27

Slide 27 text

https://once.com/campfire

Slide 28

Slide 28 text

https://rails.chat

Slide 29

Slide 29 text

Campfire Presence PresenceChannel callbacks (#after_subscribe, #after_unsubscribe) PresenceChannel#refresh (every 50s) Membership#connected_at

Slide 30

Slide 30 text

Presence patterns Channel subscription callbacks Expiration-based presence (#connected_at, Redis sorted sets) Application-level heartbeat (pong, #refresh)

Slide 31

Slide 31 text

Presence patterns Channel subscription callbacks Expiration-based presence (#connected_at, Redis sorted sets) Application-level heartbeat (pong, #refresh) To much cere-meow-ny!

Slide 32

Slide 32 text

PRESENCE IN ANYCABLE

Slide 33

Slide 33 text

https://blog.anycable.io

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

class OnlineChannel < ApplicationCable::Channel def subscribed stream_from "online_users" join_presence( id: user.id, info: {name: user.username} ) end end app/channels/online_channel.rb online_channel.rb 1 2 3 4 5 7 8 9 10 11 12 Online indicators AnyCable Presence API

Slide 36

Slide 36 text

import { Controller } from "@hotwired/stimulus" import cable from "cable" export default class extends Controller { static targets = ["user"]; async connect() { this.channel = cable.subscribeTo("OnlineChannel"); this.channel.on("presence", this.handlePresence); this.userTargets.forEach(this.userTargetConnected); } async userTargetConnected(el) { const presence = await this.channel.presence.info(); if (presence[userId]) { el.classList.add("online"); } } handlePresence(event) { this.userTargets.forEach(el => { if (el.dataset.id === event.id) { el.classList.toggle("online", event.type === "join"); } }); } } app/javascript/controllers/online_controller.js online_controller.js 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Stimulus target callback AnyCable Presence API

Slide 37

Slide 37 text

Pub/sub Engine Presence Engine AnyCable WebSocket AnyCable Presence Rails (AnyCable RPC)

Slide 38

Slide 38 text

Pub/sub Engine Presence Engine AnyCable WebSocket Channel #join_presence AnyCable Presence Rails (AnyCable RPC) SUBSCRIBE join presence subscribe WebSocket WebSocket {type: "join", ...} {type: "join", ...}

Slide 39

Slide 39 text

Pub/sub Engine Presence Engine AnyCable WebSocket AnyCable Presence Rails (AnyCable RPC) presence ping keep-alive AnyCable PONG extension takes care of abnormal closures pong

Slide 40

Slide 40 text

Pub/sub Engine Presence Engine AnyCable WebSocket AnyCable Presence Rails (AnyCable RPC) WebSocket WebSocket {type: "leave", ...} {type: "leave", ...}

Slide 41

Slide 41 text

Pub/sub Engine Presence Engine AnyCable WebSocket AnyCable Presence subscribe

Slide 42

Slide 42 text

Pub/sub Engine Presence Engine AnyCable WebSocket AnyCable Presence presence WebSocket WebSocket {type: "join", ...} {type: "join", ...} join subscribe Client can join presence on his own (no RPC required)

Slide 43

Slide 43 text

HOT- HOTWIRE PRESENCE

Slide 44

Slide 44 text

0
<%= turbo_stream.append dom_id(channel, :presence) do %>
@<%= user.username %>
<% end %> app/views/channels/_presence.html.erb _presence.html.erb 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

Slide 45

Slide 45 text

0
<%= turbo_stream.append dom_id(channel, :presence) do %>
@<%= user.username %>
<% end %> app/views/channels/_presence.html.erb _presence.html.erb 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Custom element similar to Turbo Stream source

Slide 46

Slide 46 text

0
<%= turbo_stream.append dom_id(channel, :presence) do %>
@<%= user.username %>
<% end %> app/views/channels/_presence.html.erb _presence.html.erb 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Special selector to show the total number of present users

Slide 47

Slide 47 text

0
<%= turbo_stream.append dom_id(channel, :presence) do %>
@<%= user.username %>
<% end %> app/views/channels/_presence.html.erb _presence.html.erb 1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Presence info is just a Turbo Action (executed on join)

Slide 48

Slide 48 text

AnyCable Presence No application-level heartbeats and state maintenance logic Minimalistic API (both client and server side) Let the server take care of all the pitfalls!

Slide 49

Slide 49 text

AnyCable Presence API to get presence information from your application Webhooks to get notified of "join"-s and "leave"-s Coming soon

Slide 50

Slide 50 text

Beyond chats

Slide 51

Slide 51 text

THANK YOU docs.anycable.io/edge/anycable-go/presence anycasts-demo.fly.dev Vladimir Dementyev Evil Martians / AnyCable SF Ruby Feb'25