react_on_rails 16.2.0.beta.3 → 16.2.0.beta.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -5
  3. data/CLAUDE.md +59 -0
  4. data/CONTRIBUTING.md +49 -1
  5. data/Gemfile.development_dependencies +1 -1
  6. data/Gemfile.lock +25 -10
  7. data/SWITCHING_CI_CONFIGS.md +55 -6
  8. data/Steepfile +51 -0
  9. data/bin/ci-rerun-failures +68 -22
  10. data/bin/ci-run-failed-specs +26 -2
  11. data/bin/ci-switch-config +262 -34
  12. data/bin/lefthook/check-trailing-newlines +2 -12
  13. data/bin/lefthook/eslint-lint +0 -10
  14. data/bin/lefthook/prettier-format +0 -10
  15. data/bin/lefthook/ruby-autofix +3 -6
  16. data/knip.ts +35 -9
  17. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +32 -52
  18. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +5 -1
  19. data/lib/react_on_rails/configuration.rb +56 -12
  20. data/lib/react_on_rails/controller.rb +3 -3
  21. data/lib/react_on_rails/dev/server_manager.rb +11 -4
  22. data/lib/react_on_rails/doctor.rb +249 -2
  23. data/lib/react_on_rails/helper.rb +12 -3
  24. data/lib/react_on_rails/pro_helper.rb +2 -44
  25. data/lib/react_on_rails/react_component/render_options.rb +7 -7
  26. data/lib/react_on_rails/utils.rb +40 -0
  27. data/lib/react_on_rails/version.rb +1 -1
  28. data/react_on_rails_pro/CHANGELOG.md +142 -29
  29. data/react_on_rails_pro/CONTRIBUTING.md +2 -13
  30. data/react_on_rails_pro/Gemfile.development_dependencies +1 -0
  31. data/react_on_rails_pro/Gemfile.lock +24 -3
  32. data/react_on_rails_pro/README.md +559 -38
  33. data/react_on_rails_pro/docs/code-splitting-loadable-components.md +1 -1
  34. data/react_on_rails_pro/docs/contributors-info/releasing.md +2 -2
  35. data/react_on_rails_pro/docs/installation.md +129 -109
  36. data/react_on_rails_pro/docs/node-renderer/basics.md +29 -22
  37. data/react_on_rails_pro/docs/node-renderer/error-reporting-and-tracing.md +8 -8
  38. data/react_on_rails_pro/docs/node-renderer/js-configuration.md +25 -23
  39. data/react_on_rails_pro/docs/node-renderer/troubleshooting.md +2 -0
  40. data/react_on_rails_pro/docs/updating.md +209 -15
  41. data/react_on_rails_pro/lib/react_on_rails_pro/concerns/stream.rb +58 -4
  42. data/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb +17 -3
  43. data/react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb +9 -9
  44. data/react_on_rails_pro/lib/react_on_rails_pro/request.rb +41 -25
  45. data/react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb +27 -7
  46. data/react_on_rails_pro/lib/react_on_rails_pro/utils.rb +3 -3
  47. data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
  48. data/react_on_rails_pro/package-scripts.yml +1 -1
  49. data/react_on_rails_pro/package.json +5 -8
  50. data/react_on_rails_pro/packages/node-renderer/src/integrations/api.ts +1 -1
  51. data/react_on_rails_pro/packages/node-renderer/src/master/restartWorkers.ts +39 -17
  52. data/react_on_rails_pro/packages/node-renderer/src/master.ts +15 -4
  53. data/react_on_rails_pro/packages/node-renderer/src/shared/configBuilder.ts +44 -5
  54. data/react_on_rails_pro/packages/node-renderer/src/shared/utils.ts +4 -2
  55. data/react_on_rails_pro/packages/node-renderer/src/worker/handleGracefulShutdown.ts +49 -0
  56. data/react_on_rails_pro/packages/node-renderer/src/worker/vm.ts +3 -3
  57. data/react_on_rails_pro/packages/node-renderer/src/worker.ts +5 -2
  58. data/react_on_rails_pro/packages/node-renderer/tests/helper.ts +8 -8
  59. data/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.js +1 -1
  60. data/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts +19 -19
  61. data/react_on_rails_pro/rakelib/public_key_management.rake +6 -5
  62. data/react_on_rails_pro/rakelib/rbs.rake +47 -0
  63. data/react_on_rails_pro/react_on_rails_pro.gemspec +1 -0
  64. data/react_on_rails_pro/sig/react_on_rails_pro/cache.rbs +13 -0
  65. data/react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs +100 -0
  66. data/react_on_rails_pro/sig/react_on_rails_pro/error.rbs +4 -0
  67. data/react_on_rails_pro/sig/react_on_rails_pro/utils.rbs +7 -0
  68. data/react_on_rails_pro/sig/react_on_rails_pro.rbs +5 -0
  69. data/react_on_rails_pro/spec/dummy/Gemfile.lock +23 -3
  70. data/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +3 -3
  71. data/react_on_rails_pro/spec/dummy/bin/dev +4 -8
  72. data/react_on_rails_pro/spec/dummy/client/node-renderer.js +4 -4
  73. data/react_on_rails_pro/spec/dummy/config/environments/production.rb +1 -1
  74. data/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb +28 -12
  75. data/react_on_rails_pro/spec/dummy/config.ru +1 -1
  76. data/react_on_rails_pro/spec/dummy/package.json +2 -2
  77. data/react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb +40 -11
  78. data/react_on_rails_pro/spec/dummy/spec/rails_helper.rb +1 -1
  79. data/react_on_rails_pro/spec/dummy/spec/requests/renderer_console_logging_spec.rb +5 -5
  80. data/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb +15 -10
  81. data/react_on_rails_pro/spec/dummy/spec/system/renderer_integration_spec.rb +3 -3
  82. data/react_on_rails_pro/spec/dummy/yarn.lock +4 -4
  83. data/react_on_rails_pro/spec/execjs-compatible-dummy/config/environments/production.rb +1 -1
  84. data/react_on_rails_pro/spec/execjs-compatible-dummy/config/initializers/react_on_rails.rb +16 -43
  85. data/react_on_rails_pro/spec/react_on_rails_pro/assets_precompile_spec.rb +15 -18
  86. data/react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb +1 -1
  87. data/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb +5 -3
  88. data/react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb +27 -12
  89. data/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb +0 -27
  90. data/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb +1 -1
  91. data/react_on_rails_pro/spec/react_on_rails_pro/stream_decorator_spec.rb +89 -0
  92. data/react_on_rails_pro/spec/react_on_rails_pro/stream_spec.rb +144 -0
  93. data/react_on_rails_pro/spec/react_on_rails_pro/support/caching.rb +1 -1
  94. data/react_on_rails_pro/spec/react_on_rails_pro/support/mock_block_helper.rb +4 -2
  95. data/sig/react_on_rails/controller.rbs +1 -1
  96. data/sig/react_on_rails/error.rbs +4 -0
  97. data/sig/react_on_rails/helper.rbs +2 -2
  98. data/sig/react_on_rails/json_parse_error.rbs +10 -0
  99. data/sig/react_on_rails/prerender_error.rbs +21 -0
  100. data/sig/react_on_rails/smart_error.rbs +28 -0
  101. data/sig/react_on_rails.rbs +3 -24
  102. metadata +14 -4
  103. data/lib/react_on_rails/pro_utils.rb +0 -37
  104. data/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/TestingStreamableComponent.jsx +0 -15
@@ -1228,10 +1228,6 @@
1228
1228
  dependencies:
1229
1229
  "@sentry/types" "7.120.0"
1230
1230
 
1231
- "@shakacode-tools/react-on-rails-pro-node-renderer@link:.yalc/@shakacode-tools/react-on-rails-pro-node-renderer":
1232
- version "0.0.0"
1233
- uid ""
1234
-
1235
1231
  "@shakacode/use-ssr-computation.macro@^1.2.4":
1236
1232
  version "1.2.4"
1237
1233
  resolved "https://registry.yarnpkg.com/@shakacode/use-ssr-computation.macro/-/use-ssr-computation.macro-1.2.4.tgz#b247d683e3133126dbdb42060c26e1c34a8b625d"
@@ -5461,6 +5457,10 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0:
5461
5457
  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
5462
5458
  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
5463
5459
 
5460
+ "react-on-rails-pro-node-renderer@link:.yalc/react-on-rails-pro-node-renderer":
5461
+ version "0.0.0"
5462
+ uid ""
5463
+
5464
5464
  "react-on-rails-pro@link:.yalc/react-on-rails-pro":
5465
5465
  version "0.0.0"
5466
5466
  uid ""
@@ -55,7 +55,7 @@ Rails.application.configure do
55
55
 
56
56
  # Log to STDOUT by default
57
57
  config.logger = ActiveSupport::Logger.new($stdout)
58
- .tap { |logger| logger.formatter = ::Logger::Formatter.new }
58
+ .tap { |logger| logger.formatter = Logger::Formatter.new }
59
59
  .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
60
60
 
61
61
  # Prepend all log lines with the following tags.
@@ -1,58 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # See https://github.com/shakacode/react_on_rails/blob/master/docs/guides/configuration.md
4
- # for many more options.
3
+ # ⚠️ TEST CONFIGURATION - Do not copy directly for production apps
4
+ # This is the ExecJS-compatible dummy app for testing legacy webpacker compatibility.
5
+ # See docs/api-reference/configuration.md for production configuration guidance.
5
6
 
6
7
  ReactOnRails.configure do |config|
7
- # This configures the script to run to build the production assets by webpack. Set this to nil
8
- # if you don't want react_on_rails building this file for you.
9
- # If nil, then the standard shakacode/webpacker assets:precompile will run
10
- # config.build_production_command = nil
11
-
12
- ################################################################################
13
8
  ################################################################################
14
- # TEST CONFIGURATION OPTIONS
15
- # Below options are used with the use of this test helper:
16
- # ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
9
+ # Essential Configuration
17
10
  ################################################################################
11
+ # Configure server bundle for server-side rendering
12
+ config.server_bundle_js_file = "server-bundle.js"
18
13
 
19
- # If you are using this in your spec_helper.rb (or rails_helper.rb):
20
- #
21
- # ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
22
- #
23
- # with rspec then this controls what yarn command is run
24
- # to automatically refresh your webpack assets on every test run.
25
- #
26
- # Alternately, you can remove the `ReactOnRails::TestHelper.configure_rspec_to_compile_assets`
27
- # and set the config/webpacker.yml option for test to true.
14
+ # Test configuration
28
15
  config.build_test_command = "RAILS_ENV=test bin/webpacker"
29
16
 
30
17
  ################################################################################
18
+ # File System Based Component Registry (Optional - Disabled for this test)
31
19
  ################################################################################
32
- # SERVER RENDERING OPTIONS
33
- ################################################################################
34
- # This is the file used for server rendering of React when using `(prerender: true)`
35
- # If you are never using server rendering, you should set this to "".
36
- # Note, there is only one server bundle, unlike JavaScript where you want to minimize the size
37
- # of the JS sent to the client. For the server rendering, React on Rails creates a pool of
38
- # JavaScript execution instances which should handle any component requested.
39
- #
40
- # While you may configure this to be the same as your client bundle file, this file is typically
41
- # different. You should have ONE server bundle which can create all of your server rendered
42
- # React components.
43
- #
44
- config.server_bundle_js_file = "server-bundle.js"
20
+ # Uncomment to enable automatic component registration:
21
+ # config.components_subdirectory = "ror_components"
22
+ # config.auto_load_bundle = true
23
+ config.auto_load_bundle = false
45
24
 
46
25
  ################################################################################
26
+ # Advanced Configuration
47
27
  ################################################################################
48
- # FILE SYSTEM BASED COMPONENT REGISTRY
49
- ################################################################################
50
- # `components_subdirectory` is the name of the matching directories that contain automatically registered components
51
- # for use in the Rails views. The default is nil, you can enable the feature by updating it in the next line.
52
- # config.components_subdirectory = "ror_components"
53
- #
54
- # For automated component registry, `render_component` view helper method tries to load bundle for component from
55
- # generated directory. default is false, you can pass option at the time of individual usage or update the default
56
- # in the following line
57
- config.auto_load_bundle = false
28
+ # Most options have sensible defaults. For advanced configuration including
29
+ # component loading strategies, server bundle security, and more, see:
30
+ # https://github.com/shakacode/react_on_rails/blob/master/docs/api-reference/configuration.md
58
31
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./spec_helper"
3
+ require_relative "spec_helper"
4
4
  require_relative "../../lib/react_on_rails_pro/assets_precompile"
5
5
 
6
6
  describe ReactOnRailsPro::AssetsPrecompile do
@@ -46,8 +46,6 @@ describe ReactOnRailsPro::AssetsPrecompile do
46
46
 
47
47
  ror_pro_config = instance_double(ReactOnRailsPro::Configuration)
48
48
 
49
- allow(ror_pro_config).to receive(:dependency_globs).and_return([expected_parameters.last])
50
-
51
49
  adapter = Module.new do
52
50
  def self.cache_keys
53
51
  %w[a b]
@@ -58,7 +56,8 @@ describe ReactOnRailsPro::AssetsPrecompile do
58
56
  end
59
57
  end
60
58
 
61
- allow(ror_pro_config).to receive(:remote_bundle_cache_adapter).and_return(adapter)
59
+ allow(ror_pro_config).to receive_messages(dependency_globs: [expected_parameters.last],
60
+ remote_bundle_cache_adapter: adapter)
62
61
 
63
62
  stub_const("ReactOnRailsPro::VERSION", "2.2.0")
64
63
 
@@ -136,11 +135,10 @@ describe ReactOnRailsPro::AssetsPrecompile do
136
135
  it "calls build_bundles & cache_bundles if cached bundles can't be fetched" do
137
136
  instance = described_class.instance
138
137
 
139
- allow(instance).to receive(:fetch_and_unzip_cached_bundles).and_return(false)
140
138
  expect(instance).to receive(:fetch_and_unzip_cached_bundles).once
141
- allow(instance).to receive(:build_bundles).and_return(nil)
142
139
  expect(instance).to receive(:build_bundles).once
143
- allow(instance).to receive(:cache_bundles).and_return(nil)
140
+ allow(instance).to receive_messages(fetch_and_unzip_cached_bundles: false, build_bundles: nil,
141
+ cache_bundles: nil)
144
142
  expect(instance).to receive(:cache_bundles).once
145
143
 
146
144
  instance.build_or_fetch_bundles
@@ -179,9 +177,9 @@ describe ReactOnRailsPro::AssetsPrecompile do
179
177
  unique_variable = { unique_key: "a unique value" }
180
178
 
181
179
  instance = described_class.instance
182
- allow(instance).to receive(:remote_bundle_cache_adapter).and_return(adapter_double)
183
- allow(instance).to receive(:zipped_bundles_filename).and_return(unique_variable)
184
- allow(instance).to receive(:zipped_bundles_filepath).and_return("zipped_bundles_filepath")
180
+ allow(instance).to receive_messages(remote_bundle_cache_adapter: adapter_double,
181
+ zipped_bundles_filename: unique_variable,
182
+ zipped_bundles_filepath: "zipped_bundles_filepath")
185
183
 
186
184
  allow(File).to receive(:binwrite).and_return(true)
187
185
  expect(File).to receive(:binwrite).once
@@ -197,8 +195,7 @@ describe ReactOnRailsPro::AssetsPrecompile do
197
195
  allow(File).to receive(:exist?).and_return(false)
198
196
 
199
197
  instance = described_class.instance
200
- allow(instance).to receive(:fetch_bundles).and_return(false)
201
- allow(instance).to receive(:zipped_bundles_filepath).and_return("a")
198
+ allow(instance).to receive_messages(fetch_bundles: false, zipped_bundles_filepath: "a")
202
199
 
203
200
  expect(instance.fetch_and_unzip_cached_bundles).to be(false)
204
201
  end
@@ -256,10 +253,10 @@ describe ReactOnRailsPro::AssetsPrecompile do
256
253
  zipped_bundles_filepath = Pathname.new(Dir.tmpdir).join("foobar")
257
254
 
258
255
  instance = described_class.instance
259
- allow(instance).to receive(:remote_bundle_cache_adapter).and_return(adapter_double)
260
- allow(instance).to receive(:zipped_bundles_filename).and_return("zipped_bundles_filename")
261
- allow(instance).to receive(:zipped_bundles_filepath).and_return(zipped_bundles_filepath)
262
- allow(instance).to receive(:remove_extra_files_cache_dir).and_return(nil)
256
+ allow(instance).to receive_messages(remote_bundle_cache_adapter: adapter_double,
257
+ zipped_bundles_filename: "zipped_bundles_filename",
258
+ zipped_bundles_filepath: zipped_bundles_filepath,
259
+ remove_extra_files_cache_dir: nil)
263
260
 
264
261
  expect(instance.cache_bundles).to be_truthy
265
262
 
@@ -289,8 +286,8 @@ describe ReactOnRailsPro::AssetsPrecompile do
289
286
 
290
287
  instance = described_class.instance
291
288
 
292
- allow(instance).to receive(:remote_bundle_cache_adapter).and_return(adapter)
293
- allow(instance).to receive(:extra_files_path).and_return(Pathname.new(Dir.pwd).join("extra_files_cache_dir"))
289
+ allow(instance).to receive_messages(remote_bundle_cache_adapter: adapter,
290
+ extra_files_path: Pathname.new(Dir.pwd).join("extra_files_cache_dir"))
294
291
  copied_gemfile_path = Pathname.new(Dir.pwd).join("extra_files_cache_dir", "Gemfile")
295
292
  copied_assets_precompile_path = Pathname.new(Dir.pwd).join("extra_files_cache_dir",
296
293
  "lib---react_on_rails_pro---assets_precompile.rb")
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./spec_helper"
3
+ require_relative "spec_helper"
4
4
 
5
5
  class TestingCache
6
6
  def call; end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "spec_helper"
4
4
 
5
- module ReactOnRailsPro
5
+ module ReactOnRailsPro # rubocop:disable Metrics/ModuleLength
6
6
  RSpec.describe Configuration do
7
7
  after do
8
8
  ReactOnRailsPro.instance_variable_set(:@configuration, nil)
@@ -205,7 +205,8 @@ module ReactOnRailsPro
205
205
 
206
206
  expect(ReactOnRailsPro.configuration.rsc_bundle_js_file).to eq("rsc-bundle.js")
207
207
  expect(ReactOnRailsPro.configuration.react_client_manifest_file).to eq("react-client-manifest.json")
208
- expect(ReactOnRailsPro.configuration.react_server_client_manifest_file).to eq("react-server-client-manifest.json")
208
+ expect(ReactOnRailsPro.configuration.react_server_client_manifest_file)
209
+ .to eq("react-server-client-manifest.json")
209
210
  end
210
211
 
211
212
  it "allows setting rsc_bundle_js_file" do
@@ -229,7 +230,8 @@ module ReactOnRailsPro
229
230
  config.react_server_client_manifest_file = "custom-server-client-manifest.json"
230
231
  end
231
232
 
232
- expect(ReactOnRailsPro.configuration.react_server_client_manifest_file).to eq("custom-server-client-manifest.json")
233
+ expect(ReactOnRailsPro.configuration.react_server_client_manifest_file)
234
+ .to eq("custom-server-client-manifest.json")
233
235
  end
234
236
 
235
237
  it "allows nil values for RSC configuration options" do
@@ -75,26 +75,30 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
75
75
  ENV["REACT_ON_RAILS_PRO_LICENSE"] = expired_token
76
76
  end
77
77
 
78
- context "in development/test environment" do
78
+ context "when in development/test environment" do
79
79
  before do
80
80
  allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development"))
81
81
  end
82
82
 
83
83
  it "raises error immediately" do
84
- expect { described_class.validated_license_data! }.to raise_error(ReactOnRailsPro::Error, /License has expired/)
84
+ expect do
85
+ described_class.validated_license_data!
86
+ end.to raise_error(ReactOnRailsPro::Error, /License has expired/)
85
87
  end
86
88
 
87
89
  it "includes FREE license information in error message" do
88
- expect { described_class.validated_license_data! }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
90
+ expect do
91
+ described_class.validated_license_data!
92
+ end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
89
93
  end
90
94
  end
91
95
 
92
- context "in production environment" do
96
+ context "when in production environment" do
93
97
  before do
94
98
  allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
95
99
  end
96
100
 
97
- context "within grace period (expired < 1 month ago)" do
101
+ context "with grace period (expired < 1 month ago)" do
98
102
  let(:expired_within_grace) do
99
103
  {
100
104
  sub: "test@example.com",
@@ -113,7 +117,8 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
113
117
  end
114
118
 
115
119
  it "logs warning with grace period remaining" do
116
- expect(mock_logger).to receive(:error).with(/WARNING:.*License has expired.*Grace period:.*day\(s\) remaining/)
120
+ expect(mock_logger).to receive(:error)
121
+ .with(/WARNING:.*License has expired.*Grace period:.*day\(s\) remaining/)
117
122
  described_class.validated_license_data!
118
123
  end
119
124
 
@@ -123,7 +128,7 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
123
128
  end
124
129
  end
125
130
 
126
- context "outside grace period (expired > 1 month ago)" do
131
+ context "when outside grace period (expired > 1 month ago)" do
127
132
  let(:expired_outside_grace) do
128
133
  {
129
134
  sub: "test@example.com",
@@ -138,11 +143,15 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
138
143
  end
139
144
 
140
145
  it "raises error" do
141
- expect { described_class.validated_license_data! }.to raise_error(ReactOnRailsPro::Error, /License has expired/)
146
+ expect do
147
+ described_class.validated_license_data!
148
+ end.to raise_error(ReactOnRailsPro::Error, /License has expired/)
142
149
  end
143
150
 
144
151
  it "includes FREE license information in error message" do
145
- expect { described_class.validated_license_data! }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
152
+ expect do
153
+ described_class.validated_license_data!
154
+ end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
146
155
  end
147
156
  end
148
157
  end
@@ -168,7 +177,9 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
168
177
  end
169
178
 
170
179
  it "includes FREE license information in error message" do
171
- expect { described_class.validated_license_data! }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
180
+ expect do
181
+ described_class.validated_license_data!
182
+ end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
172
183
  end
173
184
  end
174
185
 
@@ -180,11 +191,15 @@ RSpec.describe ReactOnRailsPro::LicenseValidator do
180
191
  end
181
192
 
182
193
  it "raises error" do
183
- expect { described_class.validated_license_data! }.to raise_error(ReactOnRailsPro::Error, /Invalid license signature/)
194
+ expect do
195
+ described_class.validated_license_data!
196
+ end.to raise_error(ReactOnRailsPro::Error, /Invalid license signature/)
184
197
  end
185
198
 
186
199
  it "includes FREE license information in error message" do
187
- expect { described_class.validated_license_data! }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
200
+ expect do
201
+ described_class.validated_license_data!
202
+ end.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
188
203
  end
189
204
  end
190
205
 
@@ -194,32 +194,5 @@ describe ReactOnRailsPro::Request do
194
194
  expect(mocked_block).not_to have_received(:call)
195
195
  end
196
196
  end
197
-
198
- it "does not use HTTPx retries plugin for streaming requests to prevent body duplication" do
199
- # This test verifies the fix for https://github.com/shakacode/react_on_rails/issues/1895
200
- # When streaming requests encounter connection errors mid-transmission, HTTPx retries
201
- # would cause body duplication because partial chunks are already sent to the client.
202
- # The StreamRequest class handles retries properly by starting fresh requests.
203
-
204
- # Reset connections to ensure we're using a fresh connection
205
- described_class.reset_connection
206
-
207
- # Trigger a streaming request
208
- mock_streaming_response(render_full_url, 200) do |yielder|
209
- yielder.call("Test chunk\n")
210
- end
211
-
212
- stream = described_class.render_code_as_stream("/render", "console.log('test');", is_rsc_payload: false)
213
- chunks = []
214
- stream.each_chunk { |chunk| chunks << chunk }
215
-
216
- # Verify that the streaming request completed successfully
217
- expect(chunks).to eq(["Test chunk"])
218
-
219
- # Verify that the connection_without_retries was created
220
- # by checking that a connection was created with retries disabled
221
- connection_without_retries = described_class.send(:connection_without_retries)
222
- expect(connection_without_retries).to be_a(HTTPX::Session)
223
- end
224
197
  end
225
198
  end
@@ -15,7 +15,7 @@ require "rails"
15
15
  require "rails/test_help"
16
16
  Rails.backtrace_cleaner.remove_silencers!
17
17
 
18
- require_relative "./simplecov_helper"
18
+ require_relative "simplecov_helper"
19
19
  # prevent Test::Unit's AutoRunner from executing during RSpec's rake task
20
20
  Test::Unit.run = true if defined?(Test::Unit) && Test::Unit.respond_to?(:run=)
21
21
 
@@ -62,4 +62,93 @@ RSpec.describe ReactOnRailsPro::StreamDecorator do
62
62
  expect(chunks.last).to end_with("-end")
63
63
  end
64
64
  end
65
+
66
+ describe "#rescue" do
67
+ it "catches the error happens inside the component" do
68
+ allow(mock_component).to receive(:each_chunk).and_raise(StandardError.new("Fake Error"))
69
+ mocked_block = mock_block
70
+
71
+ stream_decorator.rescue(&mocked_block.block)
72
+ chunks = []
73
+ expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
74
+
75
+ expect(mocked_block).to have_received(:call) do |error|
76
+ expect(error).to be_a(StandardError)
77
+ expect(error.message).to eq("Fake Error")
78
+ end
79
+ expect(chunks).to eq([])
80
+ end
81
+
82
+ it "catches the error happens inside subsequent component calls" do
83
+ allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(ArgumentError.new("Fake Error"))
84
+ mocked_block = mock_block
85
+
86
+ stream_decorator.rescue(&mocked_block.block)
87
+ chunks = []
88
+ expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
89
+
90
+ expect(mocked_block).to have_received(:call) do |error|
91
+ expect(chunks).to eq(["Chunk1"])
92
+ expect(error).to be_a(ArgumentError)
93
+ expect(error.message).to eq("Fake Error")
94
+ end
95
+ expect(chunks).to eq(["Chunk1"])
96
+ end
97
+
98
+ it "can yield values to the stream" do
99
+ allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(ArgumentError.new("Fake Error"))
100
+ mocked_block = mock_block
101
+
102
+ stream_decorator.rescue(&mocked_block.block)
103
+ chunks = []
104
+ expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
105
+
106
+ expect(mocked_block).to have_received(:call) do |error, &inner_block|
107
+ expect(chunks).to eq(["Chunk1"])
108
+ expect(error).to be_a(ArgumentError)
109
+ expect(error.message).to eq("Fake Error")
110
+
111
+ inner_block.call "Chunk from rescue block"
112
+ inner_block.call "Chunk2 from rescue block"
113
+ end
114
+ expect(chunks).to eq(["Chunk1", "Chunk from rescue block", "Chunk2 from rescue block"])
115
+ end
116
+
117
+ it "can convert the error into another error" do
118
+ allow(mock_component).to receive(:each_chunk).and_raise(StandardError.new("Fake Error"))
119
+ mocked_block = mock_block do |error|
120
+ expect(error).to be_a(StandardError)
121
+ expect(error.message).to eq("Fake Error")
122
+ raise ArgumentError, "Another Error"
123
+ end
124
+
125
+ stream_decorator.rescue(&mocked_block.block)
126
+ chunks = []
127
+ expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.to raise_error(ArgumentError, "Another Error")
128
+ expect(chunks).to eq([])
129
+ end
130
+
131
+ it "chains multiple rescue blocks" do
132
+ allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(StandardError.new("Fake Error"))
133
+ fist_rescue_block = mock_block do |error, &block|
134
+ expect(error).to be_a(StandardError)
135
+ expect(error.message).to eq("Fake Error")
136
+ block.call "Chunk from first rescue block"
137
+ raise ArgumentError, "Another Error"
138
+ end
139
+
140
+ second_rescue_block = mock_block do |error, &block|
141
+ expect(error).to be_a(ArgumentError)
142
+ expect(error.message).to eq("Another Error")
143
+ block.call "Chunk from second rescue block"
144
+ end
145
+
146
+ stream_decorator.rescue(&fist_rescue_block.block)
147
+ stream_decorator.rescue(&second_rescue_block.block)
148
+ chunks = []
149
+ expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
150
+
151
+ expect(chunks).to eq(["Chunk1", "Chunk from first rescue block", "Chunk from second rescue block"])
152
+ end
153
+ end
65
154
  end
@@ -1,7 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "async"
4
+ require "async/queue"
3
5
  require_relative "spec_helper"
4
6
 
7
+ class StreamController
8
+ include ReactOnRailsPro::Stream
9
+
10
+ attr_reader :response
11
+
12
+ def initialize(component_queues:, initial_response: "TEMPLATE")
13
+ @component_queues = component_queues
14
+ @initial_response = initial_response
15
+ end
16
+
17
+ def render_to_string(**_opts)
18
+ @rorp_rendering_fibers = @component_queues.map do |queue|
19
+ Fiber.new do
20
+ loop do
21
+ chunk = queue.dequeue
22
+ break if chunk.nil?
23
+
24
+ Fiber.yield chunk
25
+ end
26
+ end
27
+ end
28
+
29
+ @initial_response
30
+ end
31
+ end
32
+
5
33
  RSpec.describe "Streaming API" do
6
34
  let(:origin) { "http://api.example.com" }
7
35
  let(:path) { "/stream" }
@@ -342,4 +370,120 @@ RSpec.describe "Streaming API" do
342
370
  expect(mocked_block).to have_received(:call).with("First chunk")
343
371
  end
344
372
  end
373
+
374
+ describe "Component streaming concurrency" do
375
+ def run_stream(controller, template: "ignored")
376
+ Sync do |parent|
377
+ parent.async { controller.stream_view_containing_react_components(template: template) }
378
+ yield(parent)
379
+ end
380
+ end
381
+
382
+ def setup_stream_test(component_count: 2)
383
+ component_queues = Array.new(component_count) { Async::Queue.new }
384
+ controller = StreamController.new(component_queues: component_queues)
385
+
386
+ mocked_response = instance_double(ActionController::Live::Response)
387
+ mocked_stream = instance_double(ActionController::Live::Buffer)
388
+ allow(mocked_response).to receive(:stream).and_return(mocked_stream)
389
+ allow(mocked_stream).to receive(:write)
390
+ allow(mocked_stream).to receive(:close)
391
+ allow(controller).to receive(:response).and_return(mocked_response)
392
+
393
+ [component_queues, controller, mocked_stream]
394
+ end
395
+
396
+ it "streams components concurrently" do
397
+ queues, controller, stream = setup_stream_test
398
+
399
+ run_stream(controller) do |_parent|
400
+ queues[1].enqueue("B1")
401
+ sleep 0.05
402
+ expect(stream).to have_received(:write).with("B1")
403
+
404
+ queues[0].enqueue("A1")
405
+ sleep 0.05
406
+ expect(stream).to have_received(:write).with("A1")
407
+
408
+ queues[1].enqueue("B2")
409
+ queues[1].close
410
+ sleep 0.05
411
+
412
+ queues[0].enqueue("A2")
413
+ queues[0].close
414
+ sleep 0.1
415
+ end
416
+ end
417
+
418
+ it "maintains per-component ordering" do
419
+ queues, controller, stream = setup_stream_test
420
+
421
+ run_stream(controller) do |_parent|
422
+ queues[0].enqueue("X1")
423
+ queues[0].enqueue("X2")
424
+ queues[0].enqueue("X3")
425
+ queues[0].close
426
+
427
+ queues[1].enqueue("Y1")
428
+ queues[1].enqueue("Y2")
429
+ queues[1].close
430
+
431
+ sleep 0.2
432
+ end
433
+
434
+ # Verify all chunks were written
435
+ expect(stream).to have_received(:write).with("X1")
436
+ expect(stream).to have_received(:write).with("X2")
437
+ expect(stream).to have_received(:write).with("X3")
438
+ expect(stream).to have_received(:write).with("Y1")
439
+ expect(stream).to have_received(:write).with("Y2")
440
+ end
441
+
442
+ it "handles empty component list" do
443
+ _queues, controller, stream = setup_stream_test(component_count: 0)
444
+
445
+ run_stream(controller) do |_parent|
446
+ sleep 0.1
447
+ end
448
+
449
+ expect(stream).to have_received(:write).with("TEMPLATE")
450
+ expect(stream).to have_received(:close)
451
+ end
452
+
453
+ it "handles single component" do
454
+ queues, controller, stream = setup_stream_test(component_count: 1)
455
+
456
+ run_stream(controller) do |_parent|
457
+ queues[0].enqueue("Single1")
458
+ queues[0].enqueue("Single2")
459
+ queues[0].close
460
+
461
+ sleep 0.1
462
+ end
463
+
464
+ expect(stream).to have_received(:write).with("Single1")
465
+ expect(stream).to have_received(:write).with("Single2")
466
+ end
467
+
468
+ it "applies backpressure with slow writer" do
469
+ queues, controller, stream = setup_stream_test(component_count: 1)
470
+
471
+ write_timestamps = []
472
+ allow(stream).to receive(:write) do |_data|
473
+ write_timestamps << Process.clock_gettime(Process::CLOCK_MONOTONIC)
474
+ sleep 0.05
475
+ end
476
+
477
+ run_stream(controller) do |_parent|
478
+ 5.times { |i| queues[0].enqueue("Chunk#{i}") }
479
+ queues[0].close
480
+
481
+ sleep 1
482
+ end
483
+
484
+ expect(write_timestamps.length).to be >= 2
485
+ gaps = write_timestamps.each_cons(2).map { |a, b| b - a }
486
+ expect(gaps.all? { |gap| gap >= 0.04 }).to be true
487
+ end
488
+ end
345
489
  end
@@ -4,7 +4,7 @@ RSpec.configure do |config|
4
4
  config.before(:each, :caching) do
5
5
  cache_store = ActiveSupport::Cache::MemoryStore.new
6
6
  allow(controller).to receive(:cache_store).and_return(cache_store) if defined?(controller) && controller
7
- allow(::Rails).to receive(:cache).and_return(cache_store)
7
+ allow(Rails).to receive(:cache).and_return(cache_store)
8
8
  ReactOnRailsPro::Cache.instance_variable_set(:@serializer_checksum, nil)
9
9
  Rails.cache.clear
10
10
  end
@@ -9,9 +9,11 @@ module MockBlockHelper
9
9
  # mocked_block = mock_block
10
10
  # testing_method_taking_block(&mocked_block.block)
11
11
  # expect(mocked_block).to have_received(:call).with(1, 2, 3)
12
- def mock_block(return_value: nil)
12
+ def mock_block(&block)
13
13
  double("BlockMock").tap do |mock| # rubocop:disable RSpec/VerifiedDoubles
14
- allow(mock).to receive(:call) { return_value }
14
+ allow(mock).to receive(:call) do |*args, &inner_block|
15
+ block&.call(*args, &inner_block)
16
+ end
15
17
  def mock.block
16
18
  method(:call).to_proc
17
19
  end
@@ -5,7 +5,7 @@ module ReactOnRails
5
5
 
6
6
  def redux_store: (
7
7
  String store_name,
8
- ?props: untyped,
8
+ ?props: Hash[Symbol, untyped] | String,
9
9
  ?immediate_hydration: bool
10
10
  ) -> void
11
11