How to send notifications to group of users/topics at once? Also, am I using PubSub correctly?

I’m pretty new to this part of Phoenix, but I was able to figure out a very basic way to make the server send a message to a single room or a single user through the Phoenix API setup, without actually having to join the channel. In this case, a user will forward a message from one chat room to another chat room that they belong to (roomid) without actually joining that room in their browser:

defmodule AppWeb.ChatController do
  use Phoenix.Controller

  def forward(conn, %{"room" => roomid, "userid" => _userid, "message" => _message} = payload) do
    AppWeb.Endpoint.subscribe("room:#{roomid}")
    AppWeb.Endpoint.broadcast("room:#{roomid}", "receiveMessage", payload)
    json(conn, %{})
  end

  # catch-all error: sends back an empty JSON array for all requests that don't  have required params
  def forward(conn, %{}) do
    IO.inspect("Invalid parameters")
    json(conn, %{})
  end
end

From what I understand, AppWeb.Endpoint.subscribe / AppWeb.Endpoint.broadcast is actually using PubSub in the background? And the script is required to subscribe to that room before any message/notification can be sent to it?

In the function above, the app subscribes to that room and then sends the message. It only needs to be done once. So does it unsubscribe automatically after the function ends? I’m concerned that the server may accumulate a huge list of subscriptions in the background, when all it needs to do is fire off quick, one-time messages when needed. Or am I not using PubSub correctly in the way I’m intending?

How to send sitewide notifications to list of users as well?

Let’s say this room has 20 users - some are in the room topic, such as room:159, some are offline, and some are browsing other pages on the website. Each individual user has already joined their own user topic, such as user:1, user:244, user:6234, etc.

The next step in my journey is that in addition to sending the forwarded message to that room, I also want to send a notification to all users that might be somewhere else in the website (not actually in the room), and put a note in their notification bell that their group chat received a new message.

Is there some way for PubSub to quickly subscribe to a whole list of those user topics and send a notification to each of them? For example, how could I do something like this?:

  userList = ["user:1", "user:244", "user6234", ...]
  AppWeb.Endpoint.subscribe(userList)
  AppWeb.Endpoint.broadcast(userList, "receiveNotification", payload)

Or is there some other method designed for this?

You only need to subscribe if you want to receive messages on that topic, not to send to it.

1 Like

huh, that’s odd. When I was building it yesterday, it would absolutely not work without the subscribe, but now it seems to work fine if I remove that…

So now that I know this, I’ve removed all the subscribe functions and just try broadcasting to a list of users:

userList = ["user:1", "user:244", "user:6234", ...]
AppWeb.Endpoint.broadcast(userList, "receiveNotification", payload)

This is the type of error I get:

[error] #PID<0.535.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.534.0>, stream id 1) terminated
Server: ex.example.com:80 (http)
Request: POST /ex/chat/forward
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Phoenix.Channel.Server.broadcast/4
        (phoenix 1.7.7) lib/phoenix/channel/server.ex:139: Phoenix.Channel.Server.broadcast(App.PubSub, ["user:1", "user:244", "user:6234"], "receiveNotice", %{"message" => "test"})

This is probably because I’m trying to broadcast to a list of users/topics, but I can’t find anything in the docs on how to do this. Unless I have to iterate through a loop and broadcast to each user one-at-a-time?

The mysterious ways of computers.

About your other question, how do you know who is a member of a room?

Normally they would be subscribed to the room topic, and you’d just send a message there.

I keep track of the users in a database, as well as any messages sent. So if a user is offline, they can login, view the room page and then this channel connection gets fired:

roomchannel = socket.channel("room:"+roomid+"", {});

When I send the message to the room topic, only the people in the room will see the message popup live. But not everyone is in the room. They might be on a different part of the website reading the news.

However, every user on our site, on any page, will also join their own private user channel:

userchannel = socket.channel("user:"+userid+"", {});

So if there are 20 users that are part of that group chat room, and someone sends a message, I would like to let each user know about the new message. But let’s say 15 of them are not in the room at the time, they are browsing the news on a different part of the site, I would like Phoenix to send something to their private user topic, such as user:1 , user:244 , user:6234 … Basically an event that adds something to their notification bell. Since every user is connected to their own private channel, they won’t miss the notification.

My strategy was something like this:

userList = ["user:1", "user:244", "user:6234", ...] // 20 different userids
AppWeb.Endpoint.broadcast(userList, "receiveNotification", payload)

There is (as far as I know) no function to send to a list of subscribers, so you’ll have to iterate over the list and send it to each one individually.

Enum.each(userList, & AppWeb.Endpoint.broadcast(&1, "receiveNotification", payload))
1 Like

This solution definitely works, thanks. I wonder how resource-intensive this would be on a larger scale, looping a ton of individual broadcasts. It reminds me of a problem I had with MySQL, looping 1,000 individual inserts to the database. Hopefully that doesn’t drag Phoenix down if I need to loop 1,000 individual broadcast. We’ll see if anyone else might know of some special function.

I heard of another solution of subscribing each user to every single room they are involved with (even if they aren’t in the room at the time), but that seems overkill, especially if they are involved with 100,000 different “rooms” and if the channel has to re-subscribe on every page view / socket connection.

In the end it doesn’t matter. It has to send it to each individual channel anyway.
And internally it is just a message that’s sent to a process, and that is what the BEAM is optimized for.

1 Like