react_on_rails_pro 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.
- checksums.yaml +7 -0
- data/.controlplane/Dockerfile +49 -0
- data/.controlplane/controlplane.yml +22 -0
- data/.controlplane/gvc.yml +25 -0
- data/.controlplane/postgres.yml +33 -0
- data/.controlplane/rails.yml +49 -0
- data/.controlplane/redis.yml +18 -0
- data/.gitignore +77 -0
- data/.prettierignore +12 -0
- data/.prettierrc +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +120 -0
- data/.scss-lint.yml +205 -0
- data/CHANGELOG.md +570 -0
- data/CI_SETUP.md +502 -0
- data/CONTRIBUTING.md +376 -0
- data/Dockerfile +63 -0
- data/Gemfile +8 -0
- data/Gemfile.development_dependencies +74 -0
- data/Gemfile.loader +32 -0
- data/Gemfile.lock +527 -0
- data/LICENSE +98 -0
- data/LICENSE_SETUP.md +272 -0
- data/README.md +577 -0
- data/Rakefile +13 -0
- data/app/controllers/react_on_rails_pro/rsc_payload_controller.rb +7 -0
- data/app/helpers/react_on_rails_pro_helper.rb +360 -0
- data/app/views/react_on_rails_pro/rsc_payload.html.erb +1 -0
- data/babel.config.js +4 -0
- data/docs/bundle-caching.md +205 -0
- data/docs/caching.md +234 -0
- data/docs/code-splitting-loadable-components.md +313 -0
- data/docs/code-splitting.md +349 -0
- data/docs/configuration.md +165 -0
- data/docs/contributors-info/onboarding-customers.md +6 -0
- data/docs/contributors-info/releasing.md +40 -0
- data/docs/contributors-info/style.md +33 -0
- data/docs/home-pro.md +146 -0
- data/docs/installation.md +203 -0
- data/docs/js-memory-leaks.md +22 -0
- data/docs/node-renderer/basics.md +92 -0
- data/docs/node-renderer/debugging.md +38 -0
- data/docs/node-renderer/error-reporting-and-tracing.md +160 -0
- data/docs/node-renderer/heroku.md +102 -0
- data/docs/node-renderer/js-configuration.md +91 -0
- data/docs/node-renderer/troubleshooting.md +5 -0
- data/docs/profiling-server-side-rendering-code.md +179 -0
- data/docs/react-server-components/add-streaming-and-interactivity.md +190 -0
- data/docs/react-server-components/create-without-ssr.md +448 -0
- data/docs/react-server-components/glossary.md +102 -0
- data/docs/react-server-components/how-react-server-components-work.md +243 -0
- data/docs/react-server-components/inside-client-components.md +332 -0
- data/docs/react-server-components/purpose-and-benefits.md +243 -0
- data/docs/react-server-components/rendering-flow.md +86 -0
- data/docs/react-server-components/selective-hydration-in-streamed-components.md +75 -0
- data/docs/react-server-components/server-side-rendering.md +72 -0
- data/docs/react-server-components/tutorial.md +19 -0
- data/docs/release-notes/4.0.md +94 -0
- data/docs/release-notes/v4-react-server-components.md +66 -0
- data/docs/ruby-api.md +11 -0
- data/docs/streaming-server-rendering.md +210 -0
- data/docs/troubleshooting.md +24 -0
- data/docs/updating.md +219 -0
- data/eslint.config.mjs +220 -0
- data/lib/react_on_rails_pro/assets_precompile.rb +230 -0
- data/lib/react_on_rails_pro/cache.rb +88 -0
- data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +38 -0
- data/lib/react_on_rails_pro/concerns/stream.rb +103 -0
- data/lib/react_on_rails_pro/configuration.rb +228 -0
- data/lib/react_on_rails_pro/constants.rb +8 -0
- data/lib/react_on_rails_pro/engine.rb +24 -0
- data/lib/react_on_rails_pro/error.rb +14 -0
- data/lib/react_on_rails_pro/license_public_key.rb +30 -0
- data/lib/react_on_rails_pro/license_validator.rb +188 -0
- data/lib/react_on_rails_pro/prepare_node_renderer_bundles.rb +40 -0
- data/lib/react_on_rails_pro/rendering_error.rb +5 -0
- data/lib/react_on_rails_pro/request.rb +318 -0
- data/lib/react_on_rails_pro/routes.rb +13 -0
- data/lib/react_on_rails_pro/server_rendering_js_code.rb +102 -0
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +133 -0
- data/lib/react_on_rails_pro/server_rendering_pool/pro_rendering.rb +117 -0
- data/lib/react_on_rails_pro/stream_cache.rb +61 -0
- data/lib/react_on_rails_pro/stream_request.rb +170 -0
- data/lib/react_on_rails_pro/utils.rb +222 -0
- data/lib/react_on_rails_pro/v8_log_processor.rb +50 -0
- data/lib/react_on_rails_pro/version.rb +6 -0
- data/lib/react_on_rails_pro.rb +23 -0
- data/package-scripts.yml +109 -0
- data/package.json +159 -0
- data/rakelib/dummy_apps.rake +22 -0
- data/rakelib/lint.rake +32 -0
- data/rakelib/public_key_management.rake +155 -0
- data/rakelib/rbs.rake +47 -0
- data/rakelib/run_rspec.rake +81 -0
- data/rakelib/task_helpers.rb +45 -0
- data/rakelib/yard.rake +20 -0
- data/react_on_rails_pro.gemspec +47 -0
- data/readme-gen-docs.md +1 -0
- data/script/bootstrap +33 -0
- data/script/preinstall.js +31 -0
- data/script/setup +23 -0
- data/script/test +38 -0
- data/sig/react_on_rails_pro/cache.rbs +13 -0
- data/sig/react_on_rails_pro/configuration.rbs +100 -0
- data/sig/react_on_rails_pro/error.rbs +4 -0
- data/sig/react_on_rails_pro/utils.rbs +7 -0
- data/sig/react_on_rails_pro.rbs +5 -0
- data/yarn.lock +7599 -0
- metadata +319 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRailsPro
|
|
4
|
+
module ServerRenderingJsCode
|
|
5
|
+
class << self
|
|
6
|
+
def ssr_pre_hook_js
|
|
7
|
+
ReactOnRailsPro.configuration.ssr_pre_hook_js || ""
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Generates the JavaScript function used for React Server Components payload generation
|
|
11
|
+
# Returns the JavaScript code that defines the generateRSCPayload function.
|
|
12
|
+
# It also adds necessary information to the railsContext to generate the RSC payload for any component in the app.
|
|
13
|
+
# @return [String] JavaScript code for RSC payload generation
|
|
14
|
+
def generate_rsc_payload_js_function(render_options)
|
|
15
|
+
return "" unless ReactOnRailsPro.configuration.enable_rsc_support && render_options.streaming?
|
|
16
|
+
|
|
17
|
+
if render_options.rsc_payload_streaming?
|
|
18
|
+
# When already on RSC bundle, we prevent further RSC payload generation
|
|
19
|
+
# by throwing an error if generateRSCPayload is called
|
|
20
|
+
return <<-JS
|
|
21
|
+
if (typeof generateRSCPayload !== 'function') {
|
|
22
|
+
globalThis.generateRSCPayload = function generateRSCPayload() {
|
|
23
|
+
throw new Error('The rendering request is already running on the RSC bundle. Please ensure that generateRSCPayload is only called from any React Server Component.')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
JS
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# To minimize the size of the HTTP request body sent to the node renderer,
|
|
30
|
+
# we reuse the existing rendering request string within the generateRSCPayload function.
|
|
31
|
+
# This approach allows us to simply replace the component name and props,
|
|
32
|
+
# rather than rewriting the entire rendering request.
|
|
33
|
+
# This regex finds the empty function call pattern `()` and replaces it with the component and props
|
|
34
|
+
<<-JS
|
|
35
|
+
railsContext.serverSideRSCPayloadParameters = {
|
|
36
|
+
renderingRequest,
|
|
37
|
+
rscBundleHash: '#{ReactOnRailsPro::Utils.rsc_bundle_hash}',
|
|
38
|
+
}
|
|
39
|
+
if (typeof generateRSCPayload !== 'function') {
|
|
40
|
+
globalThis.generateRSCPayload = function generateRSCPayload(componentName, props, railsContext) {
|
|
41
|
+
const { renderingRequest, rscBundleHash } = railsContext.serverSideRSCPayloadParameters;
|
|
42
|
+
const propsString = JSON.stringify(props);
|
|
43
|
+
const newRenderingRequest = renderingRequest.replace(/\\(\\s*\\)\\s*$/, `('${componentName}', ${propsString})`);
|
|
44
|
+
return runOnOtherBundle(rscBundleHash, newRenderingRequest);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
JS
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Main rendering function that generates JavaScript code for server-side rendering
|
|
51
|
+
# @param props_string [String] JSON string of props to pass to the React component
|
|
52
|
+
# @param rails_context [String] JSON string of Rails context data
|
|
53
|
+
# @param redux_stores [String] JavaScript code for Redux stores initialization
|
|
54
|
+
# @param react_component_name [String] Name of the React component to render
|
|
55
|
+
# @param render_options [Object] Options that control the rendering behavior
|
|
56
|
+
# @return [String] JavaScript code that will render the React component on the server
|
|
57
|
+
def render(props_string, rails_context, redux_stores, react_component_name, render_options)
|
|
58
|
+
render_function_name =
|
|
59
|
+
if ReactOnRailsPro.configuration.enable_rsc_support && render_options.streaming?
|
|
60
|
+
# Select appropriate function based on whether the rendering request is running on server or rsc bundle
|
|
61
|
+
# As the same rendering request is used to generate the rsc payload and SSR the component.
|
|
62
|
+
"ReactOnRails.isRSCBundle ? 'serverRenderRSCReactComponent' : 'streamServerRenderedReactComponent'"
|
|
63
|
+
else
|
|
64
|
+
"'serverRenderReactComponent'"
|
|
65
|
+
end
|
|
66
|
+
rsc_params = if ReactOnRailsPro.configuration.enable_rsc_support && render_options.streaming?
|
|
67
|
+
config = ReactOnRailsPro.configuration
|
|
68
|
+
react_client_manifest_file = config.react_client_manifest_file
|
|
69
|
+
react_server_client_manifest_file = config.react_server_client_manifest_file
|
|
70
|
+
<<-JS
|
|
71
|
+
railsContext.reactClientManifestFileName = '#{react_client_manifest_file}';
|
|
72
|
+
railsContext.reactServerClientManifestFileName = '#{react_server_client_manifest_file}';
|
|
73
|
+
JS
|
|
74
|
+
else
|
|
75
|
+
""
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# This function is called with specific componentName and props when generateRSCPayload is invoked
|
|
79
|
+
# In that case, it replaces the empty () with ('componentName', props) in the rendering request
|
|
80
|
+
<<-JS
|
|
81
|
+
(function(componentName = '#{react_component_name}', props = undefined) {
|
|
82
|
+
var railsContext = #{rails_context};
|
|
83
|
+
#{rsc_params}
|
|
84
|
+
#{generate_rsc_payload_js_function(render_options)}
|
|
85
|
+
#{ssr_pre_hook_js}
|
|
86
|
+
#{redux_stores}
|
|
87
|
+
var usedProps = typeof props === 'undefined' ? #{props_string} : props;
|
|
88
|
+
return ReactOnRails[#{render_function_name}]({
|
|
89
|
+
name: componentName,
|
|
90
|
+
domNodeId: '#{render_options.dom_id}',
|
|
91
|
+
props: usedProps,
|
|
92
|
+
trace: #{render_options.trace},
|
|
93
|
+
railsContext: railsContext,
|
|
94
|
+
throwJsErrors: #{ReactOnRailsPro.configuration.throw_js_errors},
|
|
95
|
+
renderingReturnsPromises: #{ReactOnRailsPro.configuration.rendering_returns_promises},
|
|
96
|
+
});
|
|
97
|
+
})()
|
|
98
|
+
JS
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRailsPro
|
|
4
|
+
module ServerRenderingPool
|
|
5
|
+
# This implementation of the rendering pool uses NodeJS to execute javasript code
|
|
6
|
+
class NodeRenderingPool
|
|
7
|
+
RENDERED_HTML_KEY = "renderedHtml"
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :bundle_hash
|
|
11
|
+
|
|
12
|
+
def reset_pool
|
|
13
|
+
ReactOnRailsPro::Request.reset_connection
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset_pool_if_server_bundle_was_modified
|
|
17
|
+
# Resetting the pool for server bundle modifications is accomplished by changing the mtime
|
|
18
|
+
# of the server bundle in the request to the remote rendering server.
|
|
19
|
+
# In non-development mode, we don't need to re-read this value.
|
|
20
|
+
if @server_bundle_hash.blank? || ReactOnRails.configuration.development_mode
|
|
21
|
+
@server_bundle_hash = ReactOnRailsPro::Utils.bundle_hash
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless ReactOnRailsPro.configuration.enable_rsc_support &&
|
|
25
|
+
(@rsc_bundle_hash.blank? || ReactOnRails.configuration.development_mode)
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@rsc_bundle_hash = ReactOnRailsPro::Utils.rsc_bundle_hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def renderer_bundle_file_name
|
|
33
|
+
"#{ReactOnRailsPro::Utils.bundle_hash}.js"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def rsc_renderer_bundle_file_name
|
|
37
|
+
"#{ReactOnRailsPro::Utils.rsc_bundle_hash}.js"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# js_code: JavaScript expression that returns a string.
|
|
41
|
+
# Returns a Hash:
|
|
42
|
+
# html: string of HTML for direct insertion on the page by evaluating js_code
|
|
43
|
+
# consoleReplayScript: script for replaying console
|
|
44
|
+
# hasErrors: true if server rendering errors
|
|
45
|
+
# Note, js_code does not have to be based on React.
|
|
46
|
+
# js_code MUST RETURN json stringify Object
|
|
47
|
+
# Calling code will probably call 'html_safe' on return value before rendering to the view.
|
|
48
|
+
def exec_server_render_js(js_code, render_options)
|
|
49
|
+
# The secret sauce is passing self as the 3rd param, the js_evaluator
|
|
50
|
+
render_options.set_option(:throw_js_errors, ReactOnRailsPro.configuration.throw_js_errors)
|
|
51
|
+
ReactOnRails::ServerRenderingPool::RubyEmbeddedJavaScript
|
|
52
|
+
.exec_server_render_js(js_code, render_options, self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def eval_streaming_js(js_code, render_options)
|
|
56
|
+
path = prepare_render_path(js_code, render_options)
|
|
57
|
+
ReactOnRailsPro::Request.render_code_as_stream(
|
|
58
|
+
path,
|
|
59
|
+
js_code,
|
|
60
|
+
is_rsc_payload: ReactOnRailsPro.configuration.enable_rsc_support && render_options.rsc_payload_streaming?
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def eval_js(js_code, render_options, send_bundle: false)
|
|
65
|
+
path = prepare_render_path(js_code, render_options)
|
|
66
|
+
|
|
67
|
+
response = ReactOnRailsPro::Request.render_code(path, js_code, send_bundle)
|
|
68
|
+
|
|
69
|
+
case response.status
|
|
70
|
+
when 200
|
|
71
|
+
response.body
|
|
72
|
+
when ReactOnRailsPro::STATUS_SEND_BUNDLE
|
|
73
|
+
# To prevent infinite loop
|
|
74
|
+
ReactOnRailsPro::Error.raise_duplicate_bundle_upload_error if send_bundle
|
|
75
|
+
|
|
76
|
+
eval_js(js_code, render_options, send_bundle: true)
|
|
77
|
+
when 400
|
|
78
|
+
raise ReactOnRailsPro::Error,
|
|
79
|
+
"Renderer unhandled error at the VM level: #{response.status}:\n#{response.body}"
|
|
80
|
+
else
|
|
81
|
+
raise ReactOnRailsPro::Error,
|
|
82
|
+
"Unexpected response code from renderer: #{response.status}:\n#{response.body}"
|
|
83
|
+
end
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
raise e unless ReactOnRailsPro.configuration.renderer_use_fallback_exec_js
|
|
86
|
+
|
|
87
|
+
fallback_exec_js(js_code, render_options, e)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def server_bundle_hash
|
|
91
|
+
@server_bundle_hash ||= ReactOnRailsPro::Utils.bundle_hash
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def rsc_bundle_hash
|
|
95
|
+
@rsc_bundle_hash ||= ReactOnRailsPro::Utils.rsc_bundle_hash
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def prepare_render_path(js_code, render_options)
|
|
99
|
+
ReactOnRailsPro::ServerRenderingPool::ProRendering
|
|
100
|
+
.set_request_digest_on_render_options(js_code, render_options)
|
|
101
|
+
|
|
102
|
+
rsc_support_enabled = ReactOnRailsPro.configuration.enable_rsc_support
|
|
103
|
+
is_rendering_rsc_payload = rsc_support_enabled && render_options.rsc_payload_streaming?
|
|
104
|
+
bundle_hash = is_rendering_rsc_payload ? rsc_bundle_hash : server_bundle_hash
|
|
105
|
+
# TODO: Remove the request_digest. See https://github.com/shakacode/react_on_rails_pro/issues/119
|
|
106
|
+
# From the request path
|
|
107
|
+
# path = "/bundles/#{@bundle_hash}/render"
|
|
108
|
+
"/bundles/#{bundle_hash}/render/#{render_options.request_digest}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fallback_exec_js(js_code, render_options, error)
|
|
112
|
+
Rails.logger.warn do
|
|
113
|
+
"[ReactOnRailsPro] Falling back to ExecJS because of #{error}"
|
|
114
|
+
end
|
|
115
|
+
fallback_renderer = ReactOnRails::ServerRenderingPool::RubyEmbeddedJavaScript
|
|
116
|
+
|
|
117
|
+
# Pool is actually discarded btw requests:
|
|
118
|
+
# 1) not to keep ExecJS in memory once NodeRenderer is available back
|
|
119
|
+
# 2) to avoid issues with server bundle changes
|
|
120
|
+
fallback_renderer.reset_pool
|
|
121
|
+
result = fallback_renderer.eval_js(js_code, render_options)
|
|
122
|
+
fallback_renderer.instance_variable_set(:@js_context_pool, nil)
|
|
123
|
+
result
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if defined?(ScoutApm)
|
|
127
|
+
include ScoutApm::Tracer
|
|
128
|
+
instrument_method :exec_server_render_js, type: "ReactOnRails", name: "Node React Server Rendering"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRailsPro
|
|
4
|
+
module ServerRenderingPool
|
|
5
|
+
class ProRendering
|
|
6
|
+
RENDERED_HTML_KEY = "renderedHtml"
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def pool
|
|
10
|
+
@pool ||= if ReactOnRailsPro.configuration.node_renderer?
|
|
11
|
+
::ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
|
|
12
|
+
else
|
|
13
|
+
::ReactOnRails::ServerRenderingPool::RubyEmbeddedJavaScript
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
delegate :reset_pool_if_server_bundle_was_modified, :reset_pool, to: :pool
|
|
18
|
+
|
|
19
|
+
def exec_server_render_js(js_code, render_options)
|
|
20
|
+
::ReactOnRailsPro::Utils.with_trace(render_options.react_component_name) do
|
|
21
|
+
# See https://github.com/shakacode/react_on_rails_pro/issues/119 for why
|
|
22
|
+
# the digest is on the render options.
|
|
23
|
+
# TODO: the request digest should be removed unless prerender caching is used
|
|
24
|
+
set_request_digest_on_render_options(js_code, render_options)
|
|
25
|
+
|
|
26
|
+
# Cache non-streaming immediately. For streaming, optionally cache via write-through.
|
|
27
|
+
if cache_enabled_for?(render_options)
|
|
28
|
+
render_with_cache(js_code, render_options)
|
|
29
|
+
else
|
|
30
|
+
render_on_pool(js_code, render_options)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# See https://github.com/shakacode/react_on_rails_pro/issues/119 for why
|
|
36
|
+
# the digest is on the render options.
|
|
37
|
+
def set_request_digest_on_render_options(js_code, render_options)
|
|
38
|
+
return unless render_options.request_digest.blank?
|
|
39
|
+
|
|
40
|
+
digest = if render_options.random_dom_id?
|
|
41
|
+
Rails.logger.info do
|
|
42
|
+
"[ReactOnRailsPro] Rendering #{render_options.react_component_name}. " \
|
|
43
|
+
"Suggest setting `id` on react_component or setting react_on_rails.rb initializer " \
|
|
44
|
+
"config.random_dom_id to false for BETTER performance."
|
|
45
|
+
end
|
|
46
|
+
Digest::MD5.hexdigest(without_random_values(js_code))
|
|
47
|
+
else
|
|
48
|
+
Digest::MD5.hexdigest(js_code)
|
|
49
|
+
end
|
|
50
|
+
render_options.request_digest = digest
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def cache_enabled_for?(render_options)
|
|
56
|
+
ReactOnRailsPro.configuration.prerender_caching &&
|
|
57
|
+
render_options.internal_option(:skip_prerender_cache).nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render_with_cache(js_code, render_options)
|
|
61
|
+
prerender_cache_key = cache_key(js_code, render_options)
|
|
62
|
+
prerender_cache_hit = true
|
|
63
|
+
|
|
64
|
+
result = if render_options.streaming?
|
|
65
|
+
render_streaming_with_cache(prerender_cache_key, js_code, render_options)
|
|
66
|
+
else
|
|
67
|
+
Rails.cache.fetch(prerender_cache_key) do
|
|
68
|
+
prerender_cache_hit = false
|
|
69
|
+
render_on_pool(js_code, render_options)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Pass back the cache key in the results only if the result is a Hash
|
|
74
|
+
if result.is_a?(Hash)
|
|
75
|
+
result[:RORP_CACHE_KEY] = prerender_cache_key
|
|
76
|
+
result[:RORP_CACHE_HIT] = prerender_cache_hit
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_streaming_with_cache(prerender_cache_key, js_code, render_options)
|
|
83
|
+
# Streaming path: try to serve from cache; otherwise wrap upstream stream
|
|
84
|
+
cached_stream = ReactOnRailsPro::StreamCache.fetch_stream(prerender_cache_key)
|
|
85
|
+
return cached_stream if cached_stream
|
|
86
|
+
|
|
87
|
+
upstream = render_on_pool(js_code, render_options)
|
|
88
|
+
ReactOnRailsPro::StreamCache.wrap_and_cache(
|
|
89
|
+
prerender_cache_key,
|
|
90
|
+
upstream,
|
|
91
|
+
cache_options: render_options.internal_option(:cache_options)
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def without_random_values(js_code)
|
|
96
|
+
# domNodeId are random to enable multiple instance of the same react component on a page.
|
|
97
|
+
# See https://github.com/shakacode/react_on_rails_pro/issues/44
|
|
98
|
+
js_code.gsub(/domNodeId: '[\w-]*',/, "")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cache_key(js_code, render_options)
|
|
102
|
+
set_request_digest_on_render_options(js_code, render_options)
|
|
103
|
+
|
|
104
|
+
[
|
|
105
|
+
*ReactOnRailsPro::Cache.base_cache_key("ror_pro_rendered_html",
|
|
106
|
+
prerender: render_options.prerender),
|
|
107
|
+
render_options.request_digest
|
|
108
|
+
]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_on_pool(js_code, render_options)
|
|
112
|
+
pool.exec_server_render_js(js_code, render_options)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRailsPro
|
|
4
|
+
class StreamCache
|
|
5
|
+
class << self
|
|
6
|
+
# Returns a stream-like object that responds to `each_chunk` and yields cached chunks
|
|
7
|
+
# or nil if not present in cache.
|
|
8
|
+
def fetch_stream(cache_key)
|
|
9
|
+
cached_chunks = Rails.cache.read(cache_key)
|
|
10
|
+
return nil unless cached_chunks.is_a?(Array)
|
|
11
|
+
|
|
12
|
+
build_stream_from_chunks(cached_chunks)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Wraps an upstream stream (responds to `each_chunk`), yields chunks downstream while
|
|
16
|
+
# buffering them, and writes the chunks array to Rails.cache on successful completion.
|
|
17
|
+
# Returns a stream-like object that responds to `each_chunk`.
|
|
18
|
+
def wrap_and_cache(cache_key, upstream_stream, cache_options: nil)
|
|
19
|
+
component = CachingComponent.new(upstream_stream, cache_key, cache_options)
|
|
20
|
+
ReactOnRailsPro::StreamDecorator.new(component)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Builds a stream-like object from an array of chunks.
|
|
24
|
+
def build_stream_from_chunks(chunks)
|
|
25
|
+
component = CachedChunksComponent.new(chunks)
|
|
26
|
+
ReactOnRailsPro::StreamDecorator.new(component)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class CachedChunksComponent
|
|
31
|
+
def initialize(chunks)
|
|
32
|
+
@chunks = chunks
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def each_chunk(&block)
|
|
36
|
+
return enum_for(:each_chunk) unless block
|
|
37
|
+
|
|
38
|
+
@chunks.each(&block)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class CachingComponent
|
|
43
|
+
def initialize(upstream_stream, cache_key, cache_options)
|
|
44
|
+
@upstream_stream = upstream_stream
|
|
45
|
+
@cache_key = cache_key
|
|
46
|
+
@cache_options = cache_options
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def each_chunk(&block)
|
|
50
|
+
return enum_for(:each_chunk) unless block
|
|
51
|
+
|
|
52
|
+
buffered_chunks = []
|
|
53
|
+
@upstream_stream.each_chunk do |chunk|
|
|
54
|
+
buffered_chunks << chunk
|
|
55
|
+
yield(chunk)
|
|
56
|
+
end
|
|
57
|
+
Rails.cache.write(@cache_key, buffered_chunks, @cache_options || {})
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRailsPro
|
|
4
|
+
class StreamDecorator
|
|
5
|
+
def initialize(component)
|
|
6
|
+
@component = component
|
|
7
|
+
# @type [Array[Proc]]
|
|
8
|
+
# Proc receives 2 arguments: chunk, position
|
|
9
|
+
# @param chunk [String] The chunk to be processed
|
|
10
|
+
# @param position [Symbol] The position of the chunk in the stream (:first, :middle, or :last)
|
|
11
|
+
# The position parameter is used by actions that add content to the beginning or end of the stream
|
|
12
|
+
@actions = [] # List to store all actions
|
|
13
|
+
@rescue_blocks = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Add a prepend action
|
|
17
|
+
def prepend
|
|
18
|
+
@actions << ->(chunk, position) { position == :first ? "#{yield}#{chunk}" : chunk }
|
|
19
|
+
self # Return self to allow chaining
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Add a transformation action
|
|
23
|
+
def transform
|
|
24
|
+
@actions << lambda { |chunk, position|
|
|
25
|
+
if position == :last && chunk.empty?
|
|
26
|
+
# Return the empty chunk without modification for the last chunk
|
|
27
|
+
# This is related to the `handleChunk(:last, "")` call which gets all the appended content
|
|
28
|
+
# We don't want to make an extra call to the transformer block if there is no content appended
|
|
29
|
+
chunk
|
|
30
|
+
else
|
|
31
|
+
yield(chunk)
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
self # Return self to allow chaining
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Add an append action
|
|
38
|
+
def append
|
|
39
|
+
@actions << ->(chunk, position) { position == :last ? "#{chunk}#{yield}" : chunk }
|
|
40
|
+
self # Return self to allow chaining
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def rescue(&block)
|
|
44
|
+
@rescue_blocks << block
|
|
45
|
+
self # Return self to allow chaining
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_chunk(chunk, position)
|
|
49
|
+
@actions.reduce(chunk) do |acc, action|
|
|
50
|
+
action.call(acc, position)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def each_chunk(&block) # rubocop:disable Metrics/CyclomaticComplexity
|
|
55
|
+
return enum_for(:each_chunk) unless block
|
|
56
|
+
|
|
57
|
+
first_chunk = true
|
|
58
|
+
@component.each_chunk do |chunk|
|
|
59
|
+
position = first_chunk ? :first : :middle
|
|
60
|
+
modified_chunk = handle_chunk(chunk, position)
|
|
61
|
+
yield(modified_chunk)
|
|
62
|
+
first_chunk = false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The last chunk contains the append content after the transformation
|
|
66
|
+
# All transformations are applied to the append content
|
|
67
|
+
last_chunk = handle_chunk("", :last)
|
|
68
|
+
yield(last_chunk) unless last_chunk.empty?
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
current_error = e
|
|
71
|
+
rescue_block_index = 0
|
|
72
|
+
while current_error.present? && (rescue_block_index < @rescue_blocks.size)
|
|
73
|
+
begin
|
|
74
|
+
@rescue_blocks[rescue_block_index].call(current_error, &block)
|
|
75
|
+
current_error = nil
|
|
76
|
+
rescue StandardError => inner_error
|
|
77
|
+
current_error = inner_error
|
|
78
|
+
end
|
|
79
|
+
rescue_block_index += 1
|
|
80
|
+
end
|
|
81
|
+
raise current_error if current_error.present?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class StreamRequest
|
|
86
|
+
def initialize(&request_block)
|
|
87
|
+
@request_executor = request_block
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_class_method :new
|
|
91
|
+
|
|
92
|
+
def each_chunk(&block)
|
|
93
|
+
return enum_for(:each_chunk) unless block
|
|
94
|
+
|
|
95
|
+
send_bundle = false
|
|
96
|
+
error_body = +""
|
|
97
|
+
loop do
|
|
98
|
+
stream_response = @request_executor.call(send_bundle)
|
|
99
|
+
|
|
100
|
+
# Chunks can be merged during streaming, so we separate them by newlines
|
|
101
|
+
# Also, we check the status code inside the loop block because calling `status` outside the loop block
|
|
102
|
+
# is blocking, it will wait for the response to be fully received
|
|
103
|
+
# Look at the spec of `status` in `spec/react_on_rails_pro/stream_spec.rb` for more details
|
|
104
|
+
process_response_chunks(stream_response, error_body, &block)
|
|
105
|
+
break
|
|
106
|
+
rescue HTTPX::HTTPError => e
|
|
107
|
+
send_bundle = handle_http_error(e, error_body, send_bundle)
|
|
108
|
+
rescue HTTPX::ReadTimeoutError => e
|
|
109
|
+
raise ReactOnRailsPro::Error, "Time out error while server side render streaming a component.\n" \
|
|
110
|
+
"Original error:\n#{e}\n#{e.backtrace}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def process_response_chunks(stream_response, error_body)
|
|
115
|
+
loop_response_lines(stream_response) do |chunk|
|
|
116
|
+
if stream_response.is_a?(HTTPX::ErrorResponse) || stream_response.status >= 400
|
|
117
|
+
error_body << chunk
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
processed_chunk = chunk.strip
|
|
122
|
+
yield processed_chunk unless processed_chunk.empty?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_http_error(error, error_body, send_bundle)
|
|
127
|
+
response = error.response
|
|
128
|
+
case response.status
|
|
129
|
+
when ReactOnRailsPro::STATUS_SEND_BUNDLE
|
|
130
|
+
# To prevent infinite loop
|
|
131
|
+
ReactOnRailsPro::Error.raise_duplicate_bundle_upload_error if send_bundle
|
|
132
|
+
|
|
133
|
+
true
|
|
134
|
+
when ReactOnRailsPro::STATUS_INCOMPATIBLE
|
|
135
|
+
raise ReactOnRailsPro::Error, error_body
|
|
136
|
+
else
|
|
137
|
+
raise ReactOnRailsPro::Error, "Unexpected response code from renderer: #{response.status}:\n#{error_body}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Method to start the decoration
|
|
142
|
+
def self.create(&request_block)
|
|
143
|
+
StreamDecorator.new(new(&request_block))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# This method is considered as an override of response.each_line
|
|
149
|
+
# It fixes the problem of not yielding the last chunk on error
|
|
150
|
+
# You can check the spec of `each_line` in `spec/react_on_rails_pro/stream_spec.rb` for more details
|
|
151
|
+
def loop_response_lines(response)
|
|
152
|
+
return enum_for(__method__, response) unless block_given?
|
|
153
|
+
|
|
154
|
+
line = "".b
|
|
155
|
+
|
|
156
|
+
response.each do |chunk|
|
|
157
|
+
response.instance_variable_set(:@react_on_rails_received_first_chunk, true)
|
|
158
|
+
line << chunk
|
|
159
|
+
|
|
160
|
+
while (idx = line.index("\n"))
|
|
161
|
+
yield line.byteslice(0..idx - 1)
|
|
162
|
+
|
|
163
|
+
line = line.byteslice(idx + 1..-1)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
ensure
|
|
167
|
+
yield line unless line.empty?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|