Broadcasting an event from a controller doesn't seem to work. MyApp.Endpoint.broacast(@topic, @event, @payload)

Hey guys first post here!
It seems that I’m missing something because I can’t get this problem to work.

I have a simple controller when hit broadcast a welcome message to clients using Endpoint.broadcast!() function.
But event broadcast from my controller using the Endpoint does not seem to hit clients.
I even run test on the controller and they are failing…

What is confusing me is when i broadcast the same in my channel with both Channel.broadcast() and Endpoint.broadcast!() , the event hits the clients…

My Controller

defmodule RealTimeWeb.PageController do
  use RealTimeWeb, :controller

  def index(conn, _params) do
    RealTimeWeb.Endpoint.broadcast!("PAGE", "WELCOME", %{data: "Welcome to Real Time Page"})
    render(conn, "index.html")
  end
end

My Controller Test => Failed

defmodule RealTimeWeb.PageControllerTest do
  use RealTimeWeb.ConnCase

  test "GET /", %{conn: conn} do
    conn = get(conn, "/")
    assert_receive %Phoenix.Socket.Broadcast{event: "WELCOME", topic: "PAGE", payload: %{"data" => _}}
    #assert html_response(conn, 200) =~ "Welcome to Phoenix!"
  end
end

My Channel

defmodule RealTimeWeb.PageChannel do
    use RealTimeWeb, :channel

    @topic "PAGE"

    def join(@topic = topic, params, socket), do: _join(params, topic, socket)
    def join(_, _, _), do: {:error, %{reason: "unauthorized"}}

    defp _join(_params, topic, socket) do
        send(self(), {:after_join, topic})
        {:ok, socket}
    end

    def handle_info({:after_join, @topic}, socket) do
        broadcast!(socket, "WELCOME", %{data: "welcome to Real Time Channel"})
        RealTimeWeb.Endpoint.broadcast(@topic, "WELCOME", %{data: "Welcome to Real Time Channel with endpoint"})
        {:noreply, socket}
    end
end

My Channel Test => Passed

defmodule RealTimeWeb.PageChannelTest do
    use RealTimeWeb.ChannelCase
    alias RealTimeWeb.UserSocket

    setup do
        {:ok, socket} = connect(UserSocket, %{})
        {:ok, socket: socket}
    end

    test "send welcome message when join topic successfully", %{socket: socket} do
        {:ok, _, _socket} = subscribe_and_join(socket, "PAGE", %{})

        assert_broadcast "WELCOME", %{data: _}
    end

end

My Socket

defmodule RealTimeWeb.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel "PAGE", RealTimeWeb.PageChannel

  def connect(_params, socket, _connect_info) do
    {:ok, socket}
  end

  def id(_socket), do: nil
end

Client side (Socket.js)


import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})

socket.connect()

let channel = socket.channel("PAGE", {})

channel.on("WELCOME", data => console.log(data))

channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

I really need your help…
Thank you…

Is your problem only test-related, or does it also not work when you start the app, open browser, trigger some action - in that case do you get the broadcast sent to clients?

Also browser related…

Only events broadcasted from Channel Module is hitting the browser…
Not The ones being broadcasted from the Controller Module…

just created a new phx app from scratch and copy pasted in your code. For me it worked fine (also the broadcast from the controller). I can see all messages inside the browsers dev console.
The only change on top of your code was uncommenting this line in app.js:

import socket from "./socket"

with these kind of issues I would advise to create a public github project with your whole project inside, in that way people can help you better and you talk about the exact same code (bit by bit).

Dont have access at the moment …
I ll upload the whole demo to github and link so that u can have a look at it…

Thank you…

You need to subscribe your test process in your controller test to the topic you are broadcasting to:

RealTimeWeb.Endpoint.subscribe("PAGE")
1 Like

That’s why I asked if it’s only test-related issue. But the author claims it’s happening outside tests too.

@hubertlepicki Sure, but the fact remains that if they want to make the test work they need to join the channel within the test.

1 Like

You, sir, make a lot of sense

this is a link to the github project…
RealTime

Changed the controller test to this…

defmodule RealTimeWeb.PageControllerTest do
  use RealTimeWeb.ConnCase

  setup do
    RealTimeWeb.Endpoint.subscribe("PAGE")
  end

  test "GET /", %{conn: conn} do
    conn = get(conn, "/")
    assert_receive %Phoenix.Socket.Broadcast{event: "WELCOME", topic: "PAGE", payload: %{"data" => _}}
    assert html_response(conn, 200) =~ "Welcome to Phoenix!"
  end
end

And it passed successfully.

Yet am still not receiving the “WELCOME” event in my client side…


import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})

socket.connect()

let channel = socket.channel("PAGE", {})

channel.on("WELCOME", data => console.log(data))

channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

Am supposed to have 3 console log, but only 2 displaying…

These are event are hitting the client side (Being broadcasted from RealTime.PageChannel)

