Jun 10, 2022

The Magic of Phoenix LiveView and PubSub

When I was a kid I went through a phase where I was pretty into magic. I learned some simple card tricks and bought a couple of cheap props such as a disappearing vase and a fake thumb. Not being content with simple tricks, I remember asking my Dad if there was such a thing as “real” magic. He replied there was magic everywhere; as proof he whipped out a calculator and exclaimed how amazing it was that a little device could be so powerful. Magic was everywhere in technology! I admit to not being very impressed at the time… however nowadays with Phoenix and Elixir I’ve become a convert, I feel the magic!

Today we’ll build out a small application that brings together the magic of LiveView, PubSub, and Oban. For our application we’re going to revisit a classic 80’s movie, ET. We’ll write an application to help a group of stranded extraterrestrials “phone home” and get back to their planet.

It will look like:

We’re not going to build anything particularly special or impressive… but IMO what is impressive is how easy and with how little code something like this can be accomplished via LiveView. No API, no front-end javascript framework to worry about, all with a SPA-like feel… how is that not a win!

Creating the project

We’ll call our project phone_home, so let’s generate it. For reference I’m using version 1.13.4 of Elixir and 1.6.9 of Phoenix.

Terminal
mix phx.new phone_home

We’ll follow the instructions from the mix phx.new output and make sure everything seems to be running.

Terminal
cd phone_home
mix ecto.create
mix phx.server

Navigating to http://localhost:4000/ everything looks as it should!

All good, let’s get started by creating a database table to hold our extraterrestrials.

Adding the DB table and schema

Since this is just a little demo app we aren’t going to worry too much about the database design or file structure, we’ll create a single database table and place the schema file right in the root of the lib directory.

Let’s start by generating a migration for the database table.

Terminal
mix ecto.gen.migration create_extraterrestrials

/priv/repo/migrations/’timestamp’_create_extraterrestrials.exs
defmodule PhoneHome.Repo.Migrations.CreateExtraterrestrials do
  use Ecto.Migration
  def change do
    create table(:extraterrestrials) do
      add :name, :string
      add :communication_status, :string
      add :communication_attempt, :integer
      add :last_communication_attempt_timestamp, :utc_datetime
    end
  end
end

Very simple, we just have a name for our aliens and then a couple of fields to represent the state of their phone home attempts. In a real application you might want to think about whether the communication fields really belong in this table or should be in a separate table… but the above structure will serve our purpose.

The schema will mirror the database fields, let’s create it next.

Terminal
touch lib/phone_home/extraterrestrial.ex
/lib/phone_home/extraterrestrial.ex
defmodule PhoneHome.Extraterrestrial do
  use Ecto.Schema

  schema "extraterrestrials" do
    field :name, :string

    field :communication_status, Ecto.Enum,
      values: [:phoning_home, :space_taxi_on_the_way, :no_answer]

    field :communication_attempt, :integer, default: 0
    field :last_communication_attempt_timestamp, :utc_datetime
  end
end

Nothing complicated, we’re making use of an Ecto.Enum to give a bit more structure to the communication_status field.

Finally let’s add some seed data to initialize the database.

/priv/repo/seeds.exs
alias PhoneHome.Extraterrestrial
alias PhoneHome.Repo

%Extraterrestrial{name: "Ayala"} |> Repo.insert!()
%Extraterrestrial{name: "Bria"} |> Repo.insert!()
%Extraterrestrial{name: "Chiana"} |> Repo.insert!()
%Extraterrestrial{name: "Correllia"} |> Repo.insert!()
%Extraterrestrial{name: "Dengar"} |> Repo.insert!()
%Extraterrestrial{name: "Harishka"} |> Repo.insert!()
%Extraterrestrial{name: "Jubal"} |> Repo.insert!()
%Extraterrestrial{name: "Korben"} |> Repo.insert!()
%Extraterrestrial{name: "Luminara"} |> Repo.insert!()
%Extraterrestrial{name: "Noranti"} |> Repo.insert!()

And with that we can run the migration.

Terminal
mix ecto.migrate

… and the seeds.

Terminal
mix run priv/repo/seeds.exs

