Best way for a client to opt out of a Turbo broadcast render when it was triggered by that browser?

Okay, I have the following scenario:

  • Browser A subscribes to a channel via a turbo_stream_from tag
  • Browser A changes some state in JS via an HTTP patch and receives the updated state
  • Browser B receives the state update via broadcast_render_to over action cable and updates the view :+1:
  • Browser A… also redundantly receives the same updated state over Action Cable

I’ve implemented broadcast here only for the case that a single user is juggling two tabs or devices (all of it is scoped to a user), so in practice, 99.9% of Action Cable traffic will be exclusively for Browser A above, with no Browser B/C/D/etc connected

In this scenario, is there a clear way or an established pattern for opting the current client out of that broadcast_render_to? I’m worried not just about the waste, but for the potential of a race condition (user clicks two things quickly and the Action Cable update overwrites one)

Interestingly, there’s already a mechanism for avoiding redundant refresh stream actions but it’s only used for refresh actions.

I’m guessing you’re not broadcasting refresh action but something else?

The way the mechanism works is that it generates a unique id, saves it locally in Turbo.session and sends it to the server in X-Turbo-Request-Id header, relevant code is here: turbo/src/http/fetch.js at 7ae954621ddfb36ecac96fda758a3f1d9cae065d · hotwired/turbo · GitHub

turbo-rails gem makes sure to to add it to the refresh action and then Turbo does nothing if it finds the id in recentRequests array, relevant code here: turbo/src/core/session.js at 7ae954621ddfb36ecac96fda758a3f1d9cae065d · hotwired/turbo · GitHub

I have a blog post that goes in a bit more detail on how refreshes work, maybe you find it useful: Turbo 8 morphing deep dive - how does it work? | Radan Skorić's personal site

Since X-Turbo-Request-Id header is being send with every Turbo request already and Turbo.session.recentRequests` is publicly accessible, you could probably reuse the mechanism.

If you’re using custom stream actions, you can add the logic there and if you’re using standard stream actions, you can wrap a standard one into a custom one with bouncing logic. Here’s an example from a hobby app where I’m doing that, maybe it helps: minesvshumanity/app/javascript/application.js at e87d380e0f46a43f4fa3ee7d1c0a3d5bf84ce2bd · radanskoric/minesvshumanity · GitHub

Thanks a lot for the reply @radanskoric! (And for the deep dive article – that helped me earlier this year)

You’re right in that I am using a custom stream action to update a few (large JSON) data attributes that is broadcast in an after_commit hook on a particular model.

If I’m reading you right, I could attach the Turbo UUID from the JS patch() Fetch request and then filter out the stream from the client later – but since all the stream does is overwrite a data attribute, they’ll be equivalent, so it’s a no-op. Rather, what I’m trying to do is to prevent the redundant broadcast back to the particular client that was responsible for the PATCH that led to the model’s being updated at all. The total payload can get to be pretty large and it’s wasteful to send it over the wire unnecessarily back to the browser that triggered the change

1 Like

Ahh, you want to prevent the broadcast itself. You were mentioning a race condition so I thought you want to avoid a stale update. I misunderstood.

I have no idea how you’d do that but I’m expecting it would be very hard. I don’t think ActionCable supports any kind of conditional broadcasting. Subscribed and authorised clients get everything broadcast on the channel. Adding logic to that would complicate the whole stack. I wouldn’t be surprised if you need to monkey patch a lot of things in ActionCable to make this work. If someone knows different, please share!

But, if the payload is so large that it’s worth it to avoid double sending, may I suggest flipping the logic? Don’t return anything in the initial response and rely on everyone, including the submitting client, receiving the broadcast?

How large is the payload typically?

As I wrote the last Reply I thought of another option: instead of broadcasting the payload, broadcast just a notification to fetch the fresh data. Then you’re brodcasting a tiny mesage and the sending client can ignore it, while the others fetch fresh data.

Yeah, that’s a good idea. I just implemented the XHR/Fetch anyway so that clients will start polling while the cable is disconnected, so I can just extend that here and only push down the FYI

For anyone who finds this via Google later, that means my implementation will look like:

  1. Browser A takes an action that updates a thing via a PATCH request, sending both the update AND the action cable subscriber ID, which I’m hacking at to get
  2. Server updates the thing (which I guess may as well add a updated_by_subscriber column), and then in an after_commit runs the broadcast_render_to, which will only render a stream with the signal clients should fetch + that subscriber ID
  3. Browser B fetches
  4. Browser A doesn’t fetch

Thanks for thinking this through with me!