def handle_info({:after_join, @topic}, socket) do
        broadcast!(socket, "WELCOME", %{data: "welcome to Real Time Channel"})
        RealTimeWeb.Endpoint.broadcast(@topic, "WELCOME", %{data: "Welcome to Real Time Channel with endpoint"})
        {:noreply, socket}
end

But the following no hitting the client side (Being broadcasted from RealTime.PageController)

def index(conn, _params) do
    RealTimeWeb.Endpoint.broadcast!("PAGE", "WELCOME", %{data: "Welcome to Real Time Page"})
    render(conn, "index.html")
end

Console Log

i checked out your github project. It works correctly, the thing what happens is the following:
during the controller action you broadcast a message and then render the page, the page connects to your websocket and misses the broadcast because it is too ‘slow’ (better said the broadcast is too fast as the page renders in 250µs during development :wink: ).

So it’s more the fact that you broadcast inside the controller action and expect to see it during it’s page render what’s now going wrong.

If you open another tab to the same page and you refresh the other one you’ll see that the broadcast fires fine because that other tab doesn’t have to do a full http page render (and let javascript connect to the websocket again.

If you want to actually see the broadcast that you’re sending rendered from the same controller action you have to introduce some delay in the broadcast itself (to make sure the page is already rendered and connected to your websocket before the broadcast is sent out) example code for this could be something like this:

defmodule RealTimeWeb.PageController do
  use RealTimeWeb, :controller

  def index(conn, _params) do
    Task.start(fn -> 
    	:timer.sleep(100)
    	RealTimeWeb.Endpoint.broadcast!("PAGE", "WELCOME", %{data: "Welcome to Real Time Page"})
    end)
    render(conn, "index.html")
  end
end

(i wouldn’t do this in any production code, it’s just to show what is happening)

Hope this helps!

Redmar

3 Likes

Oooh… i see… so is there any way to do this…?

http request are completely unrelated to channel connections, so it’s certainly not possible without some extra work. If the client is only connected in one tab/window and it’s not an ajax request, then there isn’t even a working channel connection to the client at the time your controller action is running, as the browser is currently in “going to another page” mode.

Can you describe a bit more what you want to actually happen in the end. Maybe there are more appropriate ways to get to the same result.

1 Like

I have a Dashboard that displays

  • Total of post
  • Total of daily views
  • Total of daily comments

An API through which connected android app can read, comment on posts
So the idea is when an android app open and read or comment a post i need to update the dashboard accordingly.

Post Api Controller

defmodule MyApp.API.PostController do

    use MyApp, :controller
  
    action_fallback MyApp.FallbackController

    def index(conn, params) 
        render(conn, "index.json", posts: posts, pagination: pagination)
    end

    def show(conn, %{"id" => post_id}) do
        post = News.get_post!(post_id)

        MyApp.Endpoint.broadcast!("DASHBOARD", "POST_READ", @data)

        MyApp.SystemLog.log(conn, "View", "Post")
        |> render("show.json", post: post) 
    end

    def comment(conn, %{"id" => post_id, "message" => message}) do
        post_comment_params = %{message: message, client_id: client_id, post_id: post_id}

        with {:ok, %PostComment{} = c} <- News.create_post_comment(post_comment_params) do
            # IO.inspect {:comment, c}
            %Post{} = post = News.get_post!(post_id)
            {feed_comments, pagination} = News.list_feed_comments(for: feed)

            MyApp.Endpoint.broadcast!("DASHBOARD", "POST_COMMENT", @data)

            MyApp.SystemLog.log(conn, "Comment", "Post")
            |> render("comments.json", @data)
        end
    end    
end

But The above MyApp.Endpoint.broadcast!() calls not hitting the browser viewing the dashboard.

Dashboard Channel

defmodule MyApp.DashboardChannel do
    use MyApp, :channel
    
    @topic "DASHBOARD"

    @events ["POST_COMMENT", "POST_READ"]

    def join(@topic = topic, params, socket), do: _join(params, topic, socket)
    def join(_, _, _), do: {:error, %{reason: "unauthorized"}}

    defp _join(_params, topic, socket) do
        send(self(), {:after_join, topic})
        {:ok, socket}
    end

    def handle_info({:after_join, @topic}, socket) do
        Enum.each(@events, fn event -> _broadcast(event, socket) end)
        {:noreply, socket}
    end

    def _broadcast("POST_COMMENT" = event, socket) do
        _do_broadcast(event, %{count: System.Dashboard.posts(:comments)}, socket)
    end
    def _broadcast("POST_READ" = event, socket) do
        _do_broadcast(event, System.Dashboard.posts(:reads), socket)
    end

    defp _do_broadcast(event, data, _socket) do
        # broadcast(socket, event, %{data: data})
        MyApp.Endpoint.broadcast!(@topic, event, %{data: data})
    end

end

And MyApp.Endpoint.broadcast!() call from the above channel works fine…

Dashboard Client side (js)


import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})

socket.connect()

let channel = socket.channel("DASHBOARD", {})

channel.on("POST_READ", data => console.log(data))
channel.on("POST_COMMENT", data => console.log(data))

channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

i hope this gives you enough info on what i want to acheive…
thank you.