Sweet! Let’s create an initial bit of front end code to view our extraterrestrials.

Displaying extraterrestrials

For our first iteration of the front end we will simply list out the extraterrestrials. We’ll need a function to retrieve the extraterrestrials so let’s create that first. We’ll put it right in the main context of the application.

/lib/phone_home.ex
defmodule PhoneHome do
  import Ecto.Query
  alias PhoneHome.Repo
  alias PhoneHome.Extraterrestrial

  # ===========================================================================
  def list_extraterrestrials do
    Extraterrestrial
    |> order_by(:name)
    |> Repo.all()
  end
end

Simple, just grabbing all the records ordered by name.

Next we’ll add a very simple LiveView.

Terminal
mkdir -p lib/phone_home_web/live/extraterrestrial_live
touch lib/phone_home_web/live/extraterrestrial_live/index.ex
touch lib/phone_home_web/live/extraterrestrial_live/index.html.heex

For now, the liveview file just needs a mount function along with a couple of helpers to format the data.

/lib/phone_home_web/live/extraterrestrial_live/index.ex
defmodule PhoneHomeWeb.ExtraterrestrialLive.Index do
  use PhoneHomeWeb, :live_view

  # ===========================================================================
  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :extraterrestrials, PhoneHome.list_extraterrestrials())}
  end

  # ===========================================================================
  def communication_status_display_value(nil), do: "-"
  def communication_status_display_value(val), do: val

  # ===========================================================================
  def communication_attempt_display_value(0), do: "-"
  def communication_attempt_display_value(val), do: val

  # ===========================================================================
  def last_communication_attempt_timestamp_display_value(nil), do: "-"
  def last_communication_attempt_timestamp_display_value(val), do: val
end

The mount function loads up the extraterrestrials and puts them in the socket assigns so they will be available to our HTML. The xxx_display_value functions just provide some formatting help.

The HTML looks like:

/lib/phone_home_web/live/extraterrestrial_live/index.html.heex
<h1>Extraterrestrials</h1>
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Status</th>
      <th>Attempt</th>
      <th>Attempt timestamp</th>
    </tr>
  </thead>
  <tbody id="extraterrestrials">
    <%= for extraterrestrial <- @extraterrestrials do %>
      <tr id={"extraterrestrial-#{extraterrestrial.id}"}>
        <td><%= extraterrestrial.name %></td>
        <td><%= extraterrestrial.communication_status |> communication_status_display_value() %></td>
        <td><%= extraterrestrial.communication_attempt |> communication_attempt_display_value() %></td>
        <td><%= extraterrestrial.last_communication_attempt_timestamp |> last_communication_attempt_timestamp_display_value() %></td>
      </tr>
    <% end %>
  </tbody>
</table>

Simple, we just loop thru the extraterrestrials and display them.

Finally we need to add a route for the liveview.

/lib/phone_home_web/router.ex
...
...
scope "/", PhoneHomeWeb do
  pipe_through :browser

  # comment out the existing root route
  # get "/", PageController, :index
  live "/", ExtraterrestrialLive.Index, :index
end
...
...

Now if we fire up the server…

Terminal
mix phx.server

… and navigate to http://localhost:4000/ we’ll see some aliens!

Nothing magical so far, but let’s start brewing some spells by adding the phone home functionality.

ET Phone Home

We’ll start with the backend code. The idea is we’ll queue up a job for each extraterrestrial attempting to phone home. Each job will be attempted 5 times after which we’ll assume our extraterrestrial’s friends are currently busy and not answering their calls.

Repeatable jobs sound like a great use case for a job scheduling library… and an excellent choice in Elixir land is Oban. So let’s add and configure Oban.

Add Oban

First we’ll add the dependecy to mix.exs.

/mix.exs
defp deps do
  [
    {:phoenix, "~> 1.6.8"},
    {:phoenix_ecto, "~> 4.4"},
    ...
    ...
    {:plug_cowboy, "~> 2.5"},
    {:oban, "~> 2.10"}
  ]
end

And then grab the dependecy.

Terminal
mix deps.get

