Associations, PubSub and live-updating - larger payload or enumerable iteration?

Imagine this familiar situation. We have a LiveView that renders a list of posts. Each post has an author (user) and multiple comments. Each comment also has an author (user). The LiveView process subscribes to some “new comment” topic. When a new comment is created on a post, this information is broadcast.

In this situation, it is clear that the PubSub payload should include the comment’s author preloaded. This could be a new user, who registered after the LiveView mounted. All is good.

Now imagine a different situation. We have a LiveView that renders a restaurant menu. A user can add items to their basket. A basket can be shared, so multiple users are using the same basket. The LiveView process subscribes to some “new basket item” topic. When an item is added to the basket, this information is broadcast. The LiveView is also subscribed to menu changes, e.g. menu item price changes. This means the LiveView process “knows” all the menu details.

In this situation, the basket item is associated to a menu item. My scruple is this: should the PubSub payload include the menu item preloaded on the created basket item struct? It seems wasteful, since, assuming our LiveView also subscribes to menu updates, the LiveView already “knows” the menu item details (not using streams here). Moreover, if, say, a menu item price changes, we will need update all assigned basket items that have this menu item preloaded.

With this in mind, it seems silly to trust/use a basket item’s preloaded menu item association. So I naturally think “OK, I will just find the menu item in the menu assign and infer details from that for rendering the basket item”. However, my menu items are a list, and iterating over a list for every basket item render is obviously very bad. OK, so maybe I should instead have a map of menu item IDs to menu items? Now the code feels very unwieldy, since I am perpetually transforming lists into maps, just for easy access of assoc details.

This entire train of thought has been a thorn in my side while doing LiveView development. I feel like no matter which approach I take, I am begetting obvious inefficiency.

Does anyone else feel this way? Does anyone have ancient secrets of efficient nested association assigns they want to share?

P.S. I love LiveView. It is awesome. I use it every day.

Obvious because you’ve measured it?

1 Like

I had a similar sentiment to @cmo reading this—why is it very bad to iterate every time? Especially since you obviously aren’t refetching from the DB, though honestly, I just do that a lot of the time (though I don’t work on very large apps).

Not exactly sure what you mean. Like, if the price changes, you should just replace the whole menu item record with the one that came in from the event. There’s no need to go and change just the price.

Also, usually when I have a page with a single one-to-many relationship like this, I forgo preloading and just make two calls—one for the parent and one for the children—and then store each in their own assign. Lately I’ve been trying to keep preloads primarily for situations where a record doesn’t make any sense without its association. Not saying this is a particular good or bad idea since it’s not always practical.

Yes, this is what I would do. I am asking about preloaded associations.

In the hypothetical example I gave (I thought it was a common thought-experiment domain), I am asking if I should also “replace the whole menu item record” associated with each basket item. Or should the socket assigns have the menu item struct in only 1 place and refer to that for menu item details?

My question is abstract. In its purest form what I am saying is:

  1. In the socket assigns map, having the same entity twice, in different places, is inefficient. It also invites logical inconsistencies where the socket assigns can contain conflicting data.

  2. If an entity is to exist exactly once in the assigns, we need some way to quickly access those details when representing it as an association on some other entity. There are things more efficient than maps but, conceptually, whatever it is, it will be like a map (key and value).

  3. This is why I find myself turning assoc lists into maps.

I am simply asking if this resonates with anyone else out there. I know there are other experienced LiveView developers who have worked on large-scale apps that must have encountered this same problem.

Ohhhhhhh so sorry I misunderstood that. I’m also sorry that I don’t have a good answer! For me I absolutely would and worry about it if it ever became a problem. Unfortunately, I’ve never had anything that got large enough where it would be a problem so I am unqualified to answer!

I do see what you mean about too much knowledge, though. If you have BasketBasketItemMenuItem and if MenuItem changes then the LiveView has too much knowledge of the domain. You could still push this to the domain logic so the LiveView doesn’t have to know structure. Something like Baskets.update_menu_item(basket, menu_item) where it figures out which basket_items need to be updated and then you could just return the whole updated basket (or just the menu items or just the menu items that were updated depending on how granular you want to get).

