How to share a list of votes between multiple logged-in users without saving them into the database?

Hi.

I’m learning about Phoenix by trying to build an app where logged-in users would be able to vote and see each other responses in real time. Like a Sprint Planning Estimation session.

I was able to:

  • sign in users with Google
  • see who is online using Presence
  • all using LiveView, no changes in JavaScript. I would like to keep it this way if possible

Question: where to save the votes so all users see each other votes in real time?
I could save in the user object, but I would prefer to save it in a separate thing so I can use the Presence module to see who is online in other parts of the app.

I would like to avoid saving the votes in the database because they are ephemeral and I don’t need their history.

This is what I have so far:

# lib/app_web/live/online/index.ex

defmodule AppWeb.OnlineLive do
  use AppWeb, :live_view

  def mount(params, session, socket) do
    session_random_id = params["session_random_id"] # random string generated on page load
    current_user = session["current_user"] # who is authenticated with Google

    socket = stream(socket, :online_users, [])
     |> assign(:current_user, current_user)
     |> assign(:session_random_id, session_random_id)

    socket =
    if current_user && connected?(socket) do
      # adding the current user to the list of users that are online
      AppWeb.Presence.track_user(session_random_id, %{id: current_user.id, name: current_user.name})
      # listing to the session channel to know who is online
      AppWeb.Presence.subscribe(session_random_id)

      stream(socket, :online_users, AppWeb.Presence.list_online_users(session_random_id))
    else
       socket
    end

    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <h2>Online users:</h2>
    <ul id="online_users" phx-update="stream">
      <li :for={{dom_id, %{id: id, user: user, metas: _metas}} <- @streams.online_users} id={dom_id}>
        id: <%= id %> name: <%= user.name %>
     </li>
    </ul>
    <h2>Click to vote</h2>
    <button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="2">2</button>
    <button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="3">3</button>
    <button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="5">5</button>
    <button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="8">8</button>
    <button class="bg-zinc-100 rounded px-2 py-1 hover:bg-zinc-200" phx-click="vote" phx-value-option="13">13</button>
    """
  end

  @impl true
  def handle_event("vote", %{"option" => option}, socket) do
    # QUESTION: where to save the votes so all users see each other votes in real time?
    # I could save in the user object, but I would prefer to save it in a separate thing/module/etc
    {:noreply, socket}
  end

  def handle_info({AppWeb.Presence, {:join, presence}}, socket) do
    {:noreply, stream_insert(socket, :online_users, presence)}
  end

  def handle_info({AppWeb.Presence, {:leave, presence}}, socket) do
    if presence.metas == [] do
      {:noreply, stream_delete(socket, :online_users, presence)}
    else
      {:noreply, stream_insert(socket, :online_users, presence)}
    end
  end
end
# lib/app_web/channels/presence.ex
defmodule AppWeb.Presence do
  @moduledoc """
  Provides presence tracking to channels and processes.

  See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
  docs for more details.
  """
  use Phoenix.Presence,
    otp_app: :app,
    pubsub_server: App.PubSub

  def init(_opts) do
    {:ok, %{}}
  end

  def fetch(_topic, presences) do
    for {key, %{metas: [meta | metas]}} <- presences, into: %{} do
      {key, %{metas: [meta | metas], id: meta.id, user: meta}}
    end
  end

  def handle_metas(topic, %{joins: joins, leaves: leaves}, presences, state) do
    for {user_id, presence} <- joins do
      user_data = %{id: user_id, user: presence.user, metas: Map.fetch!(presences, user_id)}
      msg = {__MODULE__, {:join, user_data}}
      Phoenix.PubSub.local_broadcast(App.PubSub, "proxy:#{topic}", msg)
    end

    for {user_id, presence} <- leaves do
      metas =
        case Map.fetch(presences, user_id) do
          {:ok, presence_metas} -> presence_metas
          :error -> []
        end

      user_data = %{id: user_id, user: presence.user, metas: metas}
      msg = {__MODULE__, {:leave, user_data}}
      Phoenix.PubSub.local_broadcast(App.PubSub, "proxy:#{topic}", msg)
    end

    {:ok, state}
  end

  def list_online_users(session_random_id),
    do: list(session_random_id) |> Enum.map(fn {_id, presence} -> presence end)

  def track_user(session_random_id, user),
    do: track(self(), session_random_id, user.id, user)

  def subscribe(session_random_id),
    do: Phoenix.PubSub.subscribe(App.PubSub, "proxy:#{session_random_id}")
end

Does anyone know where and how to save the votes?

Thank you for any insight. :purple_heart:

It is time for You to learn about GenServer and OTP

A good place to store state is in a process :slight_smile:

4 Likes

Ha! Finally I have a use-case to use this GenServer thing. Thank you. :purple_heart:

If I’m understanding the docs right, it sounds like I could have a GenServer holding the user votes in a structure like this:

%{
  session_random_id => %{ user_id => vote, user_id => vote },
  session_random_id => %{ user_id => vote, user_id => vote },
  session_random_id => %{ user_id => vote, user_id => vote },
  session_random_id => %{ user_id => vote, user_id => vote 
}

Basically a map mapping user ids to their vote per session.

Is that what you mean? That sounds awesome. I will give it a try.

I still have one question though: how would LiveView know to refresh the list of votes to show in the browser? Do I also need to use another PubSub somehow?

Simplest would be to let your genserver send a pubsub message to a common channel.