How to Make a Server-Side Timer with Phoenix WebSockets

The Timer Project

Complete timer project flowchart

Part I: Joining The Channel

Setting Up The Phoenix Application

$ mix phx.new enchufe --no-ecto
$ cd enchufe
$ iex -S mix phx.server

The JavaScript

# assets/js/app.jsimport socket from "./socket"
# assets/js/socket.jslet channel = socket.channel('timer:update', {})
channel.join()
.receive('ok', resp => { console.log('Joined successfully', resp) })
.receive('error', resp => { console.log('Unable to join', resp) })

The Timer Channel

# lib/enchufe_web/channels/timer_channel.exdefmodule EnchufeWeb.TimerChannel do
use Phoenix.Channel
def join("timer:update", _msg, socket) do
{:ok, socket}
end
end

Routing Channels Through Our Socket

# lib/enchufe_web/channels/user_socket.ex## Channels
channel "timer:*", EnchufeWeb.TimerChannel

Test It Out

Front-end joined timer channel and listens to update topic

Part II: Broadcasting Messages

The Timer GenServer

# lib/enchufe/timer.exdefmodule Enchufe.Timer do
use GenServer
require Logger
def start_link() do
GenServer.start_link __MODULE__, %{}
end
## SERVER ##

def init(_state) do
Logger.warn "Enchufe timer server started"
broadcast(30, "Started timer!")
schedule_timer(1_000) # 1 sec timer
{:ok, 30}
end
def handle_info(:update, 0) do
broadcast 0, "TIMEEEE"
{:noreply, 0}
end
def handle_info(:update, time) do
leftover = time - 1
broadcast leftover, "tick tock... tick tock"
schedule_timer(1_000)
{:noreply, leftover}
end
defp schedule_timer(interval) do
Process.send_after self(), :update, interval
end
defp broadcast(time, response) do
EnchufeWeb.Endpoint.broadcast! "timer:update", "new_time", %{
response: response,
time: time,
}
end
end

Handling Incoming Messages

# lib/enchufe_web/channels/timer_channel.exdef handle_in("new_time", msg, socket) do
push socket, "new_time", msg
{:noreply, socket}
end
# assets/js/socket.jschannel.on('new_time', msg => {
console.log("The timer is: ", msg.time)
})

Test It Out

Timer server broadcasts update topics to the timer channel
iex> Enchufe.Timer.start_link()

HTML Timer

# lib/enchufe_web/templates/page/index.html.eex<div class="jumbotron">
...
<p id="status" class="lead">Ready</p>
<p id="timer" class="lead"></p>
...
</div>
# assets/js/socket.jschannel.on('new_time', msg => {
document.getElementById('status').innerHTML = msg.response
document.getElementById('timer').innerHTML = msg.time
}
Timer showing in HTML

Part III: Completing The Loop

Front-end Broadcasting

# lib/enchufe_web/templates/page/index.html.eex<div class="jumbotron">
...
<p id="status" class="lead">Ready</p>
<p id="timer" class="lead"></p>
<button class="btn btn-primary" id="start-timer">Start Timer</button>
</div>
# assets/js/socket.jslet startTimer = function (event) {
event.preventDefault()
channel.push('start_timer', {})
.receive('ok', resp => { console.log('Started timer', resp) })
}
document.getElementById('start-timer').onclick = startTimer
# lib/enchufe_web/channels/timer_channel.exdef handle_in("start_timer", _, socket) do
EnchufeWeb.Endpoint.broadcast("timer:start", "start_timer", %{})
{:noreply, socket}
end

Subscribing The Timer Server

# lib/enchufe/timer.exdef init(_state) do
Logger.warn "Enchufe timer server started"
EnchufeWeb.Endpoint.subscribe "timer:start", []
{:ok, nil}
end
def handle_info(%{event: "start_timer"}, _time) do
duration = 30
schedule_timer 1_000
broadcast duration, "Started timer!"
{:noreply, duration}
end

Starting The GenServer On App Start

# lib/enchufe/application.exchildren = [
...
worker(Enchufe.Timer, []),
]

Test It Out

GenServer and front-end socket communcation

Cancelling Timers

# lib/enchufe/timer.exdefmodule Enchufe.Timer do
use GenServer
require Logger
def start_link() do
GenServer.start_link __MODULE__, %{}
end
## SERVER ##

def init(_state) do
Logger.warn "Enchufe timer server started"
EnchufeWeb.Endpoint.subscribe "timer:start", []
state = %{timer_ref: nil, timer: nil}
{:ok, state}
end
def handle_info(:update, %{timer: 0}) do
broadcast 0, "TIMEEEE"
{:noreply, %{timer_ref: nil, timer: 0}}
end
def handle_info(:update, %{timer: time}) do
leftover = time - 1
timer_ref = schedule_timer 1_000
broadcast leftover, "tick tock... tick tock"
{:noreply, %{timer_ref: timer_ref, timer: leftover}}
end
def handle_info(%{event: "start_timer"}, %{timer_ref: old_timer_ref}) do
cancel_timer(old_timer_ref)
duration = 30
timer_ref = schedule_timer 1_000
broadcast duration, "Started timer!"
{:noreply, %{timer_ref: timer_ref, timer: duration}}
end
defp schedule_timer(interval), do: Process.send_after self(), :update, interval defp cancel_timer(nil), do: :ok
defp cancel_timer(ref), do: Process.cancel_timer(ref)
defp broadcast(time, response) do
EnchufeWeb.Endpoint.broadcast! "timer:update", "new_time", %{
response: response,
time: time,
}
end
end

Summary

Interested in more Elixir tutorials? Join my mailing list to stay updated on new content :)

Software Engineer at https://nav.com |> Chemical Engineer |> Bulldog Lover

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store