How is state stored inside a room/channel?

233 views Asked by At

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.

1

There are 1 answers

0
SyamsulMJ On

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!