Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions app/models/concerns/turbo/broadcastable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,20 @@ def broadcasts_refreshes_to(stream)
after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
end

# Same as <tt>#broadcasts_refreshes_to</tt>, but the designated stream for page refreshes is automatically set to
# the current model, for creates - to the model plural name, which can be overriden by passing <tt>stream</tt>.
# Same as <tt>#broadcasts_refreshes_to</tt>, but the designated stream for page refreshes is
# automatically set to the current model. For creates, the stream defaults to the model's plural
# name, which can be overridden by passing <tt>stream</tt>.
# If <tt>stream</tt> is a Symbol, a method with that name will be called to determine the stream.
# If <tt>stream</tt> is a Proc, it will be called with the model instance to determine the stream.
# If <tt>stream</tt> is explicitly set to nil, the after_create_commit callback will be skipped entirely.
def broadcasts_refreshes(stream = model_name.plural)
after_create_commit -> { broadcast_refresh_later_to(stream) }
after_create_commit do
case stream
when String then broadcast_refresh_later_to(stream)
when Symbol then broadcast_refresh_later_to(send(stream))
else broadcast_refresh_later_to(stream.try(:call, self))
Comment on lines +221 to +224
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this case statement aims to coerce the argument, but does not invoke different methods across the branches, what do you think about re-structuring it to extract out the method call?

Suggested change
case stream
when String then broadcast_refresh_later_to(stream)
when Symbol then broadcast_refresh_later_to(send(stream))
else broadcast_refresh_later_to(stream.try(:call, self))
stream = send(stream) if Symbol === stream
stream = stream.call(self) if stream.respond_to?(:call)
broadcast_refresh_later_to(stream)

end
end if stream
Comment on lines +220 to +226
Copy link
Contributor

@seanpdoyle seanpdoyle Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where explicitly passing nil bypasses broadcasting but still triggers the after_create_commit callback.

What is the intended behavior of supporting explicitly nil arguments? To me, invoking the after_create_commit seems to be an accidental side-effect.

Rather than suffixing the if stream conditional, would an ArgumentError make sense?

Suggested change
after_create_commit do
case stream
when String then broadcast_refresh_later_to(stream)
when Symbol then broadcast_refresh_later_to(send(stream))
else broadcast_refresh_later_to(stream.try(:call, self))
end
end if stream
raise ArgumentError, "stream is required…" if stream.nil?
after_create_commit do
case stream
when String then broadcast_refresh_later_to(stream)
when Symbol then broadcast_refresh_later_to(send(stream))
else broadcast_refresh_later_to(stream.try(:call, self))
end
end

after_update_commit -> { broadcast_refresh_later }
after_destroy_commit -> { broadcast_refresh }
end
Expand Down
3 changes: 3 additions & 0 deletions test/dummy/app/models/board.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
class Board < ApplicationRecord
broadcasts_refreshes
broadcasts_refreshes nil
broadcasts_refreshes :columns; def columns = [self, :columns]
broadcasts_refreshes ->(board) { [board, :cards] }
end
28 changes: 28 additions & 0 deletions test/streams/broadcastable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,34 @@ class Turbo::BroadcastableBoardTest < ActionCable::Channel::TestCase
end
end

test "creating a board broadcasts refreshes to a channel based on method call" do
board = Board.new(id: 1, name: "Board")
assert_broadcast_on "#{board.to_gid_param}:cards", turbo_stream_action_tag("refresh") do
perform_enqueued_jobs do
board.save!
Turbo::StreamsChannel.refresh_debouncer_for(["#{board.to_gid_param}:cards"]).wait
end
end
end

test "creating a board broadcasts refreshes to a channel based on proc evaluation" do
board = Board.new(id: 1, name: "Board")
assert_broadcast_on "#{board.to_gid_param}:columns", turbo_stream_action_tag("refresh") do
perform_enqueued_jobs do
board.save!
Turbo::StreamsChannel.refresh_debouncer_for(["#{board.to_gid_param}:columns"]).wait
end
end
end

test "creating a board enqueues 3 broadcast jobs instead of 4 skipping nil argument" do
assert_enqueued_jobs 3, only: Turbo::Streams::BroadcastStreamJob do
board = Board.create!(name: "Board")
streams = ["boards", "#{board.to_gid_param}:cards", "#{board.to_gid_param}:columns"]
streams.each { |stream| Turbo::StreamsChannel.refresh_debouncer_for(stream).wait }
end
end

test "updating a board broadcasts to the models channel" do
board = Board.suppressing_turbo_broadcasts do
Board.create!(name: "Hey")
Expand Down