Liveview diff tracking with pub/sub

Hello there.
I have been trying out liveview for some time, but the diff tracking is still kind of a mystery to me. I’m running into a weird issue when combining it with pub/sub.

I checked out this repo(which is not mine) which creates pretty basic comment functionality with liveview: https://github.com/snewcomer/live-comment

There is one liveview with all the comments. Each comment is a live component, possibly containing children comments. So we have recursive comment component(show.ex). When I post a child comment, assigns change in the specific component and the component is rerendered.

def handle_event(“save”, %{“comment” => comment_params}, socket) do
comment_params
|> Map.put(“parent_id”, socket.assigns.id)
|> Managed.create_comment()
|> case do
{:ok, new_comment} ->
{:noreply, assign(socket, form_visible: false, children: [new_comment])}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end

This works as expected.
Since we are using temporary assigns and phx-udpate append, only the new comment is send over the wire.

The trouble begins when we broadcast this new comment to all liveviews using Phoenix.PubSub.broadcast_from!
The liveview is subscribed to new_comment event and when it comes, it calls send_update with the comment component id like this:

def handle_info({Managed, :new_comment, comment}, socket) do
if comment.parent_id do
send_update(CommentLive.Show, id: comment.parent_id, children: [comment])
{:noreply, socket}
else
{:noreply, assign(socket, comments: [comment])}
end
end

Actual behaviour:
The incognito window shows that all siblings are sent over to the client. Note: not ALL comments are sent. the other root comments are not sent. only the children belonging to same parent. So if I have comments like this:

root1
    comm1
    comm2
root2
root3

and I add comm3 as a reply to root1, all children of root1 are sent to the client(comm1, comm2 and the new comm3). root2 and root3 are not sent at all.

Expected behaviour(for me)
:new_comment event is published, the liveview in incognito window is subscribed, gets the new child comment, calls send_update with the comment. Then the corresponding livecomponent updated its assigns with the new comment and only the new comment is sent as diff to the client(just as it works without pub/sub)
So from the previous example, only the comm3(without comm1 and comm2) should be sent to the client.

My question is why are all the siblings sent to the client, when I create new comment?
Is this supposed to work as I’m expecting and I’m just doing something wrong, or is this not the way it works and my understanding is wrong?

thanks

Anyone?

I still cant make sense of it.

I haven’t really used LiveView yet but what I know is that it tracks elements by DOM ID from this reply How do I optimize LiveView when using pagination? . Do you have DOM ID’s set for your elements?

Hi,
thanks for the reply. I do have DOM IDs set for every comment component.

Just have a quick look at the code from link you posted. It shows that preload/1 loads all comments before update/2 (default; i.e. didn’t override)

Try define update function next to preload function

def update(assigns, socket) do
IO.inspect(assigns.children, label: "assigns")
IO.inspect(socket.assigns.children, label: "socket")
end

I hope you find answer around there.

But wouldn’t liveview do the diff and send only the newest child comments even though I load all of them when I have children in temporary assigns and use phx-append?

Normally, yes, Diff = Current - Previous, right? The example code has temporary_assigns: [comment: nil, children: []]} so once view is rendered, and new comment is submitted, fetched_children - [] = fetched_children. (I didn’t pull the code and run debugging though that’s why you could try IO.inspect in update function there)

I’d debug both the broadcaster and the receiver. The broadcaster has children: [new_comment] on their socket, but receiver will call send_update which will call preload/1 and update/2 the first parameter (assigns) of update/2 will come from the send_update and through preload/1, and the second parameter is existing socket.assigns (at this point it should be empty [ ]), and it will merge the assigns (first parameter) into it.

Did you try debugging around the preload and update function? I won’t be sure for sure until I really pull the code down, run, and debug. Hope this helps a little more. :slight_smile:

I have not yet debugged the code around update/2 and preload/1. I will soon.

So that would mean, if I understand it correctly, that even though I’m sending only new comment through send_update(comments: [new_comment]), all of the comments are assigned to socket and sent to client, since I’m loading all of them in the preload/1 function. Right?

The broadcaster node is already correct (only send a new reply)
To fix the receiver node, put these new 3 lines of code. That’s it!

lib/live_comment_web/live/comment_live/show.ex

  def preload(list_of_assigns) do
    # https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-preloading-and-update
+   {send_update_assigns, list_of_assigns} = Enum.split_with(list_of_assigns, fn a -> Map.get(a, :prevent_preload) end)
    
+   if send_update_assigns == [] do
      parent_ids = Enum.map(list_of_assigns, & &1.id)
      children = Managed.fetch_child_comments(parent_ids)
    
      Enum.map(list_of_assigns, fn assigns ->
        Map.put(assigns, :children, Map.get(children, assigns.id, []))
      end)
+  else
+    send_update_assigns
+  end 
  end

lib/live_comment_web/live/comment_live/index.ex

  def handle_info({Managed, :new_comment, comment}, socket) do
    if comment.parent_id do
-     send_update(CommentLive.Show, id: comment.parent_id, children: [comment])
+     send_update(CommentLive.Show, id: comment.parent_id, children: [comment], prevent_preload: true)
      {:noreply, socket}
    else
      {:noreply, assign(socket, comments: [comment])}
    end 
  end 
1 Like

Thank you very much, I will try that out