One workaround (hack) might be to load the turbo-frame with a blank src, and then use a stimulus controller and update the URL once “all” the streams are connected.
That’s an interesting idea indeed. I’ll see if I can wire something like that up.
I might suggest posting this issue to the hotwire/turbo github issues. It seems you might benefit from a way to detect when a specific turbo-stream has successfully connected.
I’ll re-post there as well - I was reading that it might be better to post here first
Also I started working on some potential server side code to detect if a specific channel has a connection. I was thinking maybe I can delay the broadcast for a set period of time until the connect has been made, then potentially ignore if after a period passes.
Here’s what I came up with on the server side that could go into the worker…
streamables = [@campaign.user, "campaign_list_items"]
signed_stream_name = Turbo::StreamsChannel.signed_stream_name(streamables)
verified_stream_name = Turbo::StreamsChannel.verified_stream_name(signed_stream_name)
redis = ActionCable.server.pubsub.redis_connection_for_subscriptions
while within_time_window do # haven't spent to much time on the loop here (this is pseudo code)
# redis.pubsub("channels", verified_stream_name).present? is a valid check that returns true once the connection is made on the client.
if redis.pubsub("channels", verified_stream_name).present?
campaign.user.broadcast_update_to(
campaign.user,
:campaign_list_items,
target: "campaign_#{campaign.id}",
partial: "some_partial")
else
redo
end
end
I also tried your other suggestion here but the unfortunate part is that I’m always getting a return of true.
Back to the drawing board.
My next thing I’m going to try is to see if I can manually open a connection like I used to with ActionCable directly using createConsumer and it’s connected callback.
I decided to give the server implementation one more chance and I think I found a possible solution that I don’t hate that should be quite easy to reuse for other methods like this.
<%= turbo_stream_from [current_user, :campaign_list_items] %>
<% @campaigns.each do |campaign| %>
<%= turbo_frame_tag dom_id(campaign), src: campaign_list_items_path(campaign.id) do %>
<div class="grid grid-cols-7 p-3 bt"> </div>
<% end %>
</div>
class RetriableBroadcaster
def initialize(broadcast_method, *streamables, **options)
@broadcast_method = broadcast_method
@streamables = streamables
@options = options
end
def call(attempts = 0)
return broadcast if client_connected?
return unless attempts <= max_retries
attempts += 1
sleep wait_for
call(attempts)
end
private
def broadcast
@streamables.first.public_send(
@broadcast_method,
*@streamables,
**@options.except(:max_retries, :wait_for)
)
end
def wait_for
@options[:wait_for].presence || 0.1
end
def max_retries
@options[:max_retries].presence || 5
end
def client_connected?
redis.pubsub("channels", verified_stream_name).present?
end
def redis
@redis ||= ActionCable.server.pubsub.redis_connection_for_subscriptions
end
def signed_stream_name
Turbo::StreamsChannel.signed_stream_name(@streamables)
end
def verified_stream_name
Turbo::StreamsChannel.verified_stream_name(signed_stream_name)
end
end
module Campaigns
class ListItemStatsWorker
include Sidekiq::Worker
sidekiq_options queue: :within_30_seconds, retry: 0
def perform(campaign_id)
return unless (campaign = Campaign.find(campaign_id))
RetriableBroadcaster.new(
:broadcast_update_to,
campaign.user,
:campaign_list_items,
target: "campaign_#{campaign.id}",
partial: "dashboard/campaigns/list_items/list_item",
locals: {
campaign: campaign, stats: CampaignStatsPresenter.new(campaign, nil, nil)
},
max_retries: 5
).call
end
end
end
Ideally we could build this into source and provide a signature more like
I cloned a repo that uses the default turbo channel connection library and inspected the <turbo-cable-stream-source element in chrome. Doing so reveals all sorts of properties you can use to check state.
What I’ve been doing for expensive loading pages is try the lazy loading turbo frame, and having an animation that makes it appear like work is happening. This might make more sense, especially if your background job is in the 1-3 seconds consistently. It looks like that’s what you ended up down, but I may be misreading the code.
I’ve got a sample here (Adding Loading Screen with Turbo • Blogging On Rails) but this makes the dashboard load fast, and you arent fighting a race condition. It’s also simpler from a technical perspective, because you don’t need to add any more front end javascript code.
Jumping in here because I have a jQuery loader in a production app that I’ve been longing to replace.
In the lazy frame scenario, are there any events I could hook into that would replace jQuery’s after success callback? I need to run some processing on the returned remote content, but it only makes sense to do that if that remote content (HTML – sometimes many megabytes) is fully loaded.
Thanks in advance,
Walter
Ye Olde Code:
<script>
$('#preview').load('<%= @title.html.file.url %>', function(){
var anchor = window.location.hash.toString().split('#')[1];
if(!!anchor){
setTimeout(function(){
window.scrollTo({left: $('#' + anchor).offset()['left'], top: $('#' + anchor).offset()['top'] - 20});
},8); // this timing is not waiting for the success function to load the html, but for the DOM to settle
}
});
</script>
I believe this allows this to be quite reusable for one or many different turbo_frames. It’s been in production for a couple days now and we no longer have any issues.
Thanks again for everyone that’s provided help one this one
Interesting solution!
I think you can use the disabled attribute and let the default src do his job. Also, I checked every turbo-cable-stream-source connections, but maybe it’s unnecessary.