Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Livebook | Watches in LiveView

notebooks/watches.livemd

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:

  1. 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 with label: :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.

  2. 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.

  3. handle_info/2 receives the :ready message from the watch future. It uses Repo.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 provide watch?: true to continue listening for updates.

  4. assign_watch!/4 and handle_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.