Proposal: Frame reloads through Streams

We are starting to use Hotwire in our application. The idea is to use a combination of frames and streams to keep the information on the users screen up to date. Our application is multi tenant and one tenant can have multiple users with different permissions. Streaming all updates to all users is therefore not possible.

Our current approach involves starting multiple streams for a user based on their permissions. When we broadcast changes for an entity, we need to make very sure we broadcast to the right stream to prevent data leaking to unauthorised users.

I believe there is an opportunity for Turbo to make this situation easier and less prone to errors. Turbo Frames are already capable of loading their content. You can even reload the content by setting the url attribute. What if a Turbo Stream is capable of reloading the content of a frame:

<turbo-stream action="reload" target="dom_id">
</turbo-stream>

This has several advantages:

  1. With multiple users for a tenant, you can still have one stream per tenant. The dom_id doesn’t leak sensitive information.
  2. We leverage the permission checks done in the initial request. When a dom_id is on the page, we can safely assume the information can also be reloaded without leaking information. Furthermore, the request to the frame’s URL will do another permission check.
  3. When broadcasting an entity after a change, you never know if the user is on a page with the entity present. With lazy loading frames, the user might be on the right page, but the content might never need loading. By postponing the rendering to the frame request, we never render and broadcast unused HTML.

There are some downsides as well:

  1. Instead of only the websocket broadcast, one or more extra HTTP requests are needed to reload the frames on a page. This can cause a slight delay in reloading the content on the page.
  2. Not all frames have a src attribute because some are rendered inline. This can be tackled by allowing a src attribute and marking the frame as loaded:
<turbo-frame id="message_123" src="/messages/123" loaded="true">
  Real message content, not a placeholder
</turbo-frame>
  1. Some reloadable frames might not be exposed by a URL. Broadcasting changes doesn’t require a URL for each entity, reloading through frames does.

I’m looking forward to hear if frame reloads through streams is interesting enough to add to Turbo. I’d love to open a PR with the changes!

2 Likes

What about creating user channels where it makes sense instead of tenant channels?

Would there be a limit to the number of such channels possible? It seems like you might exhaust the hosting pretty quickly, because you are combining N channels and X users.

Walter

User channels are a viable alternative. Still I need to have business logic in place to determine which user receives which update, whereas in my proposal I can leverage existing code to handle this for me.

I needed to dig into the ActionCable internals to try to answer this. We use a Redis backend. ActionCable uses Redis pub/sub and creates a channel for each channel I create. So our N tenants with 1 to 5 permissions gives us 5*N channels. I can’t find any limitations on Redis channel usage, so the number of channels doesn’t seem to be a reason to avoid our approach. Not entirely sure though.

What about simply wrapping a turbo-frame update in the turbo-stream?

<turbo-stream action="replace" target="message_123">
  <template>
    <turbo-frame id="message_123" src="/messages/123">
      Updating...
    </turbo-frame>
  </template>
</turbo-stream>
1 Like

Nice trick, that would probably do the trick. Only downside is the temporary “Updating…” placeholder that might cause some flickering in the UI.

Do you think this should be part of Turbo?

Turbo frame pulls content, turbo stream replaces content. I don’t know that the need to be combined.

To avoid the flicker, add one more layer in loading the content.

INITIAL PAGE HTML:

<div id="message">

</div>

<turbo-frame id="message_frame" src="/messages/123" style="display: none;"></turbo-frame>

URL TURBO FRAME URL RESPONSE (/messages/123)

<turbo-stream id="message_frame" style="display: none;">
  <template>
    <turbo-frame id="message">
      Mesagges
    </turbo-frame>
  </template>
</turbo-stream>

WEBSOCKET UPDATE

<turbo-stream action="replace" target="message_123">
  <template>
    <turbo-frame id="message_frame" src="/messages/123" style="display: none;">
      Updating...
    </turbo-frame>
  </template>
</turbo-stream>

If this were a part of turbo if it was more more generic. Meaning, updating attributes on an element rather than replace the entire element.

I agree with your proposal.

I have turbo_frame_tag that shows counts of tasks. It is lazy loaded via src attribute. I need an easy way to trigger reload for this. And there is nothing right now that would make it easy.

I can do it with after_update_commit, but that would mean that i would have to have two separate partials. One with turbo_frame_tag src=path and one without src=path.

If you try to lazy load turbo_frame_tag with src attribute and the response comes back with another turbo_frame_tag with src attribute, it will just keep loading it in a loop.

Needless to say, work arounds and hacks aren’t a good idea. The solution, I think is to have the reload action for turbo_stream. That would just trigger a reload on a turbo_frame.