We need to create some Oban specific tables, so the next step is to create a migration.

Terminal
mix ecto.gen.migration create_oban_jobs

As per the Oban installation guide the contents of the migration should be:

/priv/repo/migrations/’timestamp’_create_oban_jobs.exs
defmodule PhoneHome.Repo.Migrations.CreateObanJobs do
  use Ecto.Migration

  def up do
    Oban.Migrations.up(version: 11)
  end

  # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if
  # necessary, regardless of which version we've migrated `up` to.
  def down do
    Oban.Migrations.down(version: 1)
  end
end

Now we can run the migration.

Terminal
mix ecto.migrate

We next need to add a configuration section for Oban in config.exs… we’ll add it just after the JSON configuration.

/config/config.exs
...
...
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Oban config
config :phone_home, Oban,
  repo: PhoneHome.Repo,
  queues: [phone_calls: 50]

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"

All this specific configuration does is set up a single Oban queue called phone_calls which can execute up to 50 concurrent jobs at a time.

The final bit of setup to get Oban working is to include it in application.ex.

/lib/phone_home/application.ex
defmodule PhoneHome.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start the Ecto repository
      PhoneHome.Repo,
      # Start the Telemetry supervisor
      PhoneHomeWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: PhoneHome.PubSub},
      # Start the Endpoint (http/https)
      PhoneHomeWeb.Endpoint,
      # Start a worker by calling: PhoneHome.Worker.start_link(arg)
      # {PhoneHome.Worker, arg}
      # Start Oban
      {Oban, oban_config()}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: PhoneHome.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  @impl true
  def config_change(changed, _new, removed) do
    PhoneHomeWeb.Endpoint.config_change(changed, removed)
    :ok
  end

  # Conditionally disable queues or plugins here.
  # For example on a staging environment
  defp oban_config do
    Application.fetch_env!(:phone_home, Oban)
  end
end

All we’ve done above is to add Oban to the list of children supervised by our application along with a config function.

Oban has the concept of worker modules. We’ll need one of these to process our phone calls. If you watched ET you’ll recall they used a Speak & Spell to phone home, so we’ll follow their lead and create a speak and spell worker.

The Speak and Spell worker

We’ll start with a bare minimum worker module and build out the functionality as we go along…

Terminal
touch lib/phone_home/speak_and_spell_worker.ex
/lib/phone_home/speak_and_spell_worker.ex
defmodule PhoneHome.SpeakAndSpellWorker do
  use Oban.Worker, queue: :phone_calls, max_attempts: 5

  def perform(%Job{} = _job) do
    :ok
  end
end

The use Oban.Worker line indicates this is an Oban worker. We also specify the queue the worker acts upon and the number of times a particular job will be attempted before Oban gives up, in this case 5 times.

Next let’s add a phone_home function in our main phone_home.ex module to queue up jobs.

/lib/phone_home.ex
defmodule PhoneHome do
  import Ecto.Query
  alias PhoneHome.Repo
  alias PhoneHome.Extraterrestrial
  alias PhoneHome.SpeakAndSpellWorker

  # ===========================================================================
  def list_extraterrestrials do
    Extraterrestrial
    |> order_by(:name)
    |> Repo.all()
  end

  # ===========================================================================
  def phone_home(extraterrestrials) do
    extraterrestrials
    |> Enum.each(fn extraterrestrial ->
      %{extraterrestrial_id: extraterrestrial.id}
      |> SpeakAndSpellWorker.new()
      |> Oban.insert()
    end)
  end
end

The phone_home/1 function inserts a job for each extraterrestrial passed into the function. When inserting an Oban job we include can include a map of args that will be stored in the oban_jobs table. In this case we pass in an extraterrestrial_id. This way we’ll know what extraterrestrial a job refers.

Let’s try this out in the console.

Terminal
iex -S mix phx.server
iex Terminal
PhoneHome.list_extraterrestrials() |> PhoneHome.phone_home()

If we look in the database we’ll see our jobs have processed…

It’s not very impressive as the jobs don’t actually do anything… let’s tackle that next.

Add the phone_home logic

