22
33require "async"
44require "async/queue"
5+ require "async/barrier"
56require "rails_helper"
67require "support/script_tag_utils"
78
@@ -360,13 +361,19 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
360361 end
361362
362363 describe "#stream_react_component" do
363- before do
364- # Initialize @rorp_rendering_fibers to mock the behavior of stream_view_containing_react_components.
365- # This instance variable is normally set by stream_view_containing_react_components method.
366- # By setting it here, we simulate that the view is being rendered using that method.
367- # This setup is necessary because stream_react_component relies on @rorp_rendering_fibers
368- # to function correctly within the streaming context.
369- @rorp_rendering_fibers = [ ]
364+ around do |example |
365+ # Wrap each test in Sync block to provide async context
366+ Sync do
367+ # Initialize async primitives to mock the behavior of stream_view_containing_react_components.
368+ # These instance variables are normally set by stream_view_containing_react_components method.
369+ # By setting them here, we simulate that the view is being rendered using that method.
370+ # This setup is necessary because stream_react_component relies on @async_barrier and @main_output_queue
371+ # to function correctly within the streaming context.
372+ @async_barrier = Async ::Barrier . new
373+ @main_output_queue = Async ::Queue . new
374+
375+ example . run
376+ end
370377 end
371378
372379 it "returns the component shell that exist in the initial chunk with the consoleReplayScript" do
@@ -380,35 +387,39 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
380387 expect ( initial_result ) . to include ( wrapped )
381388 end
382389 expect ( initial_result ) . not_to include ( "More content" , "Final content" )
383- expect ( chunks_read . count ) . to eq ( 1 )
390+ # Note: With async architecture, chunks are consumed in background immediately,
391+ expect ( chunks_read . count ) . to eq ( 3 )
384392 end
385393
386- it "creates a fiber to read subsequent chunks " do
394+ it "streams subsequent chunks to the output queue " do
387395 mock_request_and_response
388- stream_react_component ( component_name , props : props , **component_options )
389- expect ( @rorp_rendering_fibers . count ) . to eq ( 1 ) # rubocop:disable RSpec/InstanceVariable
390- fiber = @rorp_rendering_fibers . first # rubocop:disable RSpec/InstanceVariable
391- expect ( fiber ) . to be_alive
392-
393- second_result = fiber . resume
394- # regex that matches the html and wrapped consoleReplayScript
395- # Note: consoleReplayScript is now wrapped in a script tag with id="consoleReplayLog"
396+ initial_result = stream_react_component ( component_name , props : props , **component_options )
397+
398+ # First chunk is returned synchronously
399+ expect ( initial_result ) . to include ( react_component_div_with_initial_chunk )
400+
401+ # Wait for async task to complete
402+ @async_barrier . wait
403+ @main_output_queue . close
404+
405+ # Subsequent chunks should be in the output queue
406+ collected_chunks = [ ]
407+ while ( chunk = @main_output_queue . dequeue )
408+ collected_chunks << chunk
409+ end
410+
411+ # Should have received the remaining chunks (chunks 2 and 3)
412+ expect ( collected_chunks . length ) . to eq ( 2 )
413+
414+ # Verify second chunk content
396415 script = chunks [ 1 ] [ :consoleReplayScript ]
397416 wrapped = script . present? ? "<script id=\" consoleReplayLog\" >#{ script } </script>" : ""
398- expect ( second_result ) . to match (
417+ expect ( collected_chunks [ 0 ] ) . to match (
399418 /#{ Regexp . escape ( chunks [ 1 ] [ :html ] ) } \s +#{ Regexp . escape ( wrapped ) } /
400419 )
401- expect ( second_result ) . not_to include ( "Stream React Server Components" , "Final content" )
402- expect ( chunks_read . count ) . to eq ( 2 )
403-
404- third_result = fiber . resume
405- expect ( third_result ) . to eq ( chunks [ 2 ] [ :html ] . to_s )
406- expect ( third_result ) . not_to include ( "Stream React Server Components" , "More content" )
407- expect ( chunks_read . count ) . to eq ( 3 )
408420
409- expect ( fiber . resume ) . to be_nil
410- expect ( fiber ) . not_to be_alive
411- expect ( chunks_read . count ) . to eq ( chunks . count )
421+ # Verify third chunk content
422+ expect ( collected_chunks [ 1 ] ) . to eq ( chunks [ 2 ] [ :html ] . to_s )
412423 end
413424
414425 it "does not trim whitespaces from html" do
@@ -420,12 +431,24 @@ def mock_request_and_response(mock_chunks = chunks, count: 1)
420431 { html : "\t \t \t <div>Chunk 4: with mixed whitespaces</div> \n \n \n " }
421432 ] . map { |chunk | chunk . merge ( consoleReplayScript : "" ) }
422433 mock_request_and_response ( chunks_with_whitespaces )
434+
423435 initial_result = stream_react_component ( component_name , props : props , **component_options )
424436 expect ( initial_result ) . to include ( chunks_with_whitespaces . first [ :html ] )
425- fiber = @rorp_rendering_fibers . first # rubocop:disable RSpec/InstanceVariable
426- expect ( fiber . resume ) . to include ( chunks_with_whitespaces [ 1 ] [ :html ] )
427- expect ( fiber . resume ) . to include ( chunks_with_whitespaces [ 2 ] [ :html ] )
428- expect ( fiber . resume ) . to include ( chunks_with_whitespaces [ 3 ] [ :html ] )
437+
438+ # Wait for async task to complete
439+ @async_barrier . wait
440+ @main_output_queue . close
441+
442+ # Collect remaining chunks from queue
443+ collected_chunks = [ ]
444+ while ( chunk = @main_output_queue . dequeue )
445+ collected_chunks << chunk
446+ end
447+
448+ # Verify whitespaces are preserved in all chunks
449+ expect ( collected_chunks [ 0 ] ) . to include ( chunks_with_whitespaces [ 1 ] [ :html ] )
450+ expect ( collected_chunks [ 1 ] ) . to include ( chunks_with_whitespaces [ 2 ] [ :html ] )
451+ expect ( collected_chunks [ 2 ] ) . to include ( chunks_with_whitespaces [ 3 ] [ :html ] )
429452 end
430453 end
431454
@@ -698,15 +721,25 @@ def run_stream
698721 # we need this setup because we can't use the helper outside of stream_view_containing_react_components
699722 def render_cached_random_value ( cache_key )
700723 # Streaming helpers require this context normally provided by stream_view_containing_react_components
701- @rorp_rendering_fibers = [ ]
724+ result = nil
725+ Sync do
726+ @async_barrier = Async ::Barrier . new
727+ @main_output_queue = Async ::Queue . new
702728
703- result = cached_stream_react_component ( "RandomValue" , cache_key : cache_key ,
704- id : "RandomValue-react-component-0" ) do
705- { a : 1 , b : 2 }
706- end
729+ result = cached_stream_react_component ( "RandomValue" , cache_key : cache_key ,
730+ id : "RandomValue-react-component-0" ) do
731+ { a : 1 , b : 2 }
732+ end
733+
734+ # Complete the streaming lifecycle to trigger cache writes
735+ @async_barrier . wait
736+ @main_output_queue . close
707737
708- # Complete the streaming lifecycle to trigger cache writes
709- @rorp_rendering_fibers . each { |fiber | fiber . resume while fiber . alive? } # rubocop:disable RSpec/InstanceVariable
738+ # Drain the queue
739+ while ( chunk = @main_output_queue . dequeue )
740+ # Just consume all remaining chunks
741+ end
742+ end
710743
711744 result
712745 end
@@ -746,8 +779,15 @@ def render_cached_random_value(cache_key)
746779 ]
747780 end
748781
782+ around do |example |
783+ Sync do
784+ @async_barrier = Async ::Barrier . new
785+ @main_output_queue = Async ::Queue . new
786+ example . run
787+ end
788+ end
789+
749790 before do
750- @rorp_rendering_fibers = [ ]
751791 ReactOnRailsPro ::Request . instance_variable_set ( :@connection , nil )
752792 original_httpx_plugin = HTTPX . method ( :plugin )
753793 allow ( HTTPX ) . to receive ( :plugin ) do |*args |
0 commit comments