How to put something in a socket's assigns from another process

Yep, send a message from that other process to the process that you want to change the asisgn in and let it change it itself. :slight_smile:

That is the Actor way to change any data in a process from elsewhere. ^.^

2 Likes

Thanks, but how do I get the pid of the process that keeps the socket which I want to ban?

Oh, stupid me, I can just send it a message via MyApp.Endpoint.push/3

Oops, there is no MyApp.Endpoint.push/3

Register it somewhere is the traditional way, however it sounds like you are using Phoenix channels, in which case it has a system for this for you already! :slight_smile:

Look in the Channels section (that has no hotlink sadly) in Phoenix.Endpoint — Phoenix v1.7.10 for details on the calls, but basically:
Have each user socket that you want to listen (the one where you want to add the banned part) register itself to a unique user topic, something like MyEndpoint.subscribe(user_id) then you can just MyEndpoint.broadcast(user_id, :add_to_ban_list, ["blah", "blorp"]) or something like that. :slight_smile:

2 Likes

So do I understand correctly that every user’s socket (since every user can be banned somewhere) would subscribe itself to user_id topic in the PubSub so that sometime later it can receive a message that it has been banned?

It might sound like a premature optimization, but how much overhead would that add (if any)?

Yep yep, try to make sure the topic names would be unique of course, so you’d want to prepend something like "UserChannel:"<>user_id or so just to make sure. :slight_smile:

A direct subscribe like that links the current existing process by storing its pid in the global table, so no real overhead for that. Broadcasting just does a lookup in the global table for the PID(s) and then just sends a message like normal, so trivial there either. In other words, this already is the optimized path short of really low-level stuff that you honestly should not ever do. :slight_smile:

3 Likes

Thanks again!

1 Like

Did you check out: https://hexdocs.pm/phoenix/Phoenix.Socket.html#c:id/1

1 Like

Thanks, that’s exactly what I need. But where do I handle this message? In MyApp.UserSocket or in MyApp.UserChannel? Can’t find an example on github.

IIRC, you would receive it in your channels and intercept it (see “Intercepting Outgoing Events” in the channel doc) to change the state instead of actually broadcasting to frontends.

2 Likes

Can’t make it work …

channel/user_socket.ex

defmodule Test.Web.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel "user:*", Test.Web.UserChannel

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket

  def connect(_params, socket) do
    id = :crypto.strong_rand_bytes(9) |> Base.encode64
    {:ok, assign(socket, :id, id)}
  end

  def id(%{assigns: %{id: id}}), do: IO.inspect("user_socket:#{id}")
end

channel/user_chanel.ex

defmodule Test.Web.UserChannel do
  use Test.Web, :channel
  alias Phoenix.Socket.Broadcast

  intercept ["test"]

  def join("user:" <> _user_id, _params, %{assigns: %{id: _id}} = socket) do
    {:ok, socket}
  end

  def handle_in(event, msg, socket) do
    IO.inspect([event, msg])
    {:reply, :ok, socket}
  end

  def handle_out(event, msg, socket) do
    IO.inspect([event, msg, socket])
    {:noreply, socket}
  end

  def handle_info(%Broadcast{} = broadcast, socket) do
    IO.inspect(broadcast)
    {:noreply, socket}
  end
end

And I broadcast something like Test.Web.Endpoint.broadcast "user_socket:YL9ewkRK/Fg8", "test", %{}. No IO.inspect in handle_* gets called.

[info] GET /
[debug] Processing with Test.Web.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 252µs
"user_socket:YL9ewkRK/Fg8"
[info] JOIN "user:1" to Test.Web.UserChannel
  Transport:  Phoenix.Transports.WebSocket
  Parameters: %{}
[info] Replied user:1 :ok
iex(7)> Test.Web.Endpoint.broadcast "user_socket:YL9ewkRK/Fg8", "test", %{}
:ok

Also, I’m a little bit afraid to use intercept because of this note in the docs

Note: intercepting events can introduce significantly more overhead if a large number of subscribers must customize a message since the broadcast will be encoded N times instead of a single shared encoding across all subscribers.

That is because you are broadcasting to the id topic, which is good for pretty much only killing the channel entirely and all topics built on it, this is not what you want.

Instead you need to put something like MyServer.Endpoint.subscribe("UserChannelBlah:#{socket.assigns.user_id}") in your UserChannel module, say in the join function as an example, and you need to listen for that message as your handle_info is already doing fine (though matching on the message event is useful).

1 Like

Thanks.

I still don’t understand how Test.Web.Endpoint.broadcast <socket id>, event, msg works though. Is there only a predefined set of events that I can send to it, like "disconnect"?

Here by socket id I mean the value returned by https://hexdocs.pm/phoenix/Phoenix.Socket.html#c:id/1

Nope, can send anything and everything you want. The event just gets put into the %Broadcast{:event} field, nothing special there at all. :slight_smile:

1 Like

But no handle_* function ever got the message. Where should have I handled it?

If you are broadcasting to the topic set in your id method on the UserSocket, then you’d handle it in the UserSocket module itself if it allows you to override handle_info (I’m not sure it does for safety reasons, but I’ve not tried).

1 Like

Why is it not good for modifying the socket’s assigns?

You can sure, it just won’t do anything unless other broadcast messages use them. Remember that the channels get a ‘copy’ of the assigns, so changing it in the channel will do absolutely nothing to theirs.

2 Likes

If you are broadcasting to the topic set in your id method on the UserSocket, then you’d handle it in the UserSocket module itself if it allows you to override handle_info (I’m not sure it does for safety reasons, but I’ve not tried).

I’ve tried adding

  def handle_info(event, msg, socket) do
    IO.inspect([event, msg, socket])
    {:noreply, socket}
  end

  def handle_info(event, socket) do
    IO.inspect([event, socket])
    {:noreply, socket}
  end

to UserSocket module but still no IO.inspect got called.

And it doesn’t seem to not conform to the genserver behaviour anywhere in the code https://github.com/phoenixframework/phoenix/blob/master/lib/phoenix/socket.ex

Thought it might not, Sockets are for handling sockets only so that makes sense, keep it to channels then. :slight_smile:

I’m not sure why you would want to receive messages in the socket to be honest, I’ve never thought up a need other than disconnecting them…

1 Like