react_on_rails_pro 16.4.0.rc.6 → 16.4.0.rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +4 -4
  3. data/Gemfile.development_dependencies +1 -1
  4. data/Gemfile.lock +20 -18
  5. data/README.md +16 -16
  6. data/app/helpers/react_on_rails_pro_helper.rb +20 -8
  7. data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +16 -1
  8. data/lib/react_on_rails_pro/concerns/stream.rb +49 -12
  9. data/lib/react_on_rails_pro/engine.rb +58 -0
  10. data/lib/react_on_rails_pro/request.rb +10 -0
  11. data/lib/react_on_rails_pro/stream_request.rb +10 -1
  12. data/lib/react_on_rails_pro/version.rb +1 -1
  13. metadata +5 -39
  14. data/docs/bundle-caching.md +0 -219
  15. data/docs/caching.md +0 -246
  16. data/docs/code-splitting-loadable-components.md +0 -326
  17. data/docs/configuration.md +0 -165
  18. data/docs/contributors-info/onboarding-customers.md +0 -7
  19. data/docs/contributors-info/releasing.md +0 -41
  20. data/docs/contributors-info/style.md +0 -41
  21. data/docs/home-pro.md +0 -164
  22. data/docs/installation.md +0 -309
  23. data/docs/js-memory-leaks.md +0 -21
  24. data/docs/node-renderer/basics.md +0 -94
  25. data/docs/node-renderer/debugging.md +0 -42
  26. data/docs/node-renderer/error-reporting-and-tracing.md +0 -172
  27. data/docs/node-renderer/heroku.md +0 -101
  28. data/docs/node-renderer/js-configuration.md +0 -163
  29. data/docs/node-renderer/troubleshooting.md +0 -5
  30. data/docs/profiling-server-side-rendering-code.md +0 -180
  31. data/docs/react-server-components/add-streaming-and-interactivity.md +0 -190
  32. data/docs/react-server-components/create-without-ssr.md +0 -448
  33. data/docs/react-server-components/flight-protocol-syntax.md +0 -294
  34. data/docs/react-server-components/glossary.md +0 -121
  35. data/docs/react-server-components/how-react-server-components-work.md +0 -250
  36. data/docs/react-server-components/inside-client-components.md +0 -333
  37. data/docs/react-server-components/purpose-and-benefits.md +0 -253
  38. data/docs/react-server-components/rendering-flow.md +0 -90
  39. data/docs/react-server-components/selective-hydration-in-streamed-components.md +0 -75
  40. data/docs/react-server-components/server-side-rendering.md +0 -73
  41. data/docs/react-server-components/tutorial.md +0 -19
  42. data/docs/release-notes/4.0.md +0 -103
  43. data/docs/release-notes/v4-react-server-components.md +0 -66
  44. data/docs/ruby-api.md +0 -9
  45. data/docs/streaming-server-rendering.md +0 -208
  46. data/docs/troubleshooting.md +0 -3
  47. data/docs/updating.md +0 -273
  48. /data/app/views/react_on_rails_pro/{rsc_payload.html.erb → rsc_payload.text.erb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d41e1dcb19326994f54698beebb6194a198eb967beea7346b08a376fb86068fd
4
- data.tar.gz: 98ca3512787b5d444266eb0cac44420b6a991aab2d928305c8640c752b3981d3
3
+ metadata.gz: 717ead6ca9b9c1f7354820f063551a8403cabb9fafeb095bd46f35e3d368a483
4
+ data.tar.gz: 5062b202868b9106090becb6dd5f040761a2dd37ec2a1323053e43df111044ed
5
5
  SHA512:
6
- metadata.gz: a802fe1da5ebcd4571d4c75d5df49dec8d4894ac298c73e376f31ee34a12faaa0db4b78d137c96932354e366dfbc7ac168c79bcf911721f709e4f5ba52078592
7
- data.tar.gz: 31d4f3e670a195d169aa256876a02802341235c439dc9f815f8216c4db8bfbdca933d31acf8e69e845c2e88fa4e897cc49139e89a03a02379eca55acc91735dd
6
+ metadata.gz: b2fc8c9ad613776be17f834f8a7c2d7ff07b123e6e703b8dd21ffdc4687ee51d0037b49c813c7f7b9596266bfa15885336b1f6ce9df6530e995c68d58557d745
7
+ data.tar.gz: 025cf054e18a200c61ad22199617ce0374889b1f825290ee13952f02bb993532cf8b1738c0df4a7295c4b9bf576782bf42abe04395f9071dd46b0686697c8c5b
data/CONTRIBUTING.md CHANGED
@@ -47,12 +47,12 @@ From [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/
47
47
  ## Doc Changes
48
48
 
49
49
  When making doc changes, we want the change to work on both [the ShakaCode docs site](https://www.shakacode.com/react-on-rails-pro/docs/) and when browsing the GitHub repo.
50
- The issue is that the Shakacode site is generated only from files in [`docs`](./docs), so any references from them to non-doc files must use the full GitHub URL.
50
+ The issue is that the ShakaCode site is generated only from files in [`../docs/pro`](../docs/pro), so any references from them to non-doc files must use the full GitHub URL.
51
51
 
52
52
  ### Links to other docs:
53
53
 
54
54
  - When making references to doc files, use a relative URL path like:
55
- `[Installation Overview](docs/basics/installation-overview.md)`
55
+ `[Installation Guide](../docs/pro/installation.md)`
56
56
 
57
57
  - When making references to source code files, use a full url path like:
58
58
  `[spec/dummy/config/initializers/react_on_rails.rb](https://github.com/shakacode/react_on_rails/tree/master/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb)`
@@ -119,7 +119,7 @@ script/ci-changes-detector origin/master
119
119
  - Push Pro changes without testing locally first
120
120
  - Modify both Pro and main gem without running full tests
121
121
 
122
- For comprehensive CI documentation, see [`../docs/contributor-info/ci-optimization.md`](../docs/contributor-info/ci-optimization.md) in the repository root.
122
+ For comprehensive CI documentation, see [`../internal/contributor-info/ci-optimization.md`](../internal/contributor-info/ci-optimization.md) in the repository root.
123
123
 
124
124
  # IDE/Editor Setup
125
125
 
@@ -395,5 +395,5 @@ rake release[17.0.0,false,verdaccio]
395
395
 
396
396
  For complete documentation, see:
397
397
 
398
- - [Root Release Documentation](../docs/contributor-info/releasing.md)
398
+ - [Root Release Documentation](../internal/contributor-info/releasing.md)
399
399
  - Run `rake -D release` for inline help
@@ -8,7 +8,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
8
8
 
9
9
  gem "react_on_rails", path: "../"
10
10
 
11
- gem "shakapacker", "9.5.0"
11
+ gem "shakapacker", "9.6.1"
12
12
  gem "bootsnap", require: false
13
13
  gem "rails", "~> 7.1"
14
14
  gem "puma", "~> 6"
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: ..
11
11
  specs:
12
- react_on_rails (16.4.0.rc.6)
12
+ react_on_rails (16.4.0.rc.8)
13
13
  addressable
14
14
  connection_pool
15
15
  execjs (~> 2.5)
@@ -20,7 +20,7 @@ PATH
20
20
  PATH
21
21
  remote: .
22
22
  specs:
23
- react_on_rails_pro (16.4.0.rc.6)
23
+ react_on_rails_pro (16.4.0.rc.8)
24
24
  addressable
25
25
  async (>= 2.6)
26
26
  connection_pool
@@ -29,7 +29,7 @@ PATH
29
29
  httpx (~> 1.5)
30
30
  jwt (~> 2.7)
31
31
  rainbow
32
- react_on_rails (= 16.4.0.rc.6)
32
+ react_on_rails (= 16.4.0.rc.8)
33
33
 
34
34
  GEM
35
35
  remote: https://rubygems.org/
@@ -165,7 +165,7 @@ GEM
165
165
  drb (2.2.3)
166
166
  equivalent-xml (0.6.0)
167
167
  nokogiri (>= 1.4.3)
168
- erb (6.0.1)
168
+ erb (6.0.2)
169
169
  erubi (1.13.1)
170
170
  execjs (2.10.0)
171
171
  fakefs (2.8.0)
@@ -194,8 +194,9 @@ GEM
194
194
  concurrent-ruby (~> 1.0)
195
195
  io-console (0.8.2)
196
196
  io-event (1.14.2)
197
- irb (1.16.0)
197
+ irb (1.17.0)
198
198
  pp (>= 0.6.0)
199
+ prism (>= 1.3.0)
199
200
  rdoc (>= 4.0.0)
200
201
  reline (>= 0.4.2)
201
202
  jbuilder (2.12.0)
@@ -230,7 +231,8 @@ GEM
230
231
  method_source (1.1.0)
231
232
  metrics (0.15.0)
232
233
  mini_mime (1.1.5)
233
- minitest (6.0.1)
234
+ minitest (6.0.2)
235
+ drb (~> 2.0)
234
236
  prism (~> 1.5)
235
237
  mize (0.4.1)
236
238
  protocol (~> 2.0)
@@ -247,11 +249,11 @@ GEM
247
249
  net-smtp (0.5.1)
248
250
  net-protocol
249
251
  nio4r (2.7.5)
250
- nokogiri (1.19.0-arm64-darwin)
252
+ nokogiri (1.19.1-arm64-darwin)
251
253
  racc (~> 1.4)
252
- nokogiri (1.19.0-x86_64-darwin)
254
+ nokogiri (1.19.1-x86_64-darwin)
253
255
  racc (~> 1.4)
254
- nokogiri (1.19.0-x86_64-linux-gnu)
256
+ nokogiri (1.19.1-x86_64-linux-gnu)
255
257
  racc (~> 1.4)
256
258
  package_json (0.2.0)
257
259
  parallel (1.27.0)
@@ -262,7 +264,7 @@ GEM
262
264
  pp (0.6.3)
263
265
  prettyprint
264
266
  prettyprint (0.2.0)
265
- prism (1.6.0)
267
+ prism (1.9.0)
266
268
  protocol (2.0.0)
267
269
  ruby_parser (~> 3.0)
268
270
  pry (0.14.2)
@@ -282,7 +284,7 @@ GEM
282
284
  puma (6.5.0)
283
285
  nio4r (~> 2.0)
284
286
  racc (1.8.1)
285
- rack (3.2.4)
287
+ rack (3.2.5)
286
288
  rack-proxy (0.7.7)
287
289
  rack
288
290
  rack-session (2.1.1)
@@ -310,8 +312,8 @@ GEM
310
312
  activesupport (>= 5.0.0)
311
313
  minitest
312
314
  nokogiri (>= 1.6)
313
- rails-html-sanitizer (1.6.2)
314
- loofah (~> 2.21)
315
+ rails-html-sanitizer (1.7.0)
316
+ loofah (~> 2.25)
315
317
  nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
316
318
  railties (7.2.3)
317
319
  actionpack (= 7.2.3)
@@ -330,7 +332,7 @@ GEM
330
332
  ffi (~> 1.0)
331
333
  rbs (3.9.5)
332
334
  logger
333
- rdoc (7.1.0)
335
+ rdoc (7.2.0)
334
336
  erb
335
337
  psych (>= 4.0.0)
336
338
  tsort
@@ -414,9 +416,9 @@ GEM
414
416
  rexml (~> 3.2, >= 3.2.5)
415
417
  rubyzip (>= 1.2.2, < 3.0)
416
418
  websocket (~> 1.0)
417
- semantic_range (3.1.0)
419
+ semantic_range (3.1.1)
418
420
  sexp_processor (4.17.1)
419
- shakapacker (9.5.0)
421
+ shakapacker (9.6.1)
420
422
  activesupport (>= 5.2)
421
423
  package_json
422
424
  rack-proxy (>= 0.6.1)
@@ -483,7 +485,7 @@ GEM
483
485
  xpath (3.2.0)
484
486
  nokogiri (~> 1.8)
485
487
  yard (0.9.36)
486
- zeitwerk (2.7.4)
488
+ zeitwerk (2.7.5)
487
489
 
488
490
  PLATFORMS
489
491
  arm64-darwin-24
@@ -531,7 +533,7 @@ DEPENDENCIES
531
533
  sass-rails
532
534
  scss_lint
533
535
  selenium-webdriver (= 4.9.0)
534
- shakapacker (= 9.5.0)
536
+ shakapacker (= 9.6.1)
535
537
  spring
536
538
  spring-watcher-listen
537
539
  sprockets
data/README.md CHANGED
@@ -156,7 +156,7 @@ end %>
156
156
  - Automatic cache invalidation based on props
157
157
  - Works with Rails fragment caching infrastructure
158
158
 
159
- **📖 Learn more**: [docs/caching.md](./docs/caching.md)
159
+ **📖 Learn more**: [docs/pro/caching.md](../docs/pro/caching.md)
160
160
 
161
161
  ### 2. Prerender Caching
162
162
 
@@ -175,7 +175,7 @@ end
175
175
  - Caches across multiple requests
176
176
  - Complements fragment caching for maximum performance
177
177
 
178
- **📖 Learn more**: [docs/caching.md](./docs/caching.md)
178
+ **📖 Learn more**: [docs/pro/caching.md](../docs/pro/caching.md)
179
179
 
180
180
  ### 3. React on Rails Pro Node Renderer
181
181
 
@@ -203,7 +203,7 @@ reactOnRailsProNodeRenderer({
203
203
  });
204
204
  ```
205
205
 
206
- **📖 Learn more**: [docs/node-renderer/basics.md](./docs/node-renderer/basics.md)
206
+ **📖 Learn more**: [docs/pro/node-renderer/basics.md](../docs/pro/node-renderer/basics.md)
207
207
 
208
208
  ### 4. React Server Components (RSC)
209
209
 
@@ -236,7 +236,7 @@ Speed up webpack rebuilds by caching unchanged bundles.
236
236
  - **Faster development**: Hot reload only what changed
237
237
  - **Lower costs**: Reduce build server time
238
238
 
239
- **📖 Learn more**: [docs/bundle-caching.md](./docs/bundle-caching.md)
239
+ **📖 Learn more**: [docs/pro/bundle-caching.md](../docs/pro/bundle-caching.md)
240
240
 
241
241
  ### 6. Global State Management
242
242
 
@@ -249,7 +249,7 @@ ReactOnRailsPro.configure do |config|
249
249
  end
250
250
  ```
251
251
 
252
- **📖 Learn more**: [docs/configuration.md](./docs/configuration.md)
252
+ **📖 Learn more**: [docs/pro/configuration.md](../docs/pro/configuration.md)
253
253
 
254
254
  ---
255
255
 
@@ -326,9 +326,9 @@ rails console
326
326
 
327
327
  ### Next Steps
328
328
 
329
- - **Enable caching**: See [docs/caching.md](./docs/caching.md)
330
- - **Set up Node Renderer**: See [docs/node-renderer/basics.md](./docs/node-renderer/basics.md)
331
- - **Optimize performance**: See [docs/configuration.md](./docs/configuration.md)
329
+ - **Enable caching**: See [docs/pro/caching.md](../docs/pro/caching.md)
330
+ - **Set up Node Renderer**: See [docs/pro/node-renderer/basics.md](../docs/pro/node-renderer/basics.md)
331
+ - **Optimize performance**: See [docs/pro/configuration.md](../docs/pro/configuration.md)
332
332
  - **Set up for your team**: See [LICENSE_SETUP.md](./LICENSE_SETUP.md#team-setup)
333
333
 
334
334
  ---
@@ -337,20 +337,20 @@ rails console
337
337
 
338
338
  ### Installation & Setup
339
339
 
340
- - **[Installation Guide](./docs/installation.md)** - Detailed installation instructions
340
+ - **[Installation Guide](../docs/pro/installation.md)** - Detailed installation instructions
341
341
  - **[License Setup](./LICENSE_SETUP.md)** - Complete license configuration guide
342
- - **[Configuration Reference](./docs/configuration.md)** - All configuration options
342
+ - **[Configuration Reference](../docs/pro/configuration.md)** - All configuration options
343
343
 
344
344
  ### Features
345
345
 
346
- - **[Caching Guide](./docs/caching.md)** - Fragment and prerender caching
347
- - **[Bundle Caching](./docs/bundle-caching.md)** - Speed up webpack builds
348
- - **[Node Renderer Basics](./docs/node-renderer/basics.md)** - Standalone Node.js server
349
- - **[Node Renderer Configuration](./docs/node-renderer/js-configuration.md)** - JavaScript config
346
+ - **[Caching Guide](../docs/pro/caching.md)** - Fragment and prerender caching
347
+ - **[Bundle Caching](../docs/pro/bundle-caching.md)** - Speed up webpack builds
348
+ - **[Node Renderer Basics](../docs/pro/node-renderer/basics.md)** - Standalone Node.js server
349
+ - **[Node Renderer Configuration](../docs/pro/node-renderer/js-configuration.md)** - JavaScript config
350
350
 
351
351
  ### API Reference
352
352
 
353
- - **[Ruby API](./docs/ruby-api.md)** - Helper methods and utilities
353
+ - **[Ruby API](../docs/pro/ruby-api.md)** - Helper methods and utilities
354
354
  - **[CHANGELOG](./CHANGELOG.md)** - Version history and upgrade notes
355
355
 
356
356
  ### Upgrading
@@ -408,7 +408,7 @@ Check out these production applications using React on Rails Pro:
408
408
 
409
409
  - **📧 Email Support**: [support@shakacode.com](mailto:support@shakacode.com)
410
410
  - **💼 Sales & Licensing**: [justin@shakacode.com](mailto:justin@shakacode.com)
411
- - **📖 Documentation**: [docs/](./docs/)
411
+ - **📖 Documentation**: [docs/pro/](../docs/pro/)
412
412
  - **🐛 Found a Bug?**: Email [support@shakacode.com](mailto:support@shakacode.com) (for Pro customers)
413
413
 
414
414
  ### Professional Services
@@ -314,7 +314,13 @@ module ReactOnRailsProHelper
314
314
 
315
315
  # Enqueue remaining chunks asynchronously
316
316
  @async_barrier.async do
317
- rest_chunks.each { |chunk| @main_output_queue.enqueue(chunk) }
317
+ rest_chunks.each do |chunk|
318
+ break if response.stream.closed?
319
+
320
+ @main_output_queue.enqueue(chunk)
321
+ end
322
+ rescue Async::Queue::ClosedError
323
+ # Queue closed due to error/disconnect in another component — stop enqueuing
318
324
  end
319
325
 
320
326
  # Return first chunk directly
@@ -436,8 +442,8 @@ module ReactOnRailsProHelper
436
442
  # Start an async task on the barrier to stream all chunks
437
443
  @async_barrier.async do
438
444
  stream = yield
439
- process_stream_chunks(stream, first_chunk_var, all_chunks)
440
- on_complete&.call(all_chunks)
445
+ fully_consumed = process_stream_chunks(stream, first_chunk_var, all_chunks)
446
+ on_complete&.call(all_chunks) if fully_consumed
441
447
  rescue StandardError => e
442
448
  # Propagate the error to the calling fiber via the variable.
443
449
  # Async::Variable can only be resolved once — if it was already resolved
@@ -457,9 +463,9 @@ module ReactOnRailsProHelper
457
463
  end
458
464
  end
459
465
 
460
- # Wait for and return the first chunk (blocking)
461
- first_chunk_var.wait
462
- result = first_chunk_var.value
466
+ # Wait for and return the first chunk (blocking).
467
+ # Async::Variable#wait blocks until resolved, then returns the stored value.
468
+ result = first_chunk_var.wait
463
469
 
464
470
  # If the async task stored an exception (pre-first-chunk error), raise it now.
465
471
  # This happens BEFORE response.stream.write(template_string) in
@@ -470,12 +476,17 @@ module ReactOnRailsProHelper
470
476
  result
471
477
  end
472
478
 
479
+ # Returns true if the stream was fully consumed, false if aborted (client disconnect).
480
+ # When false, callers must NOT invoke on_complete to avoid caching partial data.
473
481
  def process_stream_chunks(stream, first_chunk_var, all_chunks)
474
482
  is_first = true
475
483
 
476
484
  stream.each_chunk do |chunk|
477
- # Check if client disconnected before processing chunk
478
- break if response.stream.closed?
485
+ # Client disconnected abort without caching partial results
486
+ if response.stream.closed?
487
+ first_chunk_var.value = nil if is_first
488
+ return false
489
+ end
479
490
 
480
491
  all_chunks&.push(chunk)
481
492
 
@@ -491,6 +502,7 @@ module ReactOnRailsProHelper
491
502
 
492
503
  # Handle case where stream has no chunks
493
504
  first_chunk_var.value = nil if is_first
505
+ true
494
506
  end
495
507
 
496
508
  def internal_stream_react_component(component_name, options = {})
@@ -15,7 +15,22 @@ module ReactOnRailsPro
15
15
 
16
16
  stream_view_containing_react_components(
17
17
  template: custom_rsc_payload_template,
18
- layout: false
18
+ layout: false,
19
+ # Render as text so Rails does not inject HTML view annotation comments
20
+ # into the NDJSON stream. Custom template overrides must resolve to a
21
+ # text or format-neutral template, not `.html.erb`.
22
+ formats: [:text],
23
+ content_type: "application/x-ndjson"
24
+ )
25
+ rescue ActionView::MissingTemplate => e
26
+ raise e.exception(
27
+ "[React on Rails Pro] RSC payload templates are now rendered with format :text. " \
28
+ "If you override `custom_rsc_payload_template`, make sure the override resolves to " \
29
+ "a text or format-neutral template (for example `rsc_payload.text.erb`) instead of " \
30
+ "only `.html.erb`. See\n" \
31
+ "https://github.com/shakacode/react_on_rails/blob/master/docs/pro/updating.md " \
32
+ "for upgrade notes.\n\n" \
33
+ "Original error: #{e.message}"
19
34
  )
20
35
  end
21
36
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "English"
4
+
3
5
  module ReactOnRailsPro
4
6
  module Stream
5
7
  extend ActiveSupport::Concern
@@ -12,6 +14,10 @@ module ReactOnRailsPro
12
14
  #
13
15
  # @param template [String] The path to the template file to be streamed.
14
16
  # @param close_stream_at_end [Boolean] Whether to automatically close the stream after rendering (default: true).
17
+ # @param content_type [String, nil] Optional response content type. Set after rendering but before the first
18
+ # stream write, overriding any content type inferred from the template format. When using
19
+ # a non-HTML `formats:` value (for example `[:text]`), pass `content_type` too unless
20
+ # committing the format-derived MIME type is intentional.
15
21
  # @param render_options [Hash] Additional options to pass to `render_to_string`.
16
22
  #
17
23
  # components must be added to the view using the `stream_react_component` helper.
@@ -30,10 +36,13 @@ module ReactOnRailsPro
30
36
  # For more details, refer to `lib/react_on_rails/helper.rb` in the react_on_rails repository.
31
37
  #
32
38
  # @see ReactOnRails::Helper#stream_react_component
33
- def stream_view_containing_react_components(template:, close_stream_at_end: true, **render_options)
39
+ def stream_view_containing_react_components(
40
+ template:, close_stream_at_end: true, content_type: nil, **render_options
41
+ )
34
42
  require "async"
35
43
  require "async/barrier"
36
44
  require "async/limited_queue"
45
+ warn_on_non_html_formats_without_content_type(render_options[:formats], content_type)
37
46
 
38
47
  Sync do |parent_task|
39
48
  # Initialize async primitives for concurrent component streaming
@@ -48,6 +57,11 @@ module ReactOnRailsPro
48
57
  # View may contain extra newlines, chunk already contains a newline
49
58
  # Having multiple newlines between chunks causes hydration errors
50
59
  # So we strip extra newlines from the template string and add a single newline
60
+ # `formats: [:text]` causes render_to_string to set response.content_type
61
+ # to `text/plain`; override it here before the first stream write, which
62
+ # is when ActionController::Live commits headers. render_to_string itself
63
+ # never writes to response.stream, so this assignment is always safe.
64
+ response.content_type = content_type if content_type
51
65
  response.stream.write(template_string)
52
66
 
53
67
  drain_streams_concurrently(parent_task)
@@ -80,8 +94,6 @@ module ReactOnRailsPro
80
94
  # - Barrier is stopped to cancel all producer tasks, preventing wasted work
81
95
  # - No exception propagates to the controller for client disconnects
82
96
  def drain_streams_concurrently(parent_task)
83
- client_disconnected = false
84
-
85
97
  writing_task = parent_task.async do
86
98
  # Drain all remaining chunks from the queue to the response stream
87
99
  while (chunk = @main_output_queue.dequeue)
@@ -89,8 +101,13 @@ module ReactOnRailsPro
89
101
  end
90
102
  rescue IOError, Errno::EPIPE => e
91
103
  # Client disconnected - stop writing gracefully
92
- client_disconnected = true
93
104
  log_client_disconnect("writer", e)
105
+ ensure
106
+ # Cancel all producers when writer exits for ANY reason (normal completion,
107
+ # client disconnect, or unexpected error). Prevents deadlock where producers
108
+ # block on enqueue to a full queue that nobody is consuming.
109
+ # Idempotent — no-op if barrier tasks already completed.
110
+ @async_barrier.stop
94
111
  end
95
112
 
96
113
  # Wait for all component streaming tasks to complete
@@ -101,16 +118,22 @@ module ReactOnRailsPro
101
118
  raise e
102
119
  end
103
120
  ensure
104
- # Close the queue first to unblock writing_task (it may be waiting on dequeue)
105
- @main_output_queue.close
121
+ # Capture the primary exception (if any) BEFORE any cleanup that could raise.
122
+ # In an ensure block, $ERROR_INFO holds the exception currently propagating
123
+ # out of the method (nil if returning normally). We must snapshot it before
124
+ # the begin/rescue below, where $ERROR_INFO would reflect the caught exception.
125
+ primary_exception = $ERROR_INFO
106
126
 
107
- # Wait for writing_task to ensure client_disconnected flag is set
108
- # before we check it (fixes race condition where ensure runs before
109
- # writing_task's rescue block sets the flag)
110
- writing_task.wait
127
+ # Close the queue to unblock writing_task (it may be waiting on dequeue)
128
+ @main_output_queue.close
111
129
 
112
- # If client disconnected, stop all producer tasks to avoid wasted work
113
- @async_barrier.stop if client_disconnected
130
+ # Wait for writing_task to finish. Wrap in rescue to avoid masking a primary
131
+ # exception (e.g., producer error) with a secondary writing_task exception.
132
+ begin
133
+ writing_task.wait
134
+ rescue StandardError
135
+ raise unless primary_exception
136
+ end
114
137
  end
115
138
 
116
139
  def log_client_disconnect(context, exception)
@@ -120,5 +143,19 @@ module ReactOnRailsPro
120
143
  "[React on Rails Pro] Client disconnected during streaming (#{context}): #{exception.class}"
121
144
  end
122
145
  end
146
+
147
+ def warn_on_non_html_formats_without_content_type(formats, content_type)
148
+ return if content_type.present?
149
+
150
+ requested_formats = Array(formats).compact.map(&:to_sym)
151
+ return if requested_formats.empty? || requested_formats.all?(:html)
152
+
153
+ Rails.logger.warn(
154
+ "[React on Rails Pro] stream_view_containing_react_components received non-HTML formats " \
155
+ "#{requested_formats.inspect} without `content_type:`. Rails will commit the format-derived " \
156
+ "MIME type (for example `text/plain` for `:text`). Pass `content_type:` explicitly when " \
157
+ "streaming non-HTML responses."
158
+ )
159
+ end
123
160
  end
124
161
  end
@@ -7,8 +7,10 @@ module ReactOnRailsPro
7
7
  LICENSE_URL = "https://www.shakacode.com/react-on-rails-pro/"
8
8
  # TODO: Remove this legacy migration warning path after 16.5.0 stable release (target: 2026-05-31).
9
9
  LEGACY_LICENSE_FILE = "config/react_on_rails_pro_license.key"
10
+ RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = ["Rack::Deflater"].freeze
10
11
  private_constant :LICENSE_URL
11
12
  private_constant :LEGACY_LICENSE_FILE
13
+ private_constant :RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS
12
14
 
13
15
  initializer "react_on_rails_pro.routes" do
14
16
  ActionDispatch::Routing::Mapper.include ReactOnRailsPro::Routes
@@ -20,6 +22,10 @@ module ReactOnRailsPro
20
22
  config.after_initialize { ReactOnRailsPro::Engine.log_license_status }
21
23
  end
22
24
 
25
+ initializer "react_on_rails_pro.check_rsc_streaming_middleware" do
26
+ config.after_initialize { ReactOnRailsPro::Engine.log_rsc_streaming_middleware_warning }
27
+ end
28
+
23
29
  class << self
24
30
  def log_license_status
25
31
  status = ReactOnRailsPro::LicenseValidator.license_status
@@ -40,6 +46,24 @@ module ReactOnRailsPro
40
46
  end
41
47
  end
42
48
 
49
+ def log_rsc_streaming_middleware_warning
50
+ return unless ReactOnRailsPro.configuration.enable_rsc_support
51
+ return if Rails.env.test?
52
+
53
+ middleware_names = middleware_stack_names
54
+ problematic = RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS & middleware_names
55
+ return if problematic.empty?
56
+
57
+ route_path = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
58
+ Rails.logger.warn(
59
+ "[React on Rails Pro] React Server Components support is enabled and the middleware " \
60
+ "stack includes #{problematic.join(', ')}. Compression and other response-transforming " \
61
+ "middleware can interfere with ActionController::Live NDJSON streaming. If your " \
62
+ "`#{route_path}` payload route is not already exempt, consider bypassing " \
63
+ "#{problematic.join(', ')} for that endpoint if you see stalled or corrupted RSC payloads."
64
+ )
65
+ end
66
+
43
67
  private
44
68
 
45
69
  def log_valid_license
@@ -105,6 +129,40 @@ module ReactOnRailsPro
105
129
  Rails.logger.info message
106
130
  end
107
131
  end
132
+
133
+ def middleware_stack_names
134
+ middleware_stack = Rails.application&.middleware
135
+ return [] unless middleware_stack
136
+
137
+ entries =
138
+ if middleware_stack.respond_to?(:middlewares)
139
+ middleware_stack.middlewares
140
+ elsif middleware_stack.respond_to?(:to_a)
141
+ middleware_stack.to_a
142
+ else
143
+ Array(middleware_stack)
144
+ end
145
+
146
+ entries.filter_map { |entry| middleware_entry_name(entry) }.uniq
147
+ end
148
+
149
+ def middleware_entry_name(entry)
150
+ candidate =
151
+ if entry.respond_to?(:klass) && entry.klass
152
+ entry.klass
153
+ elsif entry.is_a?(Array)
154
+ entry.first
155
+ else
156
+ entry
157
+ end
158
+
159
+ case candidate
160
+ when Module
161
+ candidate.name
162
+ else
163
+ candidate.to_s.presence
164
+ end
165
+ end
108
166
  end
109
167
  end
110
168
  end
@@ -319,6 +319,16 @@ module ReactOnRailsPro
319
319
  end
320
320
 
321
321
  response = HTTPX.get(path)
322
+ error = response.error
323
+ if error
324
+ # Re-raise via rescue so Ruby sets error.cause for exception chaining.
325
+ begin
326
+ raise error
327
+ rescue StandardError
328
+ raise ReactOnRailsPro::Error, "Failed to fetch dev-server asset from #{path}: #{error}"
329
+ end
330
+ end
331
+
322
332
  response.body
323
333
  else
324
334
  Pathname.new(path)
@@ -113,7 +113,7 @@ module ReactOnRailsPro
113
113
 
114
114
  def process_response_chunks(stream_response, error_body)
115
115
  loop_response_lines(stream_response) do |chunk|
116
- if stream_response.is_a?(HTTPX::ErrorResponse) || stream_response.status >= 400
116
+ if response_has_error_status?(stream_response)
117
117
  error_body << chunk
118
118
  next
119
119
  end
@@ -123,6 +123,15 @@ module ReactOnRailsPro
123
123
  end
124
124
  end
125
125
 
126
+ def response_has_error_status?(response)
127
+ return true if response.is_a?(HTTPX::ErrorResponse)
128
+
129
+ response.status >= 400
130
+ rescue NoMethodError
131
+ # HTTPX::StreamResponse can fail to delegate #status for non-streaming errors.
132
+ true
133
+ end
134
+
126
135
  def handle_http_error(error, error_body, send_bundle)
127
136
  response = error.response
128
137
  case response.status
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRailsPro
4
- VERSION = "16.4.0.rc.6"
4
+ VERSION = "16.4.0.rc.8"
5
5
  PROTOCOL_VERSION = "2.0.0"
6
6
  end