0

I'm trying to use Phoenix Channels as a dead-simple game server. For the prototype, its only purpose is to sync player positions. However, before that happens, every client must be aware of all other clients' existence.

When a new client joins the room, I broadcast that to all present clients from my RoomChannel:

@impl true
def join("room:main", _payload, socket) do
  send(self(), :after_join)
  {:ok, socket}
end

@impl true
def handle_info(:after_join, socket) do
  broadcast_from!(socket, "user_join", %{user_id: socket.assigns.user_id})
  {:noreply, socket}
end

These existing clients react to the message and spawn a new player object. However, the new user has no idea who else is in the room with them, as they joined last and didn't get sent any such information. I'm trying to rectify that.

My plan is to modify my code to something like this:

@impl true
def join("room:main", _payload, socket) do
  Agent.update(active_users_agent, fn(list) -> [list | socket.assigns.user_id] end, 5000)
  send(self(), :after_join)
  {:ok, socket}
end

@impl true
def handle_info(:after_join, socket) do
  push(socket, "room_data", Agent.get(active_users_agent, fn(list) -> list end))
  broadcast_from!(socket, "user_join", %{user_id: socket.assigns.user_id})
  {:noreply, socket}
end

In other words, I want to maintain a list of joined users and send it to the new user upon joining (and add them to it). Whether I use an Agent, a GenServer, or something else doesn't matter overly much.

The problem is, I don't know where I can initialize that Agent, as my RoomChannel doesn't have an init() callback; nor how to pass it to the join() and handle_info() callbacks. What's a working—or better yet, idiomatic—way to do this?


P.S. It's worth noting I want this stored in memory, not in a database.

verified_tinker
  • 625
  • 5
  • 17
  • you can initialize it in the application supervision tree when the server starts. – Daniel Feb 03 '22 at 10:43
  • @Daniel That's what I was thinking. What I don't get is how I can then pass the handle to the agent to `RoomChannel`. Should I register the PID as an atom and use that? – verified_tinker Feb 03 '22 at 10:45
  • 2
    you can create a named process, to do this you have to pass name option on start_link, https://hexdocs.pm/elixir/1.13/GenServer.html#t:name/0 – Daniel Feb 03 '22 at 10:48

1 Answers1

2

You can probably use Phoenix.Presence to handle all of those headache.

When a user joined the websocket, you can store the user's state data in the socket connection itself.

Example:

@impl true
def join("room:main", _payload, socket) do
  user_info = %{
    something1: "something1....",
    something2: "something2....",
    ...
  }

  send(self(), :after_join)

  {:ok, assign(socket, user_info: user_info)}
end

Then to make sure that Phoenix.Presence record those data. You need to do something like this

Example:

@impl true
def handle_info(:after_join, socket) do
  %{user_info: user_info} = socket.assigns

  {:ok, _} = Presence.track(socket, "room:main", %{user_info: user_info})

  push(socket, "presence_state", Presence.list(socket))
  {:noreply, socket}
end

The "presence_state" channel will broadcast the list of users that are subscribed to the channel.

But let say you wanna do something more specific like only displaying the number of people in the channel. So probably you can also do something like this in the handle_info(:after_join..

Example:

@impl true
def handle_info(:after_join, socket) do
  %{user_info: user_info} = socket.assigns

  {:ok, _} = Presence.track(socket, "room:main", %{user_info: user_info})
  
  total_users = 
    Presence.list(socket)["room:main"].metas
    |> Enum.count()

  broadcast!(socket, "room:main:total_users", %{total_users: total_users})
  push(socket, "presence_state", Presence.list(socket))
  {:noreply, socket}
end

You need to make sure that your client is listening to the "room:main:total_users" channel aswell in order to get the broadcasted data.

Hopefully it helps. Cheers!

SyamsulMJ
  • 39
  • 2