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
user.broadcast_update_to(
campaign.user,
:campaign_list_items,
target: "target_name",
partial: "partial_path",
max_retries: 5
)