I’m relatively new to phoenix, so I may not have the vocabulary to express myself clearly.
I ended up maintaining as a junior a phoenix application with different channels that each have subtopics.
Some new requirements came in and I opted to do that inside of a LiveView.
I would like to receive updates from a specific channel inside the LiveView, change the assigns and update some content that way.
defmodule MyApp.RoleBillboardLive do
use Phoenix.LiveView
import MyApp.{UserSocket, Endpoint}
alias MyApp.Endpoint
alias Phoenix.Socket.Broadcast
def render(assigns) do
~H"""
Message from server: <%= @param %>
"""
end
def mount(_params, _, socket) do
topic = "global:test"
MyApp.Endpoint.subscribe(topic)
IO.puts("I am now subscribed to " <> topic)
param = "This is a test,"
{:ok, assign(socket, :param, param)}
end
#def handle_info(_, socket) do
# IO.puts("info")
# {:noreply, assign(socket, assign(socket, :yeet, "yote"))}
#end
@impl true
def handle_info(msg, socket) do
IO.inspect(msg)
{:noreply, assign(socket, :param, msg)}
end
end
When I run this code, the mount function is called, I can see it clearly in the terminal.
(I run the command iex -S mix phx.server)
The page loads as expected with the initial content,
but when i publish a message on the topic, handle_info is never even called
I tried to skim through the docs, and a few forum posts, but none of them seemed to be advising on what I’m trying to do.
Can you show the code you are using to publish? Also to confirm, you’re doing the publish call in the same iex session that is running the server, not a separate one right?
Finally in here:
@impl true
def handle_info(msg, socket) do
IO.inspect(msg)
{:noreply, assign(socket, :param, msg)}
end
You should change that to assign(socket, :param, inspect(msg)) because otherwise if you get a message that isn’t a string it’s going to crash when it tries to render it.
Yes I am publishing from the same IEX session that the phx server is running on.
It is a big code base, so I don’t know exactly where all the stuff is broadcasted from.
Throughout the code with some ctrl-f ing, I can see that most broadcast calls are made with this function
def broadcast_global(event_name, payload) do
MyApp.Endpoint.broadcast("global:test", event_name, payload)
end
it’s also pretty much equivalent to what I tried doing in the IEX session
If you are doing MyApp.Endpoint.subscribe("global:test") and then you MyApp.Endpoint.broadcast("global:test", "hello", "world") that should work just fine. I see you had commented out another handle_info clause, is that actually commented out or not?
If you run in your iex session (here using my app’s name Sensetra)
@benwilson512
Yeah, it’s actually commented out. Its just me trying out different combinations of stuff I googled
Anyways, I did exactly what you did in the iex code snippet, and it works .
So now I just need to figure out why my handle_info in my live_view is never called …
no.
the flush is a way to empty the process inbox, it’s a helper from the repl, you don’t need that in normal code, on normal code you should use receive
receive do
message -> do_something(message)
end
a phoenix channel and live view are genservers, that is an abstraction of a infinite loop of receive blocks.
a phoenix channel usually handles all %Phoenix.Socket.Broadcast{} and send it directly to the websocket, if you want to do something with it before sending to the ws you need to intercept the particular event.
a live view has a callback, iirc it’s handle_event to deal with incoming events before dealing with it in the live view.
all that is a oversimplification of the thing. tthere are somewhat more details in the docs.
edit:
handle_event is for receiving client events, i’m not sure if the right way to deal with pubsub messages in live views would require to intercept it just like in the channel approach.
That is a great point. @perceval62 you really should do this in your mount/3
def mount(_params, _, socket) do
if connected?(socket) do
topic = "global:test"
MyApp.Endpoint.subscribe(topic)
IO.puts("I am now subscribed to " <> topic)
end
param = "This is a test,"
{:ok, assign(socket, :param, param)}
end
The code as you have it now will subscribe twice in roughly the following sequence:
Static mount runs
You call subscribe, which subscribes the HTTP handling process
HTTP request finishes, so the HTTP process terminates
That subscription is cleaned up automatically
live mount runs
Subscribe calls again, this time subscribing the websocket process
mount completes, but the process remains alive and subscribed.
SO even though it’s happening twice (which is unnecessary, my code removes step 2) it should still work overall. What I and I think others are wondering is if the live mount is happening at all. Maybe in your effort to make a minimal example you accidentally stripped off the javascript and so no liveview is actually happening?
Assuming the client never calls back and the live/connected mount doesn’t run, handle_info will never be called as the only process that subscribed during the static mount gets terminated.
I’d second @benwilson512’s suggestion to check if that’s what’s happening.
@perceval62 no if you only see one "I am not subscribed to" message (in your code as you provided it) then you will never get a handle_info. The static render does not keep a persistent process around that is available to receive messages. You must have the javascript on the front end initiate a websocket connection so that the persistent websocket process is available in the server to receive messages (like a broadcast).
Are you saying you accomplished the UI update with just the javascript code or did you get LiveView working? Can you show the code you used?
What you wrote should have worked just fine. We could help you better if you answered some of the earlier questions regarding whether your IO.puts lines print once or twice.
I accomplished the UI with just the javascript code,
I didn’t get the LiveView to work and update from just the backend code.
Sorry for the delays, I had some issues setting up my home workstation to be like the one at my office …
Here’s the JS frontend code:
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("test", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
const first = "https://myAppCdn.com/something.png"
const second = "https://myAppCdn.com/something_else.png"
channel.on("first", (payload) => {
const image_div = document.getElementById("image_container")
image_div.src = first
})
channel.on("second", (payload) => {
const image_div = document.getElementById("image_container")
image_div.src = second
})
export default socket
here’s the backend code now:
defmodule MyApp.RoleBillboardLive do
use Phoenix.LiveView
@impl true
def render(assigns) do
~H"""
<script src="/js/app.js"></script>
<img id="image_container" src="https://myAppCdn.com/something.png" />
"""
end
@impl true
def mount(_params, _, socket) do
param = "This is a test,"
if connected?(socket) do
IO.puts("subscribed to topic")
MyApp.Endpoint.subscribe("global:test")
end
{:ok, assign(socket, :param, param)}
end
@impl true
def handle_info(msg, socket) do
IO.puts("YEEEEEET")
param = "A very very different message"
{:noreply, assign(socket, :param, param)}
end
end
As for the IO.puts, I did see the messages only once.
But I stopped seeing “subscribed to topic” once I put a connect? check.
And, of course I never saw a “YEEEEEEET”