That’s a bit of a mix of off-the-top of my head of what I’d do in that situation and what I’ve done before!

EDIT: Your most recently message makes me think I’m still misunderstanding somethings so I’ll just shut up now :sweat_smile:

Because

On every mount, for every item in list A, iterate over list B to find a match.

is obviously inefficient.

If you don’t understand my scruple, please just leave the space clear for more helpful responses.

Broadcasting the entire root struct is simply too large in many cases, especially when there are 1000s of connected clients.

Sorry if I have not explained it well. I feel like it is one of those problems where you have to experience it to know what I mean.

I didn’t mean the entire root struct, I meant like:

def handle_info({:menu_item_updated, menu_item}, socket) do
  basket = socket.assigns.basket
  {:ok, basket} = MyApp.Baskets.replace_updated_menu_item(basket, menu_item)

  {noreply, assign(socket, :basket, basket}
end

Maybe you wouldn’t return the whole basket but mostly to illustrate I just meant broadcasting only the updated menu_item.

I merely intended to trigger the thought in you that maybe you should benchmark things instead of thinking you know what is and is not inefficient. I am sometimes surprised that my intuition about what will be faster is wrong when I benchmark different approaches.

Especially when you’re talking about a restaurant menu, which is not a large data structure.

1 Like

This is a good idea. Thank you.

I agree with broadcasting just the updated entity. Minimal payload there.

Thanks for your responses.

I have a new approach that is working quite well.

Sticking with the example given in OP, the idea is:

  1. Fetch data from a context function that has a cached result. For example, using nebulex.
  2. When another function mutates data, invalidate the cache and broadcast a tiny payload.
  3. Subscribers receiving the payload simply hit the context function and get the cached, but fresh, data.

Then it’s just a matter of doing your iterations early, if you need them, and assigning for use in components and stuff.
e.g. built a map with ID keys and string name values for quick access.

With LiveView’s under-the-hood diffing (which is way better than anything I would write), and with a careful eye to update “related” assigns when some “thing” is live-updated, I sometimes don’t have to even think about what is changing.

In practice, this means my payloads now look like this:

{:blurt, "some-binary-id"}

BLURT = Brute Live Update of Rendered Thing

TL;DR I try to send the smallest possible payload in my broadcasts. I use Nebulex to avoid hitting the database for every connected client. I iterate early, usually no later than mount or handle_params.

Here is a pseudo-example

defmodule MyApp.Blurb do
  @moduledoc """
  BLURB = Brute Live Update of Rendered Basket
  """
  alias MyApp.Baskets.NebulexCache
  alias MyApp.Baskets.Basket

  @topic "blurb"
  def topic(basket_id), do: @topic <> basket_id
  def subscribe(basket_id), do: Phoenix.PubSub.subscribe(MyApp.PubSub, topic(basket_id))

  def broadcast(basket_id),
    do: Phoenix.PubSub.broadcast(MyApp.PubSub, topic(basket_id), {:blurb, basket_id})

  @doc """
  A simple way of updating a displayed basket in real-time.

  When a basket is updated in any kind of way, we broadcast just
  the basket ID and a message like `basket_updated`.

  Subscribers should then fetch the basket but ensure to do so using
  Nebucache or whatever we are using to cache function results.


  1. clear nebucache for a basket
  2. broadcast the basket ID and message
  """
  @spec clear_and_broadcast(Basket.id()) :: :ok
  def clear_and_broadcast(basket_id) do
    NebulexCache.clear_basket_id(basket_id)
    broadcast(basket_id)
  end
end

Then, if my basket changes in any kind of way at all (or even if something nested within basket, e.g. if someone changes the quantity of one of the items) then I just write a single line in the context function:

Blurb.clear_and_broadcast(basket_id)

I would love any feedback or criticism. I’m sure there are pitfalls with this approach that I am not seeing (such is life).