0

I'm trying to extend the example of Phoenix.Channel.reply/2 from the Phoenix documentation into a fully working example of asynchronously replying to Phoenix channel/socket push events:

Taken from https://hexdocs.pm/phoenix/Phoenix.Channel.html#reply/2:

def handle_in("work", payload, socket) do
  Worker.perform(payload, socket_ref(socket))
  {:noreply, socket}
end

def handle_info({:work_complete, result, ref}, socket) do
  reply ref, {:ok, result}
  {:noreply, socket}
end

I've reworked the example as follows:

room_channels.ex

...
def handle_in("work", job, socket) do
  send worker_pid, {self, job}
  {:noreply, socket}
end

def handle_info({:work_complete, result}, socket) do
  broadcast socket, "work_complete", %{result: result}
  {:noreply, socket}
end
...

worker.ex

...
receive do
  {pid, job} ->
    result = perform(job) # stub
    send pid, {:work_complete, result}
end
...

This solution works, but it doesn't rely on generating and passing a socket_ref with socket_ref(socket) and on Phoenix.Channel.reply/2. Instead it relies on Phoenix.Channel.broadcast/3.

The documentation implies that reply/2 is specifically used for this scenario of asynchronously replying to socket push events:

reply(arg1, arg2)

Replies asynchronously to a socket push.

Useful when you need to reply to a push that can’t otherwise be handled using the {:reply, {status, payload}, socket} return from your handle_in callbacks. reply/3 will be used in the rare cases you need to perform work in another process and reply when finished by generating a reference to the push with socket_ref/1.

When I generate and pass a socket_ref, and rely on Phoenix.Channel.reply/2 for asynchronous replies to socket push, I don't get it to work at all:

room_channels.ex

...
def handle_in("work", job, socket) do
  send worker_pid, {self, job, socket_ref(socket)}
  {:noreply, socket}
end

def handle_info({:work_complete, result, ref}, socket) do
  reply ref, {:ok, result}
  {:noreply, socket}
end
...

worker.ex

...
receive do
  {pid, job, ref} ->
    result = perform(job) # stub
    send pid, {:work_complete, result, ref}
end
...

My room_channels.ex handle_info function is called but reply/2 doesn't seem to send a message down the socket. I see no stacktrace on stderr or any output on stdout to indicate an error either. What is more, keeping track of a socket_ref seems to just add overhead to my code.

What is the benefit of using a socket_ref and reply/2 over my solution with broadcast/3 and how can I get the solution with reply/2 to work?

Marcel
  • 367
  • 1
  • 12

2 Answers2

0

I was wrong, the example with Phoenix.Channel.reply/2 will work:

room_channels.ex

...
def handle_in("work", job, socket) do
  send worker_pid, {self, job, socket_ref(socket)}
  {:noreply, socket}
end

def handle_info({:work_complete, result, ref}, socket) do
  reply ref, {:ok, result}
  {:noreply, socket}
end
...

worker.ex

...
receive do
  {pid, job, ref} ->
    result = perform(job) # stub
    send pid, {:work_complete, result, ref}
end
...

In my implementation I made the mistake of sending a synchronous reply to the event push with the return value of {:reply, :ok, socket} instead of {:noreply, socket}.

On closer inspection of the websocket frames send from the server to the client, I found that the browser did receive my server replies from reply ref, {:ok, result} but that the associated callback was never called.

It seems that Phoenix' Socket.js client library accepts at most a single reply per push events.

Marcel
  • 367
  • 1
  • 12
0

hope I'm not too late to this conversation.

I managed to use your code above and got the callback to work in the javascript.

The trick is to listen to the phx_reply event. Inside priv/static/app.js each Channel javascript object has a list of predefined in CHANNEL_EVENTS (line and is set to listen on it at the following code block:

this.on(CHANNEL_EVENTS.reply, function (payload, ref) {
  _this2.trigger(_this2.replyEventName(ref), payload);
});

What I did was within the channel on callback, I listen for the phx_reply event:

channel.on("phx_reply", (data) => {
  console.log("DATA ", data);
  // Process the data
}

This has been tested on Elixir 1.3 and Phoenix 1.2

Hope that helps!