react_on_rails_pro 16.2.0.beta.11 → 16.2.0.test.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b6739f19bed16cd9ee87ddefa25d330e2fdc02d93d9e9f509248a7cb015dc1c
4
- data.tar.gz: 9de8c07648f2716cd207bdd0cff6f0c6e4f0e86bc0d21bd4ca7435544b179fae
3
+ metadata.gz: a0112c934965b40a6275bd05fe1309924b21f087f2ff0b8874ca4540dd40d4f7
4
+ data.tar.gz: ed5b0b8d9fc888529bacd29b900221b99e28544204dd016fe0d3449803f865b7
5
5
  SHA512:
6
- metadata.gz: 302b9ee59324e7408f410d1e7a910bb590e50d5dd7bffa69d59c45223254091cbedbe5d8a4670bd7bc4365af352d387330a222ed6feef216cd03e80d3421a6e2
7
- data.tar.gz: 2e64288aec2b1ea624ba056c066dddf540a89c27b1b5d8cc35a4b97a2d19eb665d3f6dea6624908987f4d2baddd71f1f5e472e0ba537713cf001ad8302604bd1
6
+ metadata.gz: 14688a5ec648396a4160bcf77a775df3a382ff8103cfed7f57e2b78a7f780e43c3c83bb3d0a5d4125005b39531845cd928fcfb255c032a9f2e5e53710ec15fe2
7
+ data.tar.gz: c675bd301c26d46d07d01f51750d4e4f6bc7c480e05f9ac1362a2730fbd30c857f1a51a714c68326c943d93135a4c90c9c5be53589ed1b1b1d75f9048eafaa32
data/.gitignore CHANGED
@@ -48,7 +48,7 @@ junit.xml
48
48
 
49
49
  node_modules
50
50
 
51
- /packages/node-renderer/tests/tmp
51
+ /../packages/react-on-rails-pro-node-renderer/tests/tmp
52
52
 
53
53
  yarn-debug.*
54
54
  spec/dummy/client/yarn-debug.log*
data/.prettierignore CHANGED
@@ -1,6 +1,5 @@
1
1
  node_modules
2
2
  **/node_modules/**
3
- packages/node-renderer/tests/fixtures/projects
4
3
  **/tmp
5
4
  **/public
6
5
  **/package.json
data/CHANGELOG.md CHANGED
@@ -20,13 +20,13 @@ You can find the **package** version numbers from this repo's tags and below in
20
20
  _Add changes in master not yet tagged._
21
21
 
22
22
  ### Improved
23
- - Significantly improved streaming performance by processing React components concurrently instead of sequentially. This reduces latency and improves responsiveness when using `stream_view_containing_react_components`.
24
23
 
25
- ### Added
26
- - Added `config.concurrent_component_streaming_buffer_size` configuration option to control the memory buffer size for concurrent component streaming (defaults to 64). This allows fine-tuning of memory usage vs. performance for streaming applications.
24
+ - **Concurrent Streaming Performance**: Implemented concurrent draining of streamed React components using the async gem. Instead of processing components sequentially, the system now uses a producer-consumer pattern with bounded buffering to allow multiple components to stream simultaneously while maintaining per-component chunk ordering. This significantly reduces latency and improves responsiveness when using `stream_view_containing_react_components`. [PR 2015](https://github.com/shakacode/react_on_rails/pull/2015) by [ihabadham](https://github.com/ihabadham).
27
25
 
28
26
  ### Added
29
27
 
28
+ - **Async React Component Rendering**: Added `async_react_component` and `cached_async_react_component` helpers for concurrent component rendering. Multiple components now execute HTTP requests to the Node renderer in parallel instead of sequentially, significantly reducing latency when rendering multiple components in a view. Requires `ReactOnRailsPro::AsyncRendering` concern in controller. [PR 2139](https://github.com/shakacode/react_on_rails/pull/2139) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
29
+ - Added `config.concurrent_component_streaming_buffer_size` configuration option to control the memory buffer size for concurrent component streaming (defaults to 64). This allows fine-tuning of memory usage vs. performance for streaming applications.
30
30
  - Added `cached_stream_react_component` helper method, similar to `cached_react_component` but for streamed components.
31
31
  - **License Validation System**: Implemented comprehensive JWT-based license validation with offline verification using RSA-256 signatures. License validation occurs at startup in both Ruby and Node.js environments. Supports required fields (`sub`, `iat`, `exp`) and optional fields (`plan`, `organization`, `iss`). FREE evaluation licenses are available for 3 months at [shakacode.com/react-on-rails-pro](https://shakacode.com/react-on-rails-pro). [PR #1857](https://github.com/shakacode/react_on_rails/pull/1857) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
32
32
  - **Pro-Specific Configurations Moved from Open-Source**: The following React Server Components (RSC) configurations are now exclusively in the Pro gem and should be configured in `ReactOnRailsPro.configure`:
@@ -46,9 +46,19 @@ _Add changes in master not yet tagged._
46
46
 
47
47
  - **Node Renderer Gem Version Validation**: The node renderer now validates that the Ruby gem version (`react_on_rails_pro`) matches the node renderer package version (`@shakacode-tools/react-on-rails-pro-node-renderer`) on every render request. Environment-aware: strict enforcement in development (returns 412 Precondition Failed on mismatch), permissive in production (allows with warning). Includes version normalization to handle Ruby gem vs NPM format differences (e.g., `4.0.0.rc.1` vs `4.0.0-rc.1`). [PR #1881](https://github.com/shakacode/react_on_rails/pull/1881) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
48
48
 
49
+ ### Fixed
50
+
51
+ - Fixed compatibility issue with httpx 1.6.x by explicitly requiring http-2 >= 1.1.1. [PR 2141](https://github.com/shakacode/react_on_rails/pull/2141) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
52
+
53
+ - **Node Renderer Worker Restart**: Fixed "descriptor closed" error that occurred when the node renderer restarts while handling an in-progress request (especially streaming requests). Workers now perform graceful shutdowns: they disconnect from the cluster to stop receiving new requests, wait for active requests to complete, then shut down cleanly. A configurable `gracefulWorkerRestartTimeout` ensures workers are forcibly killed if they don't shut down in time. [PR 1970](https://github.com/shakacode/react_on_rails/pull/1970) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
54
+
55
+ - **Body Duplication Bug On Streaming**: Fixed a bug that happens while streaming if the node renderer connection closed after streaming some chunks to the client. [PR 1995](https://github.com/shakacode/react_on_rails/pull/1995) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
56
+
49
57
  ### Changed
50
58
 
51
- - Renamed Node Renderer configuration option `bundlePath` to `serverBundleCachePath` to better clarify its purpose as a cache directory for uploaded server bundles, distinct from Shakapacker's public asset directory. The old `bundlePath` property and `RENDERER_BUNDLE_PATH` environment variable continue to work with deprecation warnings. [PR 2008](https://github.com/shakacode/react_on_rails/pull/2008) by [justin808](https://github.com/justin808).
59
+ ### Deprecated
60
+
61
+ - **Node Renderer Configuration**: Renamed `bundlePath` configuration option to `serverBundleCachePath` in the node renderer to better describe its purpose and avoid confusion with Shakapacker's public bundle path. The old `bundlePath` option continues to work with deprecation warnings. Both `RENDERER_SERVER_BUNDLE_CACHE_PATH` (new) and `RENDERER_BUNDLE_PATH` (deprecated) environment variables are supported. [PR 2008](https://github.com/shakacode/react_on_rails/pull/2008) by [justin808](https://github.com/justin808).
52
62
 
53
63
  ### Changed (Breaking)
54
64
 
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: ..
11
11
  specs:
12
- react_on_rails (16.2.0.beta.11)
12
+ react_on_rails (16.2.0.test.3)
13
13
  addressable
14
14
  connection_pool
15
15
  execjs (~> 2.5)
@@ -20,15 +20,16 @@ PATH
20
20
  PATH
21
21
  remote: .
22
22
  specs:
23
- react_on_rails_pro (16.2.0.beta.11)
23
+ react_on_rails_pro (16.2.0.test.3)
24
24
  addressable
25
25
  async (>= 2.6)
26
26
  connection_pool
27
27
  execjs (~> 2.9)
28
+ http-2 (>= 1.1.1)
28
29
  httpx (~> 1.5)
29
30
  jwt (~> 2.7)
30
31
  rainbow
31
- react_on_rails (= 16.2.0.beta.11)
32
+ react_on_rails (= 16.2.0.test.3)
32
33
 
33
34
  GEM
34
35
  remote: https://rubygems.org/
@@ -182,7 +183,7 @@ GEM
182
183
  graphiql-rails (1.10.0)
183
184
  railties
184
185
  hashdiff (1.1.0)
185
- http-2 (1.0.2)
186
+ http-2 (1.1.1)
186
187
  httpx (1.5.1)
187
188
  http-2 (>= 1.0.0)
188
189
  i18n (1.14.7)
@@ -128,7 +128,11 @@ module ReactOnRailsProHelper
128
128
  # Because setting prerender to false is equivalent to calling react_component with prerender: false
129
129
  options[:prerender] = true
130
130
  options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
131
- run_stream_inside_fiber do
131
+
132
+ # Extract streaming-specific callback
133
+ on_complete = options.delete(:on_complete)
134
+
135
+ consumer_stream_async(on_complete: on_complete) do
132
136
  internal_stream_react_component(component_name, options)
133
137
  end
134
138
  end
@@ -185,7 +189,11 @@ module ReactOnRailsProHelper
185
189
  # rsc_payload_react_component doesn't have the prerender option
186
190
  # Because setting prerender to false will not do anything
187
191
  options[:prerender] = true
188
- run_stream_inside_fiber do
192
+
193
+ # Extract streaming-specific callback
194
+ on_complete = options.delete(:on_complete)
195
+
196
+ consumer_stream_async(on_complete: on_complete) do
189
197
  internal_rsc_payload_react_component(component_name, options)
190
198
  end
191
199
  end
@@ -209,6 +217,63 @@ module ReactOnRailsProHelper
209
217
  end
210
218
  end
211
219
 
220
+ # Renders a React component asynchronously, returning an AsyncValue immediately.
221
+ # Multiple async_react_component calls will execute their HTTP rendering requests
222
+ # concurrently instead of sequentially.
223
+ #
224
+ # Requires the controller to include ReactOnRailsPro::AsyncRendering and call
225
+ # enable_async_react_rendering.
226
+ #
227
+ # @param component_name [String] Name of your registered component
228
+ # @param options [Hash] Same options as react_component
229
+ # @return [ReactOnRailsPro::AsyncValue] Call .value to get the rendered HTML
230
+ #
231
+ # @example
232
+ # <% header = async_react_component("Header", props: @header_props) %>
233
+ # <% sidebar = async_react_component("Sidebar", props: @sidebar_props) %>
234
+ # <%= header.value %>
235
+ # <%= sidebar.value %>
236
+ #
237
+ def async_react_component(component_name, options = {})
238
+ unless defined?(@react_on_rails_async_barrier) && @react_on_rails_async_barrier
239
+ raise ReactOnRailsPro::Error,
240
+ "async_react_component requires AsyncRendering concern. " \
241
+ "Include ReactOnRailsPro::AsyncRendering in your controller and call enable_async_react_rendering."
242
+ end
243
+
244
+ task = @react_on_rails_async_barrier.async do
245
+ react_component(component_name, options)
246
+ end
247
+
248
+ ReactOnRailsPro::AsyncValue.new(task: task)
249
+ end
250
+
251
+ # Renders a React component asynchronously with caching support.
252
+ # Cache lookup is synchronous - cache hits return immediately without async.
253
+ # Cache misses trigger async render and cache the result on completion.
254
+ #
255
+ # All the same options as cached_react_component apply:
256
+ # 1. You must pass the props as a block (evaluated only on cache miss)
257
+ # 2. Provide the cache_key option
258
+ # 3. Optionally provide :cache_options for Rails.cache (expires_in, etc.)
259
+ # 4. Provide :if or :unless for conditional caching
260
+ #
261
+ # @param component_name [String] Name of your registered component
262
+ # @param options [Hash] Options including cache_key and cache_options
263
+ # @yield Block that returns props (evaluated only on cache miss)
264
+ # @return [ReactOnRailsPro::AsyncValue, ReactOnRailsPro::ImmediateAsyncValue]
265
+ #
266
+ # @example
267
+ # <% card = cached_async_react_component("ProductCard", cache_key: @product) { @product.to_props } %>
268
+ # <%= card.value %>
269
+ #
270
+ def cached_async_react_component(component_name, raw_options = {}, &block)
271
+ ReactOnRailsPro::Utils.with_trace(component_name) do
272
+ check_caching_options!(raw_options, block)
273
+ fetch_async_react_component(component_name, raw_options, &block)
274
+ end
275
+ end
276
+
212
277
  if defined?(ScoutApm)
213
278
  include ScoutApm::Tracer
214
279
  instrument_method :cached_react_component, type: "ReactOnRails", name: "cached_react_component"
@@ -246,30 +311,29 @@ module ReactOnRailsProHelper
246
311
  load_pack_for_generated_component(component_name, render_options)
247
312
 
248
313
  initial_result, *rest_chunks = cached_chunks
249
- hit_fiber = Fiber.new do
250
- rest_chunks.each { |chunk| Fiber.yield(chunk) }
251
- nil
314
+
315
+ # Enqueue remaining chunks asynchronously
316
+ @async_barrier.async do
317
+ rest_chunks.each { |chunk| @main_output_queue.enqueue(chunk) }
252
318
  end
253
- @rorp_rendering_fibers << hit_fiber
319
+
320
+ # Return first chunk directly
254
321
  initial_result
255
322
  end
256
323
 
257
324
  def handle_stream_cache_miss(component_name, raw_options, auto_load_bundle, view_cache_key, &block)
258
- # Kick off the normal streaming helper to get the initial result and the original fiber
259
- initial_result = render_stream_component_with_props(component_name, raw_options, auto_load_bundle, &block)
260
- original_fiber = @rorp_rendering_fibers.pop
261
-
262
- buffered_chunks = [initial_result]
263
- wrapper_fiber = Fiber.new do
264
- while (chunk = original_fiber.resume)
265
- buffered_chunks << chunk
266
- Fiber.yield(chunk)
267
- end
268
- Rails.cache.write(view_cache_key, buffered_chunks, raw_options[:cache_options] || {})
269
- nil
270
- end
271
- @rorp_rendering_fibers << wrapper_fiber
272
- initial_result
325
+ cache_aware_options = raw_options.merge(
326
+ on_complete: lambda { |chunks|
327
+ Rails.cache.write(view_cache_key, chunks, raw_options[:cache_options] || {})
328
+ }
329
+ )
330
+
331
+ render_stream_component_with_props(
332
+ component_name,
333
+ cache_aware_options,
334
+ auto_load_bundle,
335
+ &block
336
+ )
273
337
  end
274
338
 
275
339
  def render_stream_component_with_props(component_name, raw_options, auto_load_bundle)
@@ -291,25 +355,117 @@ module ReactOnRailsProHelper
291
355
  raise ReactOnRailsPro::Error, "Option 'cache_key' is required for React on Rails caching"
292
356
  end
293
357
 
294
- def run_stream_inside_fiber
295
- if @rorp_rendering_fibers.nil?
358
+ # Async version of fetch_react_component. Handles cache lookup synchronously,
359
+ # returns ImmediateAsyncValue on hit, AsyncValue on miss.
360
+ def fetch_async_react_component(component_name, raw_options, &block)
361
+ unless defined?(@react_on_rails_async_barrier) && @react_on_rails_async_barrier
362
+ raise ReactOnRailsPro::Error,
363
+ "cached_async_react_component requires AsyncRendering concern. " \
364
+ "Include ReactOnRailsPro::AsyncRendering in your controller and call enable_async_react_rendering."
365
+ end
366
+
367
+ # Check conditional caching (:if / :unless options)
368
+ unless ReactOnRailsPro::Cache.use_cache?(raw_options)
369
+ return render_async_react_component_uncached(component_name, raw_options, &block)
370
+ end
371
+
372
+ cache_key = ReactOnRailsPro::Cache.react_component_cache_key(component_name, raw_options)
373
+ cache_options = raw_options[:cache_options] || {}
374
+ Rails.logger.debug { "React on Rails Pro async cache_key is #{cache_key.inspect}" }
375
+
376
+ # Synchronous cache lookup
377
+ cached_result = Rails.cache.read(cache_key, cache_options)
378
+ if cached_result
379
+ Rails.logger.debug { "React on Rails Pro async cache HIT for #{cache_key.inspect}" }
380
+ render_options = ReactOnRails::ReactComponent::RenderOptions.new(
381
+ react_component_name: component_name,
382
+ options: raw_options
383
+ )
384
+ load_pack_for_generated_component(component_name, render_options)
385
+ return ReactOnRailsPro::ImmediateAsyncValue.new(cached_result)
386
+ end
387
+
388
+ Rails.logger.debug { "React on Rails Pro async cache MISS for #{cache_key.inspect}" }
389
+ render_async_react_component_with_cache(component_name, raw_options, cache_key, cache_options, &block)
390
+ end
391
+
392
+ # Renders async without caching (when :if/:unless conditions disable cache)
393
+ def render_async_react_component_uncached(component_name, raw_options, &block)
394
+ options = prepare_async_render_options(raw_options, &block)
395
+
396
+ task = @react_on_rails_async_barrier.async do
397
+ react_component(component_name, options)
398
+ end
399
+
400
+ ReactOnRailsPro::AsyncValue.new(task: task)
401
+ end
402
+
403
+ # Renders async and writes to cache on completion
404
+ def render_async_react_component_with_cache(component_name, raw_options, cache_key, cache_options, &block)
405
+ options = prepare_async_render_options(raw_options, &block)
406
+
407
+ task = @react_on_rails_async_barrier.async do
408
+ result = react_component(component_name, options)
409
+ Rails.cache.write(cache_key, result, cache_options)
410
+ result
411
+ end
412
+
413
+ ReactOnRailsPro::AsyncValue.new(task: task)
414
+ end
415
+
416
+ def prepare_async_render_options(raw_options)
417
+ raw_options.merge(
418
+ props: yield,
419
+ skip_prerender_cache: true,
420
+ auto_load_bundle: ReactOnRails.configuration.auto_load_bundle || raw_options[:auto_load_bundle]
421
+ )
422
+ end
423
+
424
+ def consumer_stream_async(on_complete:)
425
+ require "async/variable"
426
+
427
+ if @async_barrier.nil?
296
428
  raise ReactOnRails::Error,
297
429
  "You must call stream_view_containing_react_components to render the view containing the react component"
298
430
  end
299
431
 
300
- rendering_fiber = Fiber.new do
432
+ # Create a variable to hold the first chunk for synchronous return
433
+ first_chunk_var = Async::Variable.new
434
+ all_chunks = [] if on_complete # Only collect if callback provided
435
+
436
+ # Start an async task on the barrier to stream all chunks
437
+ @async_barrier.async do
301
438
  stream = yield
302
- stream.each_chunk do |chunk|
303
- Fiber.yield chunk
304
- end
439
+ process_stream_chunks(stream, first_chunk_var, all_chunks)
440
+ on_complete&.call(all_chunks)
305
441
  end
306
442
 
307
- @rorp_rendering_fibers << rendering_fiber
443
+ # Wait for and return the first chunk (blocking)
444
+ first_chunk_var.wait
445
+ first_chunk_var.value
446
+ end
447
+
448
+ def process_stream_chunks(stream, first_chunk_var, all_chunks)
449
+ is_first = true
450
+
451
+ stream.each_chunk do |chunk|
452
+ # Check if client disconnected before processing chunk
453
+ break if response.stream.closed?
454
+
455
+ all_chunks&.push(chunk)
456
+
457
+ if is_first
458
+ # Store first chunk in variable for synchronous return
459
+ first_chunk_var.value = chunk
460
+ is_first = false
461
+ else
462
+ # Enqueue remaining chunks to main output queue
463
+ @main_output_queue.enqueue(chunk)
464
+ end
465
+ end
308
466
 
309
- # return the first chunk of the fiber
310
- # It contains the initial html of the component
311
- # all updates will be appended to the stream sent to browser
312
- rendering_fiber.resume
467
+ # Handle case where stream has no chunks
468
+ first_chunk_var.value = nil if is_first
313
469
  end
314
470
 
315
471
  def internal_stream_react_component(component_name, options = {})
@@ -347,7 +503,8 @@ module ReactOnRailsProHelper
347
503
  render_options: render_options
348
504
  )
349
505
  else
350
- result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : ""
506
+ console_script = chunk_json_result["consoleReplayScript"]
507
+ result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
351
508
  # No need to prepend component_specification_tag or add rails context again
352
509
  # as they're already included in the first chunk
353
510
  compose_react_component_html_with_spec_and_console(
data/eslint.config.mjs CHANGED
@@ -3,7 +3,6 @@ import { includeIgnoreFile } from '@eslint/compat';
3
3
  import js from '@eslint/js';
4
4
  import { FlatCompat } from '@eslint/eslintrc';
5
5
  import { defineConfig, globalIgnores } from 'eslint/config';
6
- import importPlugin from 'eslint-plugin-import';
7
6
  import jest from 'eslint-plugin-jest';
8
7
  import prettierRecommended from 'eslint-plugin-prettier/recommended';
9
8
  import globals from 'globals';
@@ -22,8 +21,9 @@ export default defineConfig([
22
21
  'spec/react_on_rails/dummy-for-generators',
23
22
  // includes some generated code
24
23
  'spec/dummy/client/app/packs/server-bundle.js',
25
- 'packages/node-renderer/lib/',
26
- 'packages/node-renderer/tests/fixtures',
24
+ '../packages/react-on-rails/', // Ignore open-source package (has its own linting)
25
+ '../packages/react-on-rails-pro-node-renderer/lib/',
26
+ '../packages/react-on-rails-pro-node-renderer/tests/fixtures',
27
27
  '**/node_modules/',
28
28
  '**/assets/webpack/',
29
29
  '**/generated/',
@@ -129,7 +129,7 @@ export default defineConfig([
129
129
  },
130
130
  {
131
131
  files: ['**/*.ts{x,}'],
132
- extends: [importPlugin.flatConfigs.typescript, typescriptEslint.configs.strictTypeChecked],
132
+ extends: [typescriptEslint.configs.strictTypeChecked],
133
133
 
134
134
  languageOptions: {
135
135
  parserOptions: {
@@ -166,7 +166,7 @@ export default defineConfig([
166
166
  },
167
167
  },
168
168
  {
169
- files: ['packages/node-renderer/tests/**', '**/*.test.{js,jsx,ts,tsx}'],
169
+ files: ['../packages/react-on-rails-pro-node-renderer/tests/**', '**/*.test.{js,jsx,ts,tsx}'],
170
170
 
171
171
  extends: [jest.configs['flat/recommended'], jest.configs['flat/style']],
172
172
 
@@ -183,8 +183,8 @@ export default defineConfig([
183
183
  },
184
184
  },
185
185
  {
186
- files: ['packages/node-renderer/src/integrations/**'],
187
- ignores: ['packages/node-renderer/src/integrations/api.ts'],
186
+ files: ['../packages/react-on-rails-pro-node-renderer/src/integrations/**'],
187
+ ignores: ['../packages/react-on-rails-pro-node-renderer/src/integrations/api.ts'],
188
188
 
189
189
  rules: {
190
190
  // Integrations should only use the public integration API
@@ -214,6 +214,25 @@ export default defineConfig([
214
214
  'react-hooks/rules-of-hooks': ['off'],
215
215
  },
216
216
  },
217
+ {
218
+ // Dummy apps have dependencies managed separately and may not be installed
219
+ files: ['spec/dummy/**/*', 'spec/execjs-compatible-dummy/**/*'],
220
+ rules: {
221
+ 'import/no-unresolved': 'off',
222
+ 'import/named': 'off',
223
+ },
224
+ },
225
+ {
226
+ // Pro packages use monorepo workspace imports that ESLint can't resolve
227
+ // TypeScript compiler validates these imports
228
+ files: ['../packages/react-on-rails-pro/**/*', '../packages/react-on-rails-pro-node-renderer/**/*'],
229
+ rules: {
230
+ 'import/named': 'off',
231
+ 'import/no-unresolved': 'off',
232
+ 'import/no-cycle': 'off',
233
+ 'import/extensions': 'off',
234
+ },
235
+ },
217
236
  // must be the last config in the array
218
237
  // https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs
219
238
  prettierRecommended,
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRailsPro
4
+ # AsyncValue wraps an Async task to provide a simple interface for
5
+ # retrieving the result of an async react_component render.
6
+ #
7
+ # @example
8
+ # async_value = async_react_component("MyComponent", props: { name: "World" })
9
+ # # ... do other work ...
10
+ # html = async_value.value # blocks until result is ready
11
+ #
12
+ class AsyncValue
13
+ def initialize(task:)
14
+ @task = task
15
+ end
16
+
17
+ # Blocks until result is ready, returns HTML string.
18
+ # If the async task raised an exception, it will be re-raised here.
19
+ def value
20
+ @task.wait
21
+ end
22
+
23
+ def resolved?
24
+ @task.finished?
25
+ end
26
+
27
+ def to_s
28
+ value.to_s
29
+ end
30
+
31
+ def html_safe
32
+ value.html_safe
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRailsPro
4
+ # AsyncRendering enables concurrent rendering of multiple React components.
5
+ # When enabled, async_react_component calls will execute their HTTP requests
6
+ # in parallel instead of sequentially.
7
+ #
8
+ # @example Enable for all actions
9
+ # class ProductsController < ApplicationController
10
+ # include ReactOnRailsPro::AsyncRendering
11
+ # enable_async_react_rendering
12
+ # end
13
+ #
14
+ # @example Enable for specific actions only
15
+ # class ProductsController < ApplicationController
16
+ # include ReactOnRailsPro::AsyncRendering
17
+ # enable_async_react_rendering only: [:show, :index]
18
+ # end
19
+ #
20
+ # @example Enable for all except specific actions
21
+ # class ProductsController < ApplicationController
22
+ # include ReactOnRailsPro::AsyncRendering
23
+ # enable_async_react_rendering except: [:create, :update]
24
+ # end
25
+ #
26
+ module AsyncRendering
27
+ extend ActiveSupport::Concern
28
+
29
+ class_methods do
30
+ # Enables async React component rendering for controller actions.
31
+ # Accepts standard Rails filter options like :only and :except.
32
+ #
33
+ # @param options [Hash] Options passed to around_action (e.g., only:, except:)
34
+ def enable_async_react_rendering(**options)
35
+ around_action :wrap_in_async_react_context, **options
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def wrap_in_async_react_context
42
+ require "async"
43
+ require "async/barrier"
44
+
45
+ Sync do
46
+ @react_on_rails_async_barrier = Async::Barrier.new
47
+ yield
48
+ check_for_unresolved_async_components
49
+ ensure
50
+ @react_on_rails_async_barrier&.stop
51
+ @react_on_rails_async_barrier = nil
52
+ end
53
+ end
54
+
55
+ def check_for_unresolved_async_components
56
+ return if @react_on_rails_async_barrier.nil?
57
+
58
+ pending_tasks = @react_on_rails_async_barrier.size
59
+ return if pending_tasks.zero?
60
+
61
+ Rails.logger.error(
62
+ "[React on Rails Pro] #{pending_tasks} async component(s) were started but never resolved. " \
63
+ "Make sure to call .value on all AsyncValue objects returned by async_react_component " \
64
+ "or cached_async_react_component. Unresolved tasks will be stopped."
65
+ )
66
+ end
67
+ end
68
+ end