Livebook | Watches in LiveView
Mix.install([
{:phoenix_playground, "~> 0.1.0"},
{:ecto_foundationdb, "~> 0.3"}
])
Intro
We’re going to create a simple LiveView to showcase your favorite quote. We want to make sure our LiveView always has the most up-to-date information about the quote. A common appproach to a problem like this is to use Phoenix.PubSub
. Instead, we’ll use EctoFoundationDB’s Watches to deliver the messaging.
Setup Ecto
First, we set up Ecto, defining a Quote schema, and starting the Repo. For this to work, you must have foundationdb-server
running locally. Refer to the EctoFoundationDB documentation for installation help.
defmodule Repo do
use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.FoundationDB
use EctoFoundationDB.Migrator
def migrations(), do: []
end
defmodule Quote do
use Ecto.Schema
alias Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "quotes" do
field(:author, :string)
field(:content, :string)
field(:likes, :integer, default: 0)
field(:as_of, :naive_datetime)
timestamps()
end
def changeset(quote, params \\ %{}) do
quote
|> Changeset.cast(params, [:author, :content, :likes])
end
def like!(tenant, id) do
Repo.transaction(fn ->
quote = Repo.get!(Quote, id)
quote
|> changeset(%{likes: quote.likes+1})
|> Repo.update!()
end, prefix: tenant)
end
end
{:ok, _} = Ecto.Adapters.FoundationDB.ensure_all_started(Repo.config(), :temporary)
Repo.start_link(log: false)
alias EctoFoundationDB.Tenant
tenant = Tenant.open!(Repo, "experiment-with-watches-in-liveview")
Next, we create a Quote in the DB. We’ll focus on how this specific Quote is rendered throughout the rest of the Livebook.
Repo.insert!(
%Quote{
id: "my-favorite-quote",
author: "Philippe Verdoux",
content: """
Enlightenment leads to benightedness; Science entails nescience.
""",
as_of: NaiveDateTime.utc_now(:second)
}, prefix: tenant, on_conflict: :replace_all)
Setup LiveView
We’re using Phoenix Playground to create a sample LiveView. Some key takeaways:
-
In
assign_watch!/4
, we’re reading the Quote with id"my-favorite-quote"
that we’ve inserted above. In the same transaction, we’re creating a watch withlabel: :quote
. In the LiveView assigns, we store the:quote
and the list of:futures
. You’ll want to use the same label for both the watch and the assigns. -
handle_event/3
receives the"like"
event, and uses an FDB transaction to add 1 to the count of likes. Notice we choose not to update the assigns, for demonstration purposes. -
handle_info/2
receives the:ready
message from the watch future. It usesRepo.assign_ready/3
to update the LiveView assigns map. Whenever"my-favorite-quote"
changes in the DB, this function will be called automatically. Also, we providewatch?: true
to continue listening for updates. -
assign_watch!/4
andhandle_info/2
both contain nothing specific to the Quote schema. This is a general purpose approach that can be re-used for any other schema types.
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
tenant = Tenant.open!(Repo, "experiment-with-watches-in-liveview")
{:ok, socket
|> assign(tenant: tenant)
|> assign_watch!(Quote, :quote, "my-favorite-quote")
}
end
defp assign_watch!(socket, schema, label, id) do
{struct, futures} = Repo.transaction(
fn ->
struct = Repo.get!(schema, id)
future = Repo.watch(struct, label: label)
{struct, [future]}
end,
prefix: socket.assigns.tenant)
socket
|> assign(label, struct)
|> assign(futures: futures)
end
def render(assigns) do
~H"""
My Favorite Quote
as of <%= @quote.as_of %> UTC
<%= @quote.content %>
- <%= @quote.author %>
Likes:
<%= @quote.likes %>
👍
h1 { text-align: center; }
h3 { text-align: right; }
h6 { text-align: right; }
p { text-align: center; }
.content {
max-width: 500px;
margin: auto;
}
"""
end
def handle_event("like", _params, socket) do
Quote.like!(socket.assigns.tenant, socket.assigns.quote.id)
{:noreply, socket}
end
def handle_info({ref, :ready}, socket) when is_reference(ref) do
%{assigns: assigns} = socket
{new_assigns, futures} =
Repo.assign_ready(
assigns.futures,
[ref],
watch?: true,
prefix: assigns.tenant
)
{:noreply,
socket
|> assign(new_assigns)
|> assign(futures: futures)
}
end
end
PhoenixPlayground.start(live: DemoLive)
With the block above evaluated, you can now open a web browser to http://localhost:4000
. We suggest you keep this open side-by-side with this Livebook if possible, so that you can watch the updates in real-time.
Updating My Favorite Quote
Your favorite quote might change from moment to moment. We’ll update the database with your current favorite quote, and because of the FDB watch, the LiveView will always render the most up-to-date content, without any PubSub.
tenant = Tenant.open!(Repo, "experiment-with-watches-in-liveview")
defmodule ChangeFavoriteQuote do
@id "my-favorite-quote"
def to(tenant, author, content) do
Repo.transaction(fn ->
Repo.get(Quote, @id)
|> Quote.changeset(%{
author: author,
content: content,
as_of: NaiveDateTime.utc_now(:second),
likes: 0
})
|> Repo.update()
end, prefix: tenant)
end
end
ChangeFavoriteQuote.to(
tenant,
"Duke Leto Atreides",
"""
Once you have explored a fear, it becomes less terrifying. \
Part of courage comes from extending our knowledge.
""")
ChangeFavoriteQuote.to(tenant, "Captain America", "I can do this all day.")
Takeaways
The behavior that our webpage exhibits is standard LiveView real-time updating. In real Phoenix applications, this is commonly done via Phoenix.PubSub
. There are many advantages to using PubSub, but in this demo we replaced it with a feature from FDB.
The main takeaway from this Livebook is that there is no dispatcher. Each process of our application can choose to register a watch, and it will be notified directly as needed.