@@ -362,7 +362,9 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
362362 end
363363 end
364364
365- describe "#stream_react_component" do
365+ describe "#stream_react_component" do # rubocop:disable RSpec/MultipleMemoizedHelpers
366+ let ( :mocked_rails_stream ) { instance_double ( ActionController ::Live ::Buffer ) }
367+
366368 around do |example |
367369 # Wrap each test in Sync block to provide async context
368370 Sync do
@@ -378,6 +380,14 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
378380 end
379381 end
380382
383+ before do
384+ # Mock response.stream.closed? for client disconnect detection
385+ allow ( mocked_rails_stream ) . to receive ( :closed? ) . and_return ( false )
386+ mocked_rails_response = instance_double ( ActionDispatch ::Response )
387+ allow ( mocked_rails_response ) . to receive ( :stream ) . and_return ( mocked_rails_stream )
388+ allow ( self ) . to receive ( :response ) . and_return ( mocked_rails_response )
389+ end
390+
381391 it "returns the component shell that exist in the initial chunk with the consoleReplayScript" do
382392 mock_request_and_response
383393 initial_result = stream_react_component ( component_name , props : props , **component_options )
@@ -452,6 +462,38 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
452462 expect ( collected_chunks [ 1 ] ) . to include ( chunks_with_whitespaces [ 2 ] [ :html ] )
453463 expect ( collected_chunks [ 2 ] ) . to include ( chunks_with_whitespaces [ 3 ] [ :html ] )
454464 end
465+
466+ it "stops processing chunks when client disconnects" do
467+ many_chunks = Array . new ( 10 ) do |i |
468+ { html : "<div>Chunk #{ i } </div>" , consoleReplayScript : "" }
469+ end
470+ mock_request_and_response ( many_chunks )
471+
472+ # Simulate client disconnect after first chunk
473+ call_count = 0
474+ allow ( mocked_rails_stream ) . to receive ( :closed? ) do
475+ call_count += 1
476+ call_count > 1 # false for first call, true after
477+ end
478+
479+ # Start streaming - first chunk returned synchronously
480+ initial_result = stream_react_component ( component_name , props : props , **component_options )
481+ expect ( initial_result ) . to include ( "<div>Chunk 0</div>" )
482+
483+ # Wait for async task to complete
484+ @async_barrier . wait
485+ @main_output_queue . close
486+
487+ # Collect chunks that were enqueued to output
488+ collected_chunks = [ ]
489+ while ( chunk = @main_output_queue . dequeue )
490+ collected_chunks << chunk
491+ end
492+
493+ # Should have stopped early - not all chunks processed
494+ # The exact count depends on timing, but should be less than 9 (all remaining)
495+ expect ( collected_chunks . length ) . to be < 9
496+ end
455497 end
456498
457499 describe "stream_view_containing_react_components" do # rubocop:disable RSpec/MultipleMemoizedHelpers
@@ -476,6 +518,7 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
476518 written_chunks << chunk
477519 end
478520 allow ( mocked_stream ) . to receive ( :close )
521+ allow ( mocked_stream ) . to receive ( :closed? ) . and_return ( false )
479522 mocked_response = instance_double ( ActionDispatch ::Response )
480523 allow ( mocked_response ) . to receive ( :stream ) . and_return ( mocked_stream )
481524 allow ( self ) . to receive ( :response ) . and_return ( mocked_response )
@@ -565,6 +608,7 @@ def execute_stream_view_containing_react_components
565608 written_chunks . clear
566609 allow ( mocked_stream ) . to receive ( :write ) { |chunk | written_chunks << chunk }
567610 allow ( mocked_stream ) . to receive ( :close )
611+ allow ( mocked_stream ) . to receive ( :closed? ) . and_return ( false )
568612 mocked_response = instance_double ( ActionDispatch ::Response )
569613 allow ( mocked_response ) . to receive ( :stream ) . and_return ( mocked_stream )
570614 allow ( self ) . to receive ( :response ) . and_return ( mocked_response )
@@ -709,7 +753,9 @@ def run_stream
709753 end
710754 end
711755
712- describe "cached_stream_react_component integration with RandomValue" , :caching do
756+ describe "cached_stream_react_component integration with RandomValue" , :caching do # rubocop:disable RSpec/MultipleMemoizedHelpers
757+ let ( :mocked_stream ) { instance_double ( ActionController ::Live ::Buffer ) }
758+
713759 around do |example |
714760 original_prerender_caching = ReactOnRailsPro . configuration . prerender_caching
715761 ReactOnRailsPro . configuration . prerender_caching = true
@@ -720,6 +766,13 @@ def run_stream
720766 Rails . cache . clear
721767 end
722768
769+ before do
770+ allow ( mocked_stream ) . to receive ( :closed? ) . and_return ( false )
771+ mocked_response = instance_double ( ActionDispatch ::Response )
772+ allow ( mocked_response ) . to receive ( :stream ) . and_return ( mocked_stream )
773+ allow ( self ) . to receive ( :response ) . and_return ( mocked_response )
774+ end
775+
723776 # we need this setup because we can't use the helper outside of stream_view_containing_react_components
724777 def render_cached_random_value ( cache_key )
725778 # Streaming helpers require this context normally provided by stream_view_containing_react_components
@@ -780,6 +833,7 @@ def render_cached_random_value(cache_key)
780833 { html : "<div>Test Content</div>" , consoleReplayScript : "" }
781834 ]
782835 end
836+ let ( :mocked_stream ) { instance_double ( ActionController ::Live ::Buffer ) }
783837
784838 around do |example |
785839 Sync do
@@ -790,6 +844,12 @@ def render_cached_random_value(cache_key)
790844 end
791845
792846 before do
847+ # Mock response.stream.closed? for client disconnect detection
848+ allow ( mocked_stream ) . to receive ( :closed? ) . and_return ( false )
849+ mocked_response = instance_double ( ActionDispatch ::Response )
850+ allow ( mocked_response ) . to receive ( :stream ) . and_return ( mocked_stream )
851+ allow ( self ) . to receive ( :response ) . and_return ( mocked_response )
852+
793853 ReactOnRailsPro ::Request . instance_variable_set ( :@connection , nil )
794854 original_httpx_plugin = HTTPX . method ( :plugin )
795855 allow ( HTTPX ) . to receive ( :plugin ) do |*args |
0 commit comments