With LiveView.stream, how to update the actions on a table without fetching its related item?

When using the new LiveView stream feature, I want to disable the actions of an item (in a table) which is already being edited by another user.

For now, the only way I managed to do this is by calling stream_insert/4 even though this item didn’t really change yet. Otherwise, the actions won’t be updated because they are in the <tbody phx-update="stream">

Here is an example of the table component where Presence.is_entity_used(@presence_list, fruit.id) allows me to know if this item is being edited by another user, and if so, returns the user.

# fruit_live/index.html.heex

<.table id="fruits" rows={@streams.fruits}>
  <:col :let={{_id, fruit}} label="Name"><%= fruit.name %></:col>
  <:col :let={{_id, fruit}} label="Color"><%= fruit.color %></:col>
  <:col :let={{_id, fruit}} label="Quantity"><%= fruit.quantity %></:col>
  <:action :let={{_id, fruit}}>
    <%= if user = Presence.is_entity_used(@presence_list, fruit.id) -> %>
      <%= "This fruit is being edited by #{user.email}" %>
    <% else %>
      <.link patch={~p"/fruits/#{fruit}/edit"}>Edit</.link>
      <.link
        phx-click={JS.push("delete", value: %{id: fruit.id})}
        data-confirm="Are you sure?"
      >
        Delete
      </.link>
    <% end %>
  </:action>
</.table>

Here is a sample project implementing this feature.

My guess is I shouldn’t use streams in this case, what do you think?

1 Like

Hey there, that’s an interesting scenario where only a very small portion of a streamed resource needs to be updated potentially frequently.

First thing that jumped out is that the logic checking for the resource “lock” is in the template itself, which means that every cell in the action column gets re-rendered whenever the @presence_list assign gets updated. Generally speaking, calling user/library functions and defining variables in HEEx templates is a known gotcha documented here under Pitfalls for Assigns and HEEx templates. A better place would be decorating the @streams.fruits streams to have that information outside/before rendering for example in mount.

And since you’re already making use of channels via Presence, I think it makes sense to more precisely stream_insert only the resources/entities that are being locked via subscribe, broadcast, and handle_info after the inital rendering. Basically, you could apply the event handling pattern for creating a new resource/entity with stream_insert to locking an existing resource/entity.

If you didn’t want to stream_insert, I suppose one option to explore would be using JS hooks and push_event to “re-hydrate” the action column.

p.s. Codeberg looks neat, but it seems to be missing a search bar

1 Like

Thanks for your answer @codeanpeace

First thing that jumped out is that the logic checking for the resource “lock” is in the template itself, which means that every cell in the action column gets re-rendered whenever the @presence_list assign gets updated.

Yes, but not in this case because streams are freed from socket as soon as they are rendered. The only way to trigger a re-render of a single row is by using stream_insert. But in other cases this is true.

A better place would be decorating the @streams.fruits streams to have that information outside/before rendering for example in mount.

Yes this is a good point and this is something I had in mind. I should probably add a virtual field “actions” to the fruit entity like so:

%Fruit{
...
actions: %{
    edit: :ok,
    delete: {locked: "Reason to be shown in the template"}
  }
}

But this wouldn’t solve my problem because on presence diff, I would still have to update fruits based on their ids.
And in some cases I can’t retrieve the desired struct of an item I want to update other than fetching it from database (which kind of defeats the purpose of streams) because streams doesn’t keep the state of what is displayed on the client as described in the doc:

Stream items are temporary and freed from socket state as soon as they are rendered

I think I miss a stream_update_by_dom_id which would allow to update some properties of an item without submitting the whole struct.

So my last option, as you said, would probably be to use JS hooks and update the table manually, but it seems too expensive to do, so I think I will just strop using streams for now. It is a nice to have but not essential in my case.

I will keep this post open in case I find something else.

Thanks for your help.

p.s. Codeberg looks neat, but it seems to be missing a search bar

They are looking for maintainers to work on the code search feature

2 Likes

Hmm, could you describe those cases?

When a user clicks the edit button for a streamed item, that item has to be fetched from the database anyway to populate the edit form. It should be possible to then broadcast that item to other users subscribed to the topic and handle those messages by tagging the item as disabled before passing it into a stream_insert.

Anyways, looking forward to seeing what you come up with!

1 Like

When a user clicks the edit button for a streamed item, that item has to be fetched from the database anyway to populate the edit form. It should be possible to then broadcast that item to other users subscribed to the topic and handle those messages by tagging the item as disabled before passing it into a stream_insert.

The special case

I have a special case where only one item can be edited at a time because they all share a piece of data. I must lock the editing action for every entity in this case to avoid concurent editing.

So in this case I would need to stream_insert on every item, or fetch the whole list again every time a user start editing an item.

I think the solution here would probably be to pre render the disabled buttons hidden, and then use LiveView.JS.show and hide to enable/disable the actions when needed.

For every other lists where there is no such special case, I think I found my mistake

I have to handle the case where a user start editing an item and closes the modal or his browser tab without submitting the form.
I handled this by storing the initially fetched item in the Presence metas, so on Presence join and leave events, I could use the stored item to stream_insert.
But when the user submits the form, I only had an outdated item in the Presence metas.

My mistake was that I initially thought I couldn’t update the metas, but apparently it is possible. I was fetching from database on Presence leave, but I could just update the metas when the item is updated by the user.

So this is what it should look like:

  • When a user starts editing, it triggers Presence join event with the freshly fetched item in the metas.
  • When a user submits the form, I should update the item in the metas so it is up to date before it triggers the Presence leave event.
  • When the user leaves (by either closing the modal or the browser tab), I can still use the initially fetched item.

I have to thank you again, because I found this by re-reading the doc while writing this answer. This is where I saw the Presence.update that I initally missed :smiley:.

I might update the example repo with everything we discussed here in case someone is interested to see the result. If so, I will reply here to keep you up to date.

2 Likes

I’ve updated the sample repo
Here is what it looks like with 2 different accounts side-by-side:

4 Likes

I’m interested in learning more about your use case because my understanding is that streams are designed to handle append-only lists and optimize the rendering of updated data.

It seems what you are wanting to do is manage state.

I don’t think I would call that append-only since you can update and delete items using stream_insert and stream_delete, but maybe I don’t really understand the idea behind append-only.

I think the benefits of streams are :

  • Saving the server some memory since the data is not kept on the socket once rendered.
  • Avoiding unecessary requests of data since the list is updated using the items returned after insertions, thus avoiding the need to fetch the whole list again.

It is probably a bit overkill for simple use cases as the example I made with fruits.

I wanted to manage state related to the item actions. I wanted the UI to show when an item is being edited and therefore not available to be edited/deleted for other users because some of the items I manage takes a lot of time to edit, so I want to avoid users having their work overwriten because of concurrent editing.
This was a nice-to-have feature since I already check for that using Presence when the edit modal is loaded (if you try to load the edit page of an item that isn’t available by directly putting the URL, you will be redirected with an error message), but I think it gives a better user experience to directly see what item is available or not.

The way I did it before the streams implementation was not working, because the only way to update an item inside a stream is by inserting an updated version of the item.
In the table, each line including the actions is related to one item. So I added a virtual field to the struct the list was made of. This allows me to update the rendered actions state of each item.

From there, I thought I could add some sort of synced list feature, since everything to do so was already in place (Presence, Channel broadcast).
So each time an item is added/updated/deleted, I use the Presence diff event to update the list for every user on this liveview.
Normally, the other users would need to manually refresh the whole list to see the change.

1 Like