I often seem to have a model where on creation, the user sees a placeholder page with a spinner, a background job fills in some data on that model, and then the placeholder page refreshes with the latest data. Might be generating a slow data report, or contacting an external service to notify it about the creation, or whatever.
Say we’re creating a BlogPost, and need to fill in BlogPost#short_url
from some-url-generator.com. After creation, a background job is queued up and we render blog_posts/show.html to the user:
<div id="<%= dom_id(blog_post) %>">
Title: <%= blog_post.title %>
Short URL:
<% if blog_post.short_url %>
<%= blog_post.short_url %>
<% else %>
loading...
<% end %>
</div>
<%= turbo_stream_from blog_post %>
A few seconds later, the background job completes and updates the page via broadcast_replace_to
, showing our amazing new short-url.
The problem comes when the background job completes too fast, after we’ve rendered show.html but before the user has received the response and started streaming. broadcast_replace_to is just going into a black hole with no subscribers, and the user is sat staring at the “loading…” text forever.
I’ve run into this a couple of times and never found a good fix. My awful workaround at the moment is this:
- def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, **rendering)
+ def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, attributes: {}, repeat_after: nil, **rendering)
Turbo::Streams::ActionBroadcastJob.perform_later \
stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering
+ if repeat_after
+ Turbo::Streams::ActionBroadcastJob.set(wait: repeat_after).perform_later \
+ stream_name_from(streamables), action: action, target: target, targets: targets, attributes: attributes, **rendering
+ end
end
which adds a repeat_after option to Turbo::Broadcastable. This broadcasts the replacement immediately as normal, and then sends a duplicate replacement after a second or two to make sure the client has had chance to set up its subscription.
Ideally turbo-rails might be able to maintain a short-lived buffer in redis so that any missed messages could be replayed, but actually implementing that seems kind of hard. Any other suggestions, or do I need to just give up on turbo-streams for this and go back to old-fashioned polling?