Instead of just returning :ok we’ll update the speak_and_spell_worker to include a fake phone_home function. Based on the result of this function we’ll update the communication_status and communication_attempt values for the appropriate extraterrestrial.

/lib/phone_home/speak_and_spell_worker.ex
defmodule PhoneHome.SpeakAndSpellWorker do
  use Oban.Worker, queue: :phone_calls, max_attempts: 5

  # ===========================================================================
  def perform(%Job{attempt: attempt, args: %{"extraterrestrial_id" => extraterrestrial_id}}) do
    extraterrestrial = PhoneHome.get_extraterrestrial!(extraterrestrial_id)

    phone_home()
    |> case do
      {:ok, :call_connected} ->
        process_phone_home_result(extraterrestrial, :space_taxi_on_the_way, attempt)

        :ok

      {:error, :no_answer} ->
        error_status = error_status(attempt)
        process_phone_home_result(extraterrestrial, error_status, attempt)

        {:error, :no_answer}
    end
  end

  # ===========================================================================
  defp process_phone_home_result(extraterrestrial, status, attempt) do
    PhoneHome.update_extraterrestrial_communication_status!(
      extraterrestrial,
      status,
      attempt
    )
  end

  # ===========================================================================
  defp phone_home do
    1..5
    |> Enum.random()
    |> case do
      1 -> {:ok, :call_connected}
      _ -> {:error, :no_answer}
    end
  end

  # ===========================================================================
  defp error_status(5 = _attempt), do: :no_answer
  defp error_status(_attempt), do: :phoning_home
end

Pretty simple… the first thing we do is grab the extraterrestrial based on the Oban args field, i.e.

def perform(%Job{attempt: attempt, args: %{"extraterrestrial_id" => extraterrestrial_id}}) do
    extraterrestrial = PhoneHome.get_extraterrestrial!(extraterrestrial_id)
    ...

Next we call into a function we’ve faked up to simulate some phone home logic:

defp phone_home do
  1..5
  |> Enum.random()
  |> case do
    1 -> {:ok, :call_connected}
    _ -> {:error, :no_answer}
  end
end

Based on the return value of the above function we update the extraterrestrial:

phone_home()
|> case do
  {:ok, :call_connected} ->
    process_phone_home_result(extraterrestrial, :space_taxi_on_the_way, attempt)

    :ok

  {:error, :no_answer} ->
    error_status = error_status(attempt)
    process_phone_home_result(extraterrestrial, error_status, attempt)

    {:error, :no_answer}
end

Notice we return an error tuple when we receive no answer in the perform function. When Oban receives an error tuple it records the error and schedules a retry of the job (assuming the max_attempts value has not yet been reached). When we return :ok in the success case Oban just marks the job as complete.

The process_phone_home_result function we call prior to returning :ok or {:error, :no_answer} just updates the extraterrestrial record in the database.

defp process_phone_home_result(extraterrestrial, status, attempt) do
  PhoneHome.update_extraterrestrial_communication_status!(
    extraterrestrial,
    status,
    attempt
  )
end

The worker module is using a few functions that don’t yet exist so we need to add get_extraterrestrial! and update_extraterrestrial_communication_status! to the main phone_home.ex module.

/lib/phone_home.ex
defmodule PhoneHome do
  import Ecto.Query

  # add an alias for Ecto.Changeset
  alias Ecto.Changeset

  ...


  # ===========================================================================
  def get_extraterrestrial!(id), do: Repo.get!(Extraterrestrial, id)

  # ===========================================================================
  def update_extraterrestrial_communication_status!(extraterrestrial, status, attempt \\ 0) do
    extraterrestrial
    |> Changeset.change(%{
      communication_status: status,
      communication_attempt: attempt,
      last_communication_attempt_timestamp: DateTime.utc_now() |> DateTime.truncate(:second)
    })
    |> Repo.update!()
  end

  ...

Nothing tricky about either of these, just standard Repo / Changeset stuff.

Let’s hit up the console again now that we’ve made these changes.

Note: when making changes to an Oban worker you need to recompile in order for the changes to take affect (or restart the server).

iex Terminal
recompile
PhoneHome.list_extraterrestrials() |> PhoneHome.phone_home()

Now if we refresh the browser as our jobs process we’ll see the status of our phone home attempts.

So this works alright but we don’t really want to have to refresh the browser in order to see the status updates… and this is where PubSub comes in.

Broadcasting alien updates

We need to broadcast when an alien is updated. We can then subscribe to these updates in our LiveView and dynamically update the status fields.

Let’s add the subscribe and broadcast functions to phone_home.ex

/lib/phone_home.ex
@pub_sub_topic "extraterrestrial"
# ===========================================================================
def subscribe_to_extraterrestrial_updates() do
  Phoenix.PubSub.subscribe(PhoneHome.PubSub, @pub_sub_topic)
end

# ===========================================================================
def broadcast_extraterrestrial_update(extraterrestrial) do
  Phoenix.PubSub.broadcast(
    PhoneHome.PubSub,
    @pub_sub_topic,
    {:extraterrestrial_updated, extraterrestrial}
  )
end

Pretty darn simple, we’re broadcasting / subscribing to a topic called extraterrestrial, and in the broadcast function we are passing a tuple containing an atom (:extraterrestrial_updated) and the actual extraterrestrial.

We need to call broadcast_extraterrestrial_update from our speak_and_spell_worker worker. This is simple, we just need to update process_phone_home_result.

/lib/phone_home/speak_and_spell_worker.ex
defp process_phone_home_result(extraterrestrial, status, attempt) do
  PhoneHome.update_extraterrestrial_communication_status!(
    extraterrestrial,
    status,
    attempt
  )
  |> PhoneHome.broadcast_extraterrestrial_update()
end

All we’ve done is pipe the result of update_extraterrestrial_communication_status! to the broadcast function.

That takes care of the back-end, now for a few front-end updates.

Reflecting the status in the UI

The first thing we’ll do is add a button to our HTML file so we can trigger the phone home functionality.

/lib/phone_home_web/live/extraterrestrial_live/index.html.heex
<h1>Extraterrestrials</h1>
<button phx-click="phone_home">Phone Home</button>
<table>
  ...
  ...

Now in the live view module we’ll:

  • Subscribe to the alien update broadcasts.
  • Add handlers for the button we just added and for alien broadcast events.
  • update the communication_status_display_value functions to display a status specific SVG.

The full code is:

/lib/phone_home_web/live/extraterrestrial_live/index.ex
defmodule PhoneHomeWeb.ExtraterrestrialLive.Index do
  use PhoneHomeWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket), do: PhoneHome.subscribe_to_extraterrestrial_updates()

    {:ok, assign(socket, :extraterrestrials, PhoneHome.list_extraterrestrials())}
  end

  # ===========================================================================
  @impl true
  def handle_event("phone_home", _params, socket) do
    socket.assigns.extraterrestrials
    |> Enum.filter(&(&1.communication_status == nil || &1.communication_status == :no_answer))
    |> PhoneHome.phone_home()

    {:noreply, socket}
  end

  # ===========================================================================
  @impl true
  def handle_info({:extraterrestrial_updated, updated_extraterrestrial}, socket) do
    updated_extraterrestrials =
      socket.assigns.extraterrestrials
      |> Enum.map(fn extraterrestrial ->
        if extraterrestrial.id == updated_extraterrestrial.id do
          updated_extraterrestrial
        else
          extraterrestrial
        end
      end)

    {:noreply, assign(socket, :extraterrestrials, updated_extraterrestrials)}
  end

  # ===========================================================================
  def communication_status_display_value(nil), do: "-"

  def communication_status_display_value(:space_taxi_on_the_way) do
    """
    <div class="row">
      <svg xmlns="http://www.w3.org/2000/svg" style="height:24px;color:green;margin-right:10px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
      </svg>
      Space Taxi On The Way
    </div>
    """
    |> raw()
  end

  def communication_status_display_value(:no_answer) do
    """
    <div class="row">
      <svg xmlns="http://www.w3.org/2000/svg" style="height:24px;color:red;margin-right:10px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
      </svg>
      No Answer
    </div>
    """
    |> raw()
  end

  def communication_status_display_value(:phoning_home) do
    """
    <div class="row">
      <svg style="height:24px;color:orange;margin-right:10px" class="spinner" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
      </svg>
      Phoning Home
    </div>
    """
    |> raw()
  end

  # ===========================================================================
  def communication_attempt_display_value(0), do: "-"
  def communication_attempt_display_value(val), do: val

  # ===========================================================================
  def last_communication_attempt_timestamp_display_value(nil), do: "-"
  def last_communication_attempt_timestamp_display_value(val), do: val
end

Let’s have a quick look at some of the changes, starting with the mount callback.

def mount(_params, _session, socket) do
  if connected?(socket), do: PhoneHome.subscribe_to_extraterrestrial_updates()

  {:ok, assign(socket, :extraterrestrials, PhoneHome.list_extraterrestrials())}
end

Here we subscribe to the extraterrestrial broadcasts. Note we need to do this after the live socket has connected… which is why we make use of the connected? function provided by LiveView.

The button handler is straight forward…

def handle_event("phone_home", _params, socket) do
  socket.assigns.extraterrestrials
  |> Enum.filter(&(&1.communication_status == nil || &1.communication_status == :no_answer))
  |> PhoneHome.phone_home()

  {:noreply, socket}
end

The handler matches on the event name we gave in the HTML for the button, i.e. phx-click="phone_home".

In the body of the function we filter on aliens with a status of nil or :no_answer (as these are the aliens who have neither successfully phoned home or are currently phoning home) and queue jobs for them via PhoneHome.phone_home().

The handler for broadcasts is also pretty simple.

def handle_info({:extraterrestrial_updated, updated_extraterrestrial}, socket) do
  updated_extraterrestrials =
    socket.assigns.extraterrestrials
    |> Enum.map(fn extraterrestrial ->
      if extraterrestrial.id == updated_extraterrestrial.id do
        updated_extraterrestrial
      else
        extraterrestrial
      end
    end)

  {:noreply, assign(socket, :extraterrestrials, updated_extraterrestrials)}
end

We match on the {:extraterrestrial_updated, extraterrestrial} tuple that is sent from the broadcast and then replace the old extraterrestrial struct with the updated struct in our list of extraterrestrials currently contained in the socket assignments.

Other than a couple of changes to the display helper functions which we won’t bother going over that’s it for the liveview module updates.

Update the CSS

The final piece of the pie is to update our css file with some classes to provide a spin operation for the SVG we display during the phoning home status.

/assets/css/app.css
...
...

.spinner {
  border: 2px solid white;
  border-radius: 50%;
  border-top: 2px solid orange;
  border-bottom: 2px solid orange;
  width: 20px;
  height: 20px;
  -webkit-animation: spin 2s linear infinite;
  animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
  0% { -webkit-transform: rotate(0deg); }
  100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

Note: in a real application I would suggest using something like Tailwind CSS instead of writing a bunch of possibly (in my case probably) janky custom CSS.

Let’s give everything a final test… we’ll reset our DB, fire up the server…

Terminal
mix ecto.reset
mix phx.server

… and click the Phone Home button… a sped up version of the result is:

Nice, looks like half our extraterrestrials have their transport home all figured out!

Summary

That’s it for our application, the synergy of LiveView, PubSub and Oban is really impressive in terms of how easy these technologies make it to build something out.

There are things you’d want to consider in a real application. For instance we are storing all our extraterrestrials in the LiveView assigns. This works fine when we have a limited number of extraterrestrials, but if we were in say a They Live situation where there are aliens freakin’ everywhere we might need a different strategy in order to avoid storing everything in memory. Likewise we’d want to think carefully about the broadcast and subscribe functions and what / when we are broadcasting.

And of course some testing would be good! Luckily both LiveView and Oban have solid testing stories… which is as you would expect when it comes to the Elixir eco-system. If you’re interested in diving into LV testing, I recommend the Testing LiveView course by German Velasco. It provides an excellent and comprehensive run thru of LV testing and I personally really enjoyed the course.

Thanks for reading!

Today’s code is available on GitHub.



Comment on this post!