react_on_rails_pro 17.0.1 → 17.1.0.rc.0
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 +4 -4
- data/CLAUDE.md +4 -3
- data/CONTEXT.md +34 -27
- data/Gemfile.development_dependencies +1 -0
- data/Gemfile.lock +18 -3
- data/LICENSE_SETUP.md +42 -9
- data/README.md +12 -1
- data/app/controllers/react_on_rails_pro/rolling_deploy/bundles_controller.rb +69 -95
- data/app/helpers/react_on_rails_pro_helper.rb +178 -45
- data/docs/adr/0001-boot-seed-rolling-deploy-bundles.md +28 -17
- data/lib/react_on_rails_pro/assets_precompile.rb +34 -20
- data/lib/react_on_rails_pro/async_props_emitter.rb +64 -1
- data/lib/react_on_rails_pro/cache.rb +1 -1
- data/lib/react_on_rails_pro/concerns/async_rendering.rb +11 -6
- data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +7 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +17 -9
- data/lib/react_on_rails_pro/configuration.rb +79 -11
- data/lib/react_on_rails_pro/engine.rb +11 -5
- data/lib/react_on_rails_pro/error.rb +5 -0
- data/lib/react_on_rails_pro/license_task_formatter.rb +1 -1
- data/lib/react_on_rails_pro/license_validator.rb +38 -38
- data/lib/react_on_rails_pro/open_telemetry.rb +210 -0
- data/lib/react_on_rails_pro/pre_seed_renderer_cache.rb +269 -35
- data/lib/react_on_rails_pro/renderer_artifact.rb +176 -0
- data/lib/react_on_rails_pro/renderer_artifact_support.rb +287 -0
- data/lib/react_on_rails_pro/renderer_cache_helpers.rb +58 -32
- data/lib/react_on_rails_pro/renderer_http_client.rb +134 -39
- data/lib/react_on_rails_pro/request.rb +249 -118
- data/lib/react_on_rails_pro/rolling_deploy/tarball.rb +48 -18
- data/lib/react_on_rails_pro/rolling_deploy_adapters/http.rb +242 -93
- data/lib/react_on_rails_pro/rolling_deploy_cache_stager.rb +77 -19
- data/lib/react_on_rails_pro/server_rendering_js_code.rb +48 -4
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +46 -7
- data/lib/react_on_rails_pro/stream_cache.rb +18 -0
- data/lib/react_on_rails_pro/stream_request.rb +9 -2
- data/lib/react_on_rails_pro/utils.rb +77 -17
- data/lib/react_on_rails_pro/version.rb +1 -1
- data/lib/react_on_rails_pro.rb +1 -0
- data/sig/react_on_rails_pro/configuration.rbs +7 -0
- data/sig/react_on_rails_pro/open_telemetry.rbs +52 -0
- data/sig/react_on_rails_pro/renderer_http_client.rbs +18 -0
- metadata +7 -3
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# For licensing terms:
|
|
14
14
|
# https://github.com/shakacode/react_on_rails/blob/main/REACT-ON-RAILS-PRO-LICENSE.md
|
|
15
15
|
|
|
16
|
+
require_relative "../open_telemetry"
|
|
17
|
+
|
|
16
18
|
module ReactOnRailsPro
|
|
17
19
|
# AsyncRendering enables concurrent rendering of multiple React components.
|
|
18
20
|
# When enabled, async_react_component calls will execute their HTTP requests
|
|
@@ -54,14 +56,17 @@ module ReactOnRailsPro
|
|
|
54
56
|
def wrap_in_async_react_context
|
|
55
57
|
require "async"
|
|
56
58
|
require "async/barrier"
|
|
59
|
+
parent_context = ReactOnRailsPro::OpenTelemetry.capture_context
|
|
57
60
|
|
|
58
61
|
Sync do
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
ReactOnRailsPro::OpenTelemetry.with_context(parent_context) do
|
|
63
|
+
@react_on_rails_async_barrier = Async::Barrier.new
|
|
64
|
+
yield
|
|
65
|
+
check_for_unresolved_async_components
|
|
66
|
+
ensure
|
|
67
|
+
@react_on_rails_async_barrier&.stop
|
|
68
|
+
@react_on_rails_async_barrier = nil
|
|
69
|
+
end
|
|
65
70
|
end
|
|
66
71
|
end
|
|
67
72
|
|
|
@@ -24,6 +24,8 @@ module ReactOnRailsPro
|
|
|
24
24
|
|
|
25
25
|
def rsc_payload
|
|
26
26
|
@rsc_payload_component_name = rsc_payload_component_name
|
|
27
|
+
return head :forbidden unless rsc_payload_authorized?(@rsc_payload_component_name)
|
|
28
|
+
|
|
27
29
|
@rsc_payload_component_props =
|
|
28
30
|
begin
|
|
29
31
|
rsc_payload_component_props
|
|
@@ -58,6 +60,11 @@ module ReactOnRailsPro
|
|
|
58
60
|
|
|
59
61
|
private
|
|
60
62
|
|
|
63
|
+
def rsc_payload_authorized?(component_name)
|
|
64
|
+
authorizer = ReactOnRailsPro.configuration.rsc_payload_authorizer
|
|
65
|
+
authorizer.nil? || authorizer.call(self, component_name)
|
|
66
|
+
end
|
|
67
|
+
|
|
61
68
|
def rsc_payload_component_props
|
|
62
69
|
return {} if params[:props].blank?
|
|
63
70
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
require "English"
|
|
17
17
|
require "erb/util"
|
|
18
|
+
require_relative "../open_telemetry"
|
|
18
19
|
|
|
19
20
|
module ReactOnRailsPro
|
|
20
21
|
module StreamCacheWrites
|
|
@@ -66,8 +67,10 @@ module ReactOnRailsPro
|
|
|
66
67
|
extend ActiveSupport::Concern
|
|
67
68
|
RENDERER_SERVER_TIMING_COLLECTOR_KEY = :react_on_rails_pro_rsc_stream_renderer_server_timing_entries
|
|
68
69
|
RENDERER_SERVER_TIMING_ATTEMPT_COLLECTOR_KEY = :react_on_rails_pro_rsc_stream_renderer_server_timing_attempt
|
|
70
|
+
MAX_BROWSER_PERFORMANCE_MARK_FALLBACK_ENTRIES = 200
|
|
69
71
|
private_constant :RENDERER_SERVER_TIMING_COLLECTOR_KEY
|
|
70
72
|
private_constant :RENDERER_SERVER_TIMING_ATTEMPT_COLLECTOR_KEY
|
|
73
|
+
private_constant :MAX_BROWSER_PERFORMANCE_MARK_FALLBACK_ENTRIES
|
|
71
74
|
|
|
72
75
|
included do
|
|
73
76
|
include ActionController::Live
|
|
@@ -221,16 +224,19 @@ module ReactOnRailsPro
|
|
|
221
224
|
require_streaming_dependencies
|
|
222
225
|
warn_on_non_html_formats_without_content_type(render_options[:formats], content_type)
|
|
223
226
|
initialize_rsc_stream_observability_state(rsc_stream_observability)
|
|
227
|
+
parent_context = ReactOnRailsPro::OpenTelemetry.capture_context
|
|
224
228
|
|
|
225
229
|
Sync do |parent_task|
|
|
226
|
-
ReactOnRailsPro::
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
ReactOnRailsPro::OpenTelemetry.with_context(parent_context) do
|
|
231
|
+
ReactOnRailsPro::Stream.with_renderer_server_timing_collector(renderer_server_timing_collector_for_stream) do
|
|
232
|
+
stream_view_containing_react_components_in_sync(
|
|
233
|
+
parent_task,
|
|
234
|
+
template:,
|
|
235
|
+
close_stream_at_end:,
|
|
236
|
+
content_type:,
|
|
237
|
+
render_options:
|
|
238
|
+
)
|
|
239
|
+
end
|
|
234
240
|
end
|
|
235
241
|
end
|
|
236
242
|
ensure
|
|
@@ -429,7 +435,9 @@ module ReactOnRailsPro
|
|
|
429
435
|
try{perf.mark(#{mark_name_json});entry.fallback="mark-detail-unavailable";}
|
|
430
436
|
catch(fallbackError){entry.fallback="performance-mark-unavailable";}}
|
|
431
437
|
else{entry.fallback="performance-mark-unavailable";}
|
|
432
|
-
|
|
438
|
+
var queue=self.REACT_ON_RAILS_PERFORMANCE_MARKS=self.REACT_ON_RAILS_PERFORMANCE_MARKS||[];
|
|
439
|
+
queue.push(entry);if(queue.length>#{MAX_BROWSER_PERFORMANCE_MARK_FALLBACK_ENTRIES}){
|
|
440
|
+
queue.splice(0,queue.length-#{MAX_BROWSER_PERFORMANCE_MARK_FALLBACK_ENTRIES});}
|
|
433
441
|
})()</script>
|
|
434
442
|
HTML
|
|
435
443
|
end
|
|
@@ -31,7 +31,9 @@ module ReactOnRailsPro
|
|
|
31
31
|
renderer_http_pool_timeout: Configuration::DEFAULT_RENDERER_HTTP_POOL_TIMEOUT,
|
|
32
32
|
renderer_http_pool_warn_timeout: Configuration::DEFAULT_RENDERER_HTTP_POOL_WARN_TIMEOUT,
|
|
33
33
|
renderer_http_keep_alive_timeout: Configuration::DEFAULT_RENDERER_HTTP_KEEP_ALIVE_TIMEOUT,
|
|
34
|
+
renderer_http_force_http2: Configuration::DEFAULT_RENDERER_HTTP_FORCE_HTTP2,
|
|
34
35
|
renderer_password: nil,
|
|
36
|
+
license_token: nil,
|
|
35
37
|
tracing: Configuration::DEFAULT_TRACING,
|
|
36
38
|
dependency_globs: Configuration::DEFAULT_DEPENDENCY_GLOBS,
|
|
37
39
|
excluded_dependency_globs: Configuration::DEFAULT_EXCLUDED_DEPENDENCY_GLOBS,
|
|
@@ -50,6 +52,7 @@ module ReactOnRailsPro
|
|
|
50
52
|
raise_non_shell_server_rendering_errors: Configuration::DEFAULT_RAISE_NON_SHELL_SERVER_RENDERING_ERRORS,
|
|
51
53
|
enable_rsc_support: Configuration::DEFAULT_ENABLE_RSC_SUPPORT,
|
|
52
54
|
rsc_payload_generation_url_path: Configuration::DEFAULT_RSC_PAYLOAD_GENERATION_URL_PATH,
|
|
55
|
+
rsc_payload_authorizer: nil,
|
|
53
56
|
rsc_bundle_js_file: Configuration::DEFAULT_RSC_BUNDLE_JS_FILE,
|
|
54
57
|
react_client_manifest_file: Configuration::DEFAULT_REACT_CLIENT_MANIFEST_FILE,
|
|
55
58
|
react_server_client_manifest_file: Configuration::DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE,
|
|
@@ -63,14 +66,16 @@ module ReactOnRailsPro
|
|
|
63
66
|
DEFAULT_RENDERER_URL = "http://localhost:3800"
|
|
64
67
|
DEFAULT_RENDERER_METHOD = "ExecJS"
|
|
65
68
|
DEFAULT_RENDERER_FALLBACK_EXEC_JS = true
|
|
66
|
-
# Maximum concurrent
|
|
67
|
-
#
|
|
68
|
-
# Without a scheduler,
|
|
69
|
+
# Maximum concurrent renderer connections per persistent client. With HTTP/1.1, each connection serves one
|
|
70
|
+
# request at a time, so this is also the request-concurrency cap for a client shared by a long-lived scheduler.
|
|
71
|
+
# Without a Fiber.scheduler, streaming renders use an ephemeral client and non-streaming renders use a persistent
|
|
72
|
+
# per-thread client; this bounds connections within each of those clients.
|
|
69
73
|
DEFAULT_RENDERER_HTTP_POOL_SIZE = 10
|
|
70
74
|
# TCP connect timeout. Request and response processing are still bounded by ssr_timeout.
|
|
71
75
|
DEFAULT_RENDERER_HTTP_POOL_TIMEOUT = 5
|
|
72
76
|
DEFAULT_RENDERER_HTTP_POOL_WARN_TIMEOUT = 0.25
|
|
73
77
|
DEFAULT_RENDERER_HTTP_KEEP_ALIVE_TIMEOUT = 30
|
|
78
|
+
DEFAULT_RENDERER_HTTP_FORCE_HTTP2 = true
|
|
74
79
|
DEFAULT_SSR_TIMEOUT = 5
|
|
75
80
|
DEFAULT_PRERENDER_CACHING = false
|
|
76
81
|
DEFAULT_TRACING = false
|
|
@@ -95,6 +100,7 @@ module ReactOnRailsPro
|
|
|
95
100
|
DEFAULT_RSC_BUNDLE_JS_FILE = "rsc-bundle.js"
|
|
96
101
|
DEFAULT_REACT_CLIENT_MANIFEST_FILE = "react-client-manifest.json"
|
|
97
102
|
DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE = "react-server-client-manifest.json"
|
|
103
|
+
RSC_PAYLOAD_AUTHORIZER_POSITIONAL_PARAMS = %i[req opt].freeze
|
|
98
104
|
DEFAULT_CONCURRENT_COMPONENT_STREAMING_BUFFER_SIZE = 64
|
|
99
105
|
# Ceiling TTL for a tag->cache-key index entry when the tagged cache entry
|
|
100
106
|
# has no :expires_in of its own (see ReactOnRailsPro::Cache::TagIndex).
|
|
@@ -107,7 +113,7 @@ module ReactOnRailsPro
|
|
|
107
113
|
ROLLING_DEPLOY_UPLOAD_ALL_KEYWORD_PARAMS = %i[keyrest].freeze
|
|
108
114
|
ROLLING_DEPLOY_UPLOAD_REQUIRED_KEYWORDS = %i[bundle assets].freeze
|
|
109
115
|
|
|
110
|
-
attr_accessor :renderer_url, :renderer_password, :tracing,
|
|
116
|
+
attr_accessor :renderer_url, :renderer_password, :license_token, :tracing,
|
|
111
117
|
:server_renderer, :renderer_use_fallback_exec_js, :prerender_caching,
|
|
112
118
|
:renderer_http_pool_timeout, :renderer_http_pool_warn_timeout,
|
|
113
119
|
:dependency_globs, :excluded_dependency_globs, :rendering_returns_promises,
|
|
@@ -120,7 +126,8 @@ module ReactOnRailsPro
|
|
|
120
126
|
:react_server_client_manifest_file
|
|
121
127
|
|
|
122
128
|
attr_reader :concurrent_component_streaming_buffer_size, :renderer_http_keep_alive_timeout,
|
|
123
|
-
:renderer_http_pool_size, :
|
|
129
|
+
:renderer_http_pool_size, :renderer_http_force_http2, :cache_tag_index_expires_in,
|
|
130
|
+
:cache_tag_index_max_keys, :rsc_payload_authorizer
|
|
124
131
|
|
|
125
132
|
# Sets how long tag->key index entries live (see Cache::TagIndex).
|
|
126
133
|
#
|
|
@@ -164,11 +171,12 @@ module ReactOnRailsPro
|
|
|
164
171
|
@concurrent_component_streaming_buffer_size = value
|
|
165
172
|
end
|
|
166
173
|
|
|
167
|
-
# Sets the maximum concurrent
|
|
174
|
+
# Sets the maximum concurrent renderer connections per persistent client.
|
|
168
175
|
#
|
|
169
176
|
# When Fiber.scheduler is available (e.g., inside Sync {} blocks), HTTP clients are
|
|
170
177
|
# reused across requests within the same scheduler context, making this limit effective
|
|
171
|
-
# for connection pooling. Without a scheduler,
|
|
178
|
+
# for connection pooling. Without a Fiber.scheduler, streaming renders use an ephemeral
|
|
179
|
+
# client and non-streaming renders use a persistent per-thread client.
|
|
172
180
|
#
|
|
173
181
|
# @param value [Integer, nil] A positive integer or nil (uses default)
|
|
174
182
|
# @raise [ReactOnRailsPro::Error] if value is not a positive integer or nil
|
|
@@ -194,10 +202,40 @@ module ReactOnRailsPro
|
|
|
194
202
|
@renderer_http_keep_alive_timeout = value
|
|
195
203
|
end
|
|
196
204
|
|
|
197
|
-
|
|
205
|
+
# Sets whether cleartext renderer connections force HTTP/2 prior knowledge (h2c).
|
|
206
|
+
#
|
|
207
|
+
# Set this to false only when the Node Renderer is also configured for HTTP/1.1. Direct HTTP/1.1 connections can
|
|
208
|
+
# stream in both directions, but async props are not supported through request-buffering or half-duplex proxies.
|
|
209
|
+
#
|
|
210
|
+
# @param value [Boolean] true to force h2c for http URLs, false to use HTTP/1.1
|
|
211
|
+
# @raise [ReactOnRailsPro::Error] if value is not true or false
|
|
212
|
+
def renderer_http_force_http2=(value)
|
|
213
|
+
unless [true, false].include?(value)
|
|
214
|
+
raise ReactOnRailsPro::Error, "config.renderer_http_force_http2 must be true or false"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
@renderer_http_force_http2 = value
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def rsc_payload_authorizer=(value)
|
|
221
|
+
unless value.nil? || value.respond_to?(:call)
|
|
222
|
+
raise ReactOnRailsPro::Error, "config.rsc_payload_authorizer must be nil or respond to #call"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if value && !rsc_payload_authorizer_signature_valid?(value)
|
|
226
|
+
raise ReactOnRailsPro::Error,
|
|
227
|
+
"config.rsc_payload_authorizer must accept call(controller, component_name) without required keywords"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
@rsc_payload_authorizer = value
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def initialize(renderer_url: nil, renderer_password: nil, license_token: nil, # rubocop:disable Metrics/AbcSize
|
|
234
|
+
server_renderer: nil,
|
|
198
235
|
renderer_use_fallback_exec_js: nil, prerender_caching: nil,
|
|
199
236
|
renderer_http_pool_size: nil, renderer_http_pool_timeout: nil,
|
|
200
237
|
renderer_http_pool_warn_timeout: nil, renderer_http_keep_alive_timeout: nil,
|
|
238
|
+
renderer_http_force_http2: DEFAULT_RENDERER_HTTP_FORCE_HTTP2,
|
|
201
239
|
tracing: nil,
|
|
202
240
|
dependency_globs: nil, excluded_dependency_globs: nil, rendering_returns_promises: nil,
|
|
203
241
|
remote_bundle_cache_adapter: nil, rolling_deploy_adapter: nil,
|
|
@@ -206,7 +244,7 @@ module ReactOnRailsPro
|
|
|
206
244
|
ssr_pre_hook_js: nil, assets_to_copy: nil,
|
|
207
245
|
renderer_request_retry_limit: nil, throw_js_errors: nil, ssr_timeout: nil,
|
|
208
246
|
profile_server_rendering_js_code: nil, raise_non_shell_server_rendering_errors: nil,
|
|
209
|
-
enable_rsc_support: nil, rsc_payload_generation_url_path: nil,
|
|
247
|
+
enable_rsc_support: nil, rsc_payload_generation_url_path: nil, rsc_payload_authorizer: nil,
|
|
210
248
|
rsc_bundle_js_file: nil, react_client_manifest_file: nil,
|
|
211
249
|
react_server_client_manifest_file: nil,
|
|
212
250
|
concurrent_component_streaming_buffer_size: DEFAULT_CONCURRENT_COMPONENT_STREAMING_BUFFER_SIZE,
|
|
@@ -214,6 +252,7 @@ module ReactOnRailsPro
|
|
|
214
252
|
cache_tag_index_max_keys: DEFAULT_CACHE_TAG_INDEX_MAX_KEYS)
|
|
215
253
|
self.renderer_url = renderer_url
|
|
216
254
|
self.renderer_password = renderer_password
|
|
255
|
+
self.license_token = license_token
|
|
217
256
|
self.server_renderer = server_renderer
|
|
218
257
|
self.renderer_use_fallback_exec_js = renderer_use_fallback_exec_js
|
|
219
258
|
self.prerender_caching = prerender_caching
|
|
@@ -222,6 +261,7 @@ module ReactOnRailsPro
|
|
|
222
261
|
self.renderer_http_pool_warn_timeout = renderer_http_pool_warn_timeout
|
|
223
262
|
# Initial assignment applies the default constructor value; warn only when users set this deprecated config.
|
|
224
263
|
assign_initial_renderer_http_keep_alive_timeout(renderer_http_keep_alive_timeout)
|
|
264
|
+
self.renderer_http_force_http2 = renderer_http_force_http2
|
|
225
265
|
self.tracing = tracing
|
|
226
266
|
self.rendering_returns_promises = server_renderer == "NodeRenderer" ? rendering_returns_promises : false
|
|
227
267
|
self.dependency_globs = dependency_globs
|
|
@@ -242,6 +282,7 @@ module ReactOnRailsPro
|
|
|
242
282
|
self.raise_non_shell_server_rendering_errors = raise_non_shell_server_rendering_errors
|
|
243
283
|
self.enable_rsc_support = enable_rsc_support
|
|
244
284
|
self.rsc_payload_generation_url_path = rsc_payload_generation_url_path
|
|
285
|
+
self.rsc_payload_authorizer = rsc_payload_authorizer
|
|
245
286
|
self.rsc_bundle_js_file = rsc_bundle_js_file
|
|
246
287
|
self.react_client_manifest_file = react_client_manifest_file
|
|
247
288
|
self.react_server_client_manifest_file = react_server_client_manifest_file
|
|
@@ -318,6 +359,33 @@ module ReactOnRailsPro
|
|
|
318
359
|
|
|
319
360
|
private
|
|
320
361
|
|
|
362
|
+
def rsc_payload_authorizer_signature_valid?(authorizer)
|
|
363
|
+
parameters = rsc_payload_authorizer_parameters(authorizer)
|
|
364
|
+
return false if parameters.any? { |type, _name| type == :keyreq }
|
|
365
|
+
|
|
366
|
+
# A non-lambda Proc intentionally ignores surplus positional arguments,
|
|
367
|
+
# unlike lambdas, Methods, and callable service objects.
|
|
368
|
+
return true if authorizer.is_a?(Proc) && !authorizer.lambda?
|
|
369
|
+
|
|
370
|
+
strict_rsc_payload_authorizer_signature_valid?(parameters)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def rsc_payload_authorizer_parameters(authorizer)
|
|
374
|
+
return authorizer.parameters if authorizer.is_a?(Proc) || authorizer.is_a?(Method)
|
|
375
|
+
|
|
376
|
+
authorizer.method(:call).parameters
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def strict_rsc_payload_authorizer_signature_valid?(parameters)
|
|
380
|
+
required_positionals = parameters.count { |type, _name| type == :req }
|
|
381
|
+
accepted_positionals = parameters.count do |type, _name|
|
|
382
|
+
RSC_PAYLOAD_AUTHORIZER_POSITIONAL_PARAMS.include?(type)
|
|
383
|
+
end
|
|
384
|
+
accepts_rest = parameters.any? { |type, _name| type == :rest }
|
|
385
|
+
|
|
386
|
+
required_positionals <= 2 && (accepted_positionals >= 2 || accepts_rest)
|
|
387
|
+
end
|
|
388
|
+
|
|
321
389
|
def assign_initial_renderer_http_pool_size(value)
|
|
322
390
|
validate_renderer_http_pool_size(value)
|
|
323
391
|
@renderer_http_pool_size = value
|
|
@@ -407,8 +475,8 @@ module ReactOnRailsPro
|
|
|
407
475
|
|
|
408
476
|
# Only fires when the user has selected the built-in HTTP adapter. Custom
|
|
409
477
|
# adapters that re-use the new config knobs for their own reasons are not
|
|
410
|
-
# forced through this validation. We do not validate
|
|
411
|
-
# an unset
|
|
478
|
+
# forced through this validation. We do not validate previous_url here:
|
|
479
|
+
# an unset URL is a valid "discovery off; this is the upload-only side of
|
|
412
480
|
# a one-way deploy" mode (the adapter returns [] for previous_bundle_hashes).
|
|
413
481
|
def validate_rolling_deploy_http_adapter_config
|
|
414
482
|
return unless rolling_deploy_http_adapter?
|
|
@@ -79,9 +79,13 @@ module ReactOnRailsPro
|
|
|
79
79
|
when :expired
|
|
80
80
|
expiration = ReactOnRailsPro::LicenseValidator.license_expiration
|
|
81
81
|
expired_on = expiration ? " (expired on #{expiration.strftime('%Y-%m-%d')})" : ""
|
|
82
|
-
log_license_issue(
|
|
82
|
+
log_license_issue(
|
|
83
|
+
"License has expired#{expired_on}",
|
|
84
|
+
"Renew your license at #{LICENSE_URL}",
|
|
85
|
+
warn_outside_production: true
|
|
86
|
+
)
|
|
83
87
|
when :invalid
|
|
84
|
-
log_license_issue("Invalid license", "Get a license at #{LICENSE_URL}")
|
|
88
|
+
log_license_issue("Invalid license", "Get a license at #{LICENSE_URL}", warn_outside_production: true)
|
|
85
89
|
end
|
|
86
90
|
end
|
|
87
91
|
|
|
@@ -109,7 +113,7 @@ module ReactOnRailsPro
|
|
|
109
113
|
|
|
110
114
|
message += " Attribution required for this license type." if attribution_required
|
|
111
115
|
|
|
112
|
-
Rails.logger.
|
|
116
|
+
Rails.logger.debug message
|
|
113
117
|
end
|
|
114
118
|
|
|
115
119
|
def plan_display_name(plan)
|
|
@@ -126,15 +130,17 @@ module ReactOnRailsPro
|
|
|
126
130
|
end
|
|
127
131
|
end
|
|
128
132
|
|
|
129
|
-
def log_license_issue(issue, action)
|
|
133
|
+
def log_license_issue(issue, action, warn_outside_production: false)
|
|
130
134
|
prefix = "[React on Rails Pro] #{issue}."
|
|
131
135
|
|
|
132
136
|
if Rails.env.production?
|
|
133
137
|
warning = "Production Use of React on Rails Pro requires a valid license. " \
|
|
134
138
|
"If this deployment is Production Use, #{action}"
|
|
135
139
|
Rails.logger.warn "#{prefix} #{warning}"
|
|
140
|
+
elsif warn_outside_production
|
|
141
|
+
Rails.logger.warn "#{prefix} #{action}"
|
|
136
142
|
else
|
|
137
|
-
Rails.logger.
|
|
143
|
+
Rails.logger.debug "#{prefix} No license required for development/test environments."
|
|
138
144
|
end
|
|
139
145
|
end
|
|
140
146
|
end
|
|
@@ -24,4 +24,9 @@ module ReactOnRailsPro
|
|
|
24
24
|
"This is unexpected behavior."
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
# Distinguishes a missing renderer bundle from other artifact-construction
|
|
29
|
+
# failures so cache-key callers can preserve the established missing-RSC
|
|
30
|
+
# fallback without swallowing unrelated configuration or companion errors.
|
|
31
|
+
class MissingRendererBundleError < Error; end
|
|
27
32
|
end
|
|
@@ -41,55 +41,35 @@ module ReactOnRailsPro
|
|
|
41
41
|
# See: https://bugs.ruby-lang.org/issues/20875
|
|
42
42
|
LICENSE_MUTEX = Mutex.new
|
|
43
43
|
|
|
44
|
+
# Explicit tokens receive distinct cache keys so later application configuration
|
|
45
|
+
# cannot reuse ENV/default metadata. The ENV/default path intentionally retains
|
|
46
|
+
# one process-lifetime key and the existing reset-required semantics.
|
|
47
|
+
ENV_LICENSE_CACHE_KEY = Object.new.freeze
|
|
48
|
+
|
|
44
49
|
class << self
|
|
45
50
|
# Returns the current license status (never raises, never logs)
|
|
46
51
|
# Thread-safe: uses Mutex to prevent race conditions during initialization
|
|
47
52
|
# @return [Symbol] One of :valid, :expired, :invalid, :missing
|
|
48
53
|
def license_status
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
LICENSE_MUTEX.synchronize do
|
|
52
|
-
# Double-check pattern: another thread may have set it while we waited
|
|
53
|
-
return @license_status if defined?(@license_status)
|
|
54
|
-
|
|
55
|
-
@license_status = determine_license_status
|
|
56
|
-
end
|
|
54
|
+
fetch_cached_license_value(:@license_status) { determine_license_status }
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
# Returns the license expiration time if available
|
|
60
58
|
# @return [Time, nil] The expiration time or nil if not available
|
|
61
59
|
def license_expiration
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
LICENSE_MUTEX.synchronize do
|
|
65
|
-
return @license_expiration if defined?(@license_expiration)
|
|
66
|
-
|
|
67
|
-
@license_expiration = determine_license_expiration
|
|
68
|
-
end
|
|
60
|
+
fetch_cached_license_value(:@license_expiration) { determine_license_expiration }
|
|
69
61
|
end
|
|
70
62
|
|
|
71
63
|
# Returns the organization name from the license if available
|
|
72
64
|
# @return [String, nil] The organization name or nil if not available
|
|
73
65
|
def license_organization
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
LICENSE_MUTEX.synchronize do
|
|
77
|
-
return @license_organization if defined?(@license_organization)
|
|
78
|
-
|
|
79
|
-
@license_organization = determine_license_organization
|
|
80
|
-
end
|
|
66
|
+
fetch_cached_license_value(:@license_organization) { determine_license_organization }
|
|
81
67
|
end
|
|
82
68
|
|
|
83
69
|
# Returns the license plan type if available
|
|
84
70
|
# @return [String, nil] The plan type (e.g., "paid", "startup") or nil if not available
|
|
85
71
|
def license_plan
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
LICENSE_MUTEX.synchronize do
|
|
89
|
-
return @license_plan if defined?(@license_plan)
|
|
90
|
-
|
|
91
|
-
@license_plan = determine_license_plan
|
|
92
|
-
end
|
|
72
|
+
fetch_cached_license_value(:@license_plan) { determine_license_plan }
|
|
93
73
|
end
|
|
94
74
|
|
|
95
75
|
# Returns whether the product-level attribution is required.
|
|
@@ -97,13 +77,7 @@ module ReactOnRailsPro
|
|
|
97
77
|
# A license JWT cannot waive the requirement.
|
|
98
78
|
# @return [Boolean] True if attribution is required
|
|
99
79
|
def attribution_required?
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
LICENSE_MUTEX.synchronize do
|
|
103
|
-
return @attribution_required if defined?(@attribution_required)
|
|
104
|
-
|
|
105
|
-
@attribution_required = determine_attribution_required
|
|
106
|
-
end
|
|
80
|
+
fetch_cached_license_value(:@attribution_required) { determine_attribution_required }
|
|
107
81
|
end
|
|
108
82
|
|
|
109
83
|
# Returns license information for use in helpers and components
|
|
@@ -131,6 +105,30 @@ module ReactOnRailsPro
|
|
|
131
105
|
|
|
132
106
|
private
|
|
133
107
|
|
|
108
|
+
def fetch_cached_license_value(cache_variable)
|
|
109
|
+
key = license_cache_key
|
|
110
|
+
if instance_variable_defined?(cache_variable)
|
|
111
|
+
cached = instance_variable_get(cache_variable)
|
|
112
|
+
return cached[:value] if cached[:key] == key
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
LICENSE_MUTEX.synchronize do
|
|
116
|
+
key = license_cache_key
|
|
117
|
+
if instance_variable_defined?(cache_variable)
|
|
118
|
+
cached = instance_variable_get(cache_variable)
|
|
119
|
+
return cached[:value] if cached[:key] == key
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
value = yield
|
|
123
|
+
instance_variable_set(cache_variable, { key:, value: }.freeze)
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def license_cache_key
|
|
129
|
+
ReactOnRailsPro.configuration.license_token&.strip.presence || ENV_LICENSE_CACHE_KEY
|
|
130
|
+
end
|
|
131
|
+
|
|
134
132
|
# Determines the license status by loading, decoding, and validating
|
|
135
133
|
# @return [Symbol] The license status
|
|
136
134
|
def determine_license_status
|
|
@@ -219,10 +217,12 @@ module ReactOnRailsPro
|
|
|
219
217
|
true
|
|
220
218
|
end
|
|
221
219
|
|
|
222
|
-
# Loads license string from environment
|
|
220
|
+
# Loads the license string from explicit configuration, then the environment.
|
|
221
|
+
# Blank configured values fall through to preserve the environment-variable default.
|
|
223
222
|
# @return [String, nil] License string or nil if not found
|
|
224
223
|
def load_license_string
|
|
225
|
-
|
|
224
|
+
configured_token = ReactOnRailsPro.configuration.license_token&.strip.presence
|
|
225
|
+
configured_token || ENV.fetch("REACT_ON_RAILS_PRO_LICENSE", nil)&.strip.presence
|
|
226
226
|
end
|
|
227
227
|
|
|
228
228
|
# Decodes and verifies the JWT license
|