-
-
Notifications
You must be signed in to change notification settings - Fork 638
Add support for console replay while streaming component #1647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for console replay while streaming component #1647
Conversation
WalkthroughThe recent changes encompass updates to several files, enhancing the project's functionality by introducing new methods for streaming React components and improving console log handling during server rendering. Key updates include the addition of the Changes
Possibly related PRs
Suggested reviewers
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Inline review comments failed to post
Actionable comments posted: 5
Outside diff range, codebase verification and nitpick comments (1)
package.json (1)
18-19: Deprecated React API DetectedThe codebase contains deprecated React API usage that needs to be addressed for compatibility with React 18:
spec/dummy/client/app/startup/HelloWorldES5.jsx: ReplaceReact.createClasswith ES6 class components or functional components with hooks.Please update these instances to ensure compatibility with the latest React version.
Analysis chain
Approve dependency upgrades but verify compatibility.
The upgrades to React and React DOM, along with their type definitions, are essential for staying current with the latest features. However, it's crucial to ensure that these changes are thoroughly tested to avoid potential breaking changes due to the major version upgrade.
The changes to the dependencies are approved.
Run the following script to verify compatibility with the new React version:
Also applies to: 42-43
Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify that no deprecated React APIs are used in the codebase. # Test: Search for deprecated React APIs. Expect: No occurrences. rg --type javascript --type typescript 'React\.createClass|React\.PropTypes'Length of output: 113
Script:
#!/bin/bash # Description: Verify that no deprecated React APIs are used in the codebase. # Test: Search for deprecated React APIs. Expect: No occurrences. rg --glob '*.js' --glob '*.jsx' --glob '*.ts' --glob '*.tsx' 'React\.createClass|React\.PropTypes'Length of output: 215
Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
Files selected for processing (16)
- .gitignore (1 hunks)
- jest.config.js (1 hunks)
- lib/react_on_rails/helper.rb (5 hunks)
- lib/react_on_rails/react_component/render_options.rb (1 hunks)
- lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (3 hunks)
- node_package/src/ReactOnRails.ts (2 hunks)
- node_package/src/buildConsoleReplay.ts (2 hunks)
- node_package/src/serverRenderReactComponent.ts (2 hunks)
- node_package/src/types/index.ts (2 hunks)
- node_package/tests/ReactOnRails.test.js (1 hunks)
- node_package/tests/jest.setup.js (1 hunks)
- package.json (2 hunks)
- spec/dummy/config/webpack/alias.js (1 hunks)
- spec/dummy/config/webpack/commonWebpackConfig.js (1 hunks)
- spec/dummy/config/webpack/webpackConfig.js (1 hunks)
- spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (2 hunks)
Files skipped from review due to trivial changes (1)
- .gitignore
Additional context used
Biome
node_package/src/buildConsoleReplay.ts
[error] 17-17: Use Array.isArray() instead of instanceof Array.
instanceof Array returns false for array-like objects and arrays from other execution contexts.
Unsafe fix: Use Array.isArray() instead.(lint/suspicious/useIsArray)
rubocop
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb
[convention] 60-60: Use 2 (not -7) spaces for indentation.
(Layout/IndentationWidth)
[convention] 61-61: Align
elsewithif.(Layout/ElseAlignment)
[warning] 63-63:
endat 63, 12 is not aligned withifat 59, 21.(Layout/EndAlignment)
[convention] 78-78: Trailing whitespace detected.
(Layout/TrailingWhitespace)
lib/react_on_rails/helper.rb
[warning] 402-402: Useless assignment to variable -
rendered_html_stream.(Lint/UselessAssignment)
[convention] 414-414: Line is too long. [130/120]
(Layout/LineLength)
Additional comments not posted (16)
jest.config.js (1)
4-4: Approved: Addition of setupFiles in Jest configuration.The addition of
setupFilesto the Jest configuration is correctly implemented and enhances the testing setup by allowing for the execution of initialization code before tests run.spec/dummy/config/webpack/alias.js (1)
7-7: Approved: Addition of stream alias in Webpack configuration.The new alias mapping for
streamtostream-browserifyis correctly implemented. This change enhances module resolution and compatibility with browser environments, facilitating the use of streaming functionalities in client-side applications.spec/dummy/config/webpack/webpackConfig.js (1)
7-7: Approved: Addition of stream-browserify as a fallback.This change enhances client-side compatibility by providing a fallback for the
streammodule usingstream-browserify. It's a good practice to ensure broader environment support.The code change is approved.
Consider verifying this fallback integration by testing in environments that do not natively support the Node.js
streammodule to ensure smooth functionality.node_package/src/buildConsoleReplay.ts (2)
14-21: Approved: Enhanced flexibility in console message handling.The addition of the
skipFirstNumberOfMessagesparameter to bothconsoleReplayandbuildConsoleReplayfunctions enhances flexibility and user control over the output. This is a valuable feature for managing console logs more effectively during server rendering.The code changes are approved.
Also applies to: 42-43
Tools
Biome
[error] 17-17: Use Array.isArray() instead of instanceof Array.
instanceof Array returns false for array-like objects and arrays from other execution contexts.
Unsafe fix: Use Array.isArray() instead.(lint/suspicious/useIsArray)
21-21: Fix required: Use Array.isArray() instead of instanceof Array.The use of
instanceof Arraycan lead to incorrect type checks in certain contexts. Replace it withArray.isArray()to ensure reliable and accurate type checking.Apply this diff to address the issue:
- if (!(console.history instanceof Array)) { + if (!Array.isArray(console.history)) { return ''; }Likely invalid or redundant comment.
spec/dummy/config/webpack/commonWebpackConfig.js (1)
44-44: Approved: Addition of process/browser to ProvidePlugin.This change enhances client-side compatibility by polyfilling the
processvariable, which is crucial for libraries that rely on Node.js-like environments. It's a good practice to ensure broader library support.The code change is approved.
Consider verifying this polyfill integration by testing with libraries that rely on the
processobject to ensure smooth functionality.Verification successful
Verification Complete: process/browser Polyfill Integration Validated
The addition of
process: 'process/browser'in the Webpack configuration is appropriate. It ensures compatibility for client-side code that relies on theprocessobject, as evidenced by its usage in various configuration files. This change supports the correct functioning of environment-dependent code in client-side applications.
- The
processobject is used in several Webpack and configuration files, indicating the necessity of this polyfill.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check the integration of process/browser polyfill in different environments. # Test: Ensure that libraries relying on the `process` object function correctly with this polyfill. echo "Manual testing required: Verify process/browser polyfill with libraries depending on the process object."Length of output: 218
Script:
#!/bin/bash # Search for occurrences of 'process' in the codebase to identify dependencies or code that might rely on this object. rg '\bprocess\b' -A 3Length of output: 22959
lib/react_on_rails/react_component/render_options.rb (1)
106-108: Approve the addition of thestream?method.The new
stream?method is a useful addition for checking the streaming status of components. Ensure that this method is integrated and utilized properly in the context of component rendering.The addition of the
stream?method is approved.Run the following script to verify the integration of the
stream?method:Verification successful
The
stream?method is properly integrated and utilized.The
stream?method is effectively used in the codebase to manage rendering logic based on streaming status. Its integration inhelper.rbandruby_embedded_java_script.rbconfirms its role in enhancing component rendering control.
- File:
lib/react_on_rails/helper.rb- File:
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rbScripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify the usage of the `stream?` method in the codebase. # Test: Search for the usage of the `stream?` method. Expect: Proper integration in component rendering logic. rg --type ruby 'stream\?'Length of output: 625
node_package/tests/ReactOnRails.test.js (1)
22-30: Approve the updated test setup but verify its effectiveness.The modifications to the test setup, including the dynamic creation of the root element and the updated assertion, are well-thought-out improvements. However, it's crucial to ensure that these changes accurately reflect the intended behavior and enhance the test's effectiveness.
The changes to the test setup are approved.
Run the following script to verify the effectiveness of the new test setup:
Verification successful
The test setup in
ReactOnRails.test.jsis consistent and effective.The usage of
document.createElementandappendChildin bothReactOnRails.test.jsandAuthenticity.test.jsdemonstrates a consistent approach to DOM manipulation in the test suite. This consistency supports the effectiveness of the updated test setup in accurately reflecting the rendered output. No issues were found with the current implementation.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify that the updated test setup accurately reflects the rendered output. # Test: Search for the usage of the new test setup in other test files. Expect: Consistent implementation and accurate test results. rg --type javascript 'document\.createElement|appendChild'Length of output: 95
Script:
#!/bin/bash # Description: Verify that the updated test setup accurately reflects the rendered output. # Test: Search for the usage of the new test setup in other test files. Expect: Consistent implementation and accurate test results. rg 'document\.createElement|appendChild' --glob '*.js'Length of output: 379
node_package/src/types/index.ts (2)
2-2: Approved: Import statement forReadable.The import of
Readablefrom thestreammodule is correctly added to support the new streaming functionality in theReactOnRailsinterface.
141-141: Approved: New methodstreamServerRenderedReactComponent.The addition of the
streamServerRenderedReactComponentmethod to theReactOnRailsinterface is correctly implemented. This method enhances the server-side rendering capabilities by enabling streaming, which aligns with the PR's objectives.node_package/src/ReactOnRails.ts (1)
245-251: Approved: Integration ofstreamServerRenderedReactComponentintoReactOnRails.The method
streamServerRenderedReactComponentis correctly integrated into thectx.ReactOnRailsobject, making it accessible as part of the module's API. The documentation comment provides clear guidance on its usage, which is beneficial for maintainability and developer understanding.lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (2)
229-248: Review new method for parsing and replaying console messages.The new method
parse_result_and_replay_console_messagesis crucial for handling console messages during server-side rendering. Ensure that the JSON parsing and error handling are robust, and that the method correctly logs messages to the server whenlogging_on_serveris enabled.The implementation seems robust, but consider adding more detailed logging or error handling if necessary.
79-83: Ensure proper handling of streamed output.The transformation of the result to handle streamed output should be carefully reviewed to ensure that each chunk is processed correctly and that there are no performance implications or errors in handling the streamed data.
Run the following script to verify the proper handling of streamed output:
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (2)
10-10: Confirm inclusion ofTagHelpermodule.The inclusion of the
ActionView::Helpers::TagHelpermodule should enhance the HTML tag generation capabilities of thePlainReactOnRailsHelperclass. Ensure that this inclusion does not introduce any conflicts or issues with existing methods.The change is approved, but verify that all methods that use HTML tag generation are still functioning as expected.
350-373: Review new tests forrails_context_if_not_already_rendered.The new tests for the
rails_context_if_not_already_renderedmethod should ensure that it behaves correctly in scenarios where the Rails context has and has not been rendered. Verify that the tests cover all relevant cases and that they are correctly set up to simulate these scenarios.The tests appear to be well-written and cover the necessary scenarios. Ensure that they are integrated correctly with the rest of the test suite.
lib/react_on_rails/helper.rb (1)
396-419: Ensure efficient handling of streamed content.The
build_react_component_result_for_server_streamed_contentmethod constructs the final output for streamed content. Verify that the method efficiently handles the first chunk differently from subsequent chunks and that it does not introduce any performance issues.Run the following script to verify the efficiency and correctness of the streamed content handling:
Tools
rubocop
[warning] 402-402: Useless assignment to variable -
rendered_html_stream.(Lint/UselessAssignment)
[convention] 414-414: Line is too long. [130/120]
(Layout/LineLength)
Comments failed to post (5)
node_package/tests/jest.setup.js (1)
1-13: Approved: Addition of TextEncoder and TextDecoder polyfill in jest.setup.js.
The polyfill for
TextEncoderandTextDecoderis correctly implemented to ensure compatibility with the jsdom environment used in Jest tests. The conditional checks are well-placed to prevent unnecessary redefinitions.Consider monitoring future updates to jsdom that might include native support for
TextEncoderandTextDecoder, making this polyfill redundant.node_package/src/serverRenderReactComponent.ts (1)
177-236: Approved with suggestions: New function
streamServerRenderedReactComponent.The implementation of
streamServerRenderedReactComponentis robust, incorporating necessary error handling and stream transformations. The use ofReactDOMServer.renderToPipeableStreamis appropriate for streaming server-rendered components.Suggestions:
- Consider adding more detailed logging for debugging purposes, especially around the stream transformation process.
- Ensure comprehensive testing of error scenarios to validate the stream's error handling capabilities.
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (1)
59-63: Refactor conditional structure for clarity.
The conditional structure for handling streaming and non-streaming scenarios can be simplified to improve readability. Consider using a ternary operator or extracting the condition into a separate method if it grows more complex.
Apply this diff to refactor the conditional structure:
- result = if render_options.stream? - js_evaluator.eval_streaming_js(js_code, render_options) - else - js_evaluator.eval_js(js_code, render_options) - end + result = render_options.stream? ? js_evaluator.eval_streaming_js(js_code, render_options) : js_evaluator.eval_js(js_code, render_options)Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.result = render_options.stream? ? js_evaluator.eval_streaming_js(js_code, render_options) : js_evaluator.eval_js(js_code, render_options)Tools
rubocop
[convention] 60-60: Use 2 (not -7) spaces for indentation.
(Layout/IndentationWidth)
[convention] 61-61: Align
elsewithif.(Layout/ElseAlignment)
[warning] 63-63:
endat 63, 12 is not aligned withifat 59, 21.(Layout/EndAlignment)
lib/react_on_rails/helper.rb (2)
94-114: Review new method for streaming React components.
The new
stream_react_componentmethod introduces streaming capabilities for React components using Ruby'sFiber. Ensure that the method handles errors appropriately and that the streaming process is efficient and robust.Consider adding more detailed error messages and handling potential exceptions during the streaming process more gracefully.
355-362: Optimize streaming content handling.
The
internal_stream_react_componentmethod modifies options to include a streaming flag and processes the component rendering accordingly. Ensure that the options modification does not inadvertently affect other parts of the codebase and that the streaming process is optimized for performance.Address the static analysis hint regarding the useless assignment to
rendered_html_streamand ensure that all modifications to options are localized to this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- One suggestion for readability
- I updated the description with the information from ChatGPT on how this works. Please double-check.
- Should we have any unit tests?
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb
Outdated
Show resolved
Hide resolved
f38ed50 to
0c2313e
Compare
64ce293 to
e67efaf
Compare
d5d5d68 to
8937c5f
Compare
bcfddd6 to
6ad16f1
Compare
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have a great and extensive summary... but it doesn't seem to be for this PR :) Please update it to be relevant to these changes only. The changelog may not need to be updated if it's covered in the previous PR.
Approving with that understanding.
d1df75f to
bd57e3d
Compare
bd57e3d to
49547bb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Outside diff range and nitpick comments (2)
node_package/src/buildConsoleReplay.ts (1)
Line range hint 12-48: Consider documenting streaming behavior.
Since this change is part of the streaming SSR implementation, consider adding JSDoc comments explaining how this function integrates with streaming, particularly how numberOfMessagesToSkip relates to chunk processing.
Example documentation:
/**
* Generates a console replay script for server-side rendering.
* @param customConsoleHistory - Optional custom console history to use instead of global
* @param numberOfMessagesToSkip - Number of initial messages to skip, useful when streaming
* to avoid replaying console messages from previous chunks
* @returns A string containing JavaScript code to replay console messages
*/CHANGELOG.md (1)
21-22: Fix indentation in changelog entry
The bullet point indentation should be consistent with other entries in the changelog.
Apply this diff to fix the indentation:
### Added
- Added support for replaying console logs that occur during server rendering of streamed React components. This enables debugging of server-side rendering issues by capturing and displaying console output on the client and on the server output. [PR #1647](https://github.com/shakacode/react_on_rails/pull/1647) by [AbanoubGhadban](https://github.com/AbanoubGhadban).🧰 Tools
🪛 Markdownlint
22-22: Expected: 0; Actual: 1
Unordered list indentation
(MD007, ul-indent)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (5)
- CHANGELOG.md (1 hunks)
- lib/react_on_rails/helper.rb (2 hunks)
- lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (4 hunks)
- node_package/src/buildConsoleReplay.ts (2 hunks)
- node_package/src/serverRenderReactComponent.ts (3 hunks)
🧰 Additional context used
🪛 Markdownlint
CHANGELOG.md
22-22: Expected: 0; Actual: 1
Unordered list indentation
(MD007, ul-indent)
🔇 Additional comments (6)
node_package/src/buildConsoleReplay.ts (1)
12-12: LGTM: Function signature changes maintain backward compatibility.
The addition of the optional numberOfMessagesToSkip parameter with a default value of 0 is well-implemented, ensuring backward compatibility while adding new functionality.
Also applies to: 47-48
node_package/src/serverRenderReactComponent.ts (2)
2-2: LGTM: Transform import added for streaming functionality.
The addition of Transform to the stream imports is appropriate for implementing the new chunk processing functionality.
207-207:
Consider potential race conditions in console history handling.
The current implementation might miss console messages if they are added between capturing the history and processing chunks. Consider implementing a locking mechanism or using a queue to ensure all console messages are captured accurately.
Consider this safer approach:
- const consoleHistory = console.history;
+ let currentHistory: typeof console.history = [];
+ const updateHistory = () => {
+ currentHistory = [...(console.history || [])];
+ };
+ updateHistory();Also applies to: 225-230
CHANGELOG.md (1)
21-22: Verify changelog entry matches PR objectives
The changelog entry accurately reflects the PR objectives regarding console replay support for streamed React components. The entry provides good context about the feature's purpose and benefits.
🧰 Tools
🪛 Markdownlint
22-22: Expected: 0; Actual: 1
Unordered list indentation
(MD007, ul-indent)
lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb (1)
59-63: Conditional logic correctly handles streaming and non-streaming modes
The updated conditional statement appropriately selects between eval_streaming_js and eval_js based on the stream? flag in render_options. This ensures that the correct evaluation method is used for each rendering mode.
lib/react_on_rails/helper.rb (1)
438-453: Stream transformation logic is correctly implemented
The implementation correctly handles the first chunk differently from subsequent chunks, ensuring that the component specification tag and Rails context are included only once. This approach optimizes the streaming of server-rendered content.
| } | ||
|
|
||
| const lines = consoleHistory.map(msg => { | ||
| const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add validation for numberOfMessagesToSkip parameter.
The slice operation could behave unexpectedly with negative numbers or values larger than the array length. Consider adding validation to ensure robust behavior.
Here's a suggested improvement:
+ if (numberOfMessagesToSkip < 0) {
+ throw new Error('numberOfMessagesToSkip must be non-negative');
+ }
+ if (numberOfMessagesToSkip > consoleHistory.length) {
+ return ''; // or throw error depending on desired behavior
+ }
const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => { | |
| if (numberOfMessagesToSkip < 0) { | |
| throw new Error('numberOfMessagesToSkip must be non-negative'); | |
| } | |
| if (numberOfMessagesToSkip > consoleHistory.length) { | |
| return ''; // or throw error depending on desired behavior | |
| } | |
| const lines = consoleHistory.slice(numberOfMessagesToSkip).map(msg => { |
| const jsonChunk = JSON.stringify({ | ||
| html: htmlChunk, | ||
| consoleReplayScript, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enhance security and performance of JSON serialization.
The current implementation might be vulnerable to XSS attacks through console messages and could have performance issues with large chunks.
Consider these improvements:
-const jsonChunk = JSON.stringify({
- html: htmlChunk,
- consoleReplayScript,
-});
+const jsonChunk = JSON.stringify({
+ html: htmlChunk,
+ consoleReplayScript: consoleReplayScript.replace(/<|>/g, match =>
+ match === '<' ? '\\u003c' : '\\u003e'
+ ),
+}, null, 0);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const jsonChunk = JSON.stringify({ | |
| html: htmlChunk, | |
| consoleReplayScript, | |
| }); | |
| const jsonChunk = JSON.stringify({ | |
| html: htmlChunk, | |
| consoleReplayScript: consoleReplayScript.replace(/<|>/g, match => | |
| match === '<' ? '\\u003c' : '\\u003e' | |
| ), | |
| }, null, 0); |
| ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(transformStream); | ||
|
|
||
| // TODO: Add console replay script to the stream | ||
| renderResult = transformStream; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error boundary and cleanup for streaming pipeline.
The streaming pipeline should handle errors gracefully and clean up resources properly.
Consider adding error handling:
-ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(transformStream);
-renderResult = transformStream;
+const stream = ReactDOMServer.renderToPipeableStream(reactRenderingResult);
+stream.on('error', (error) => {
+ transformStream.destroy(error);
+});
+transformStream.on('error', (error) => {
+ console.error('Error during streaming:', error);
+});
+stream.pipe(transformStream);
+renderResult = transformStream;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(transformStream); | |
| // TODO: Add console replay script to the stream | |
| renderResult = transformStream; | |
| const stream = ReactDOMServer.renderToPipeableStream(reactRenderingResult); | |
| stream.on('error', (error) => { | |
| transformStream.destroy(error); | |
| }); | |
| transformStream.on('error', (error) => { | |
| console.error('Error during streaming:', error); | |
| }); | |
| stream.pipe(transformStream); | |
| renderResult = transformStream; |
| const transformStream = new Transform({ | ||
| transform(chunk, _, callback) { | ||
| const htmlChunk = chunk.toString(); | ||
| const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages); | ||
| previouslyReplayedConsoleMessages = consoleHistory?.length || 0; | ||
|
|
||
| const jsonChunk = JSON.stringify({ | ||
| html: htmlChunk, | ||
| consoleReplayScript, | ||
| }); | ||
|
|
||
| this.push(jsonChunk); | ||
| callback(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling and backpressure management to transform stream.
The transform function lacks error handling and proper backpressure management, which could lead to memory issues with large streams or when errors occur during transformation.
Consider this improved implementation:
const transformStream = new Transform({
- transform(chunk, _, callback) {
+ async transform(chunk, encoding, callback) {
try {
- const htmlChunk = chunk.toString();
+ const htmlChunk = chunk.toString(encoding);
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages);
previouslyReplayedConsoleMessages = consoleHistory?.length || 0;
- const jsonChunk = JSON.stringify({
+ const jsonChunk = JSON.stringify({
html: htmlChunk,
consoleReplayScript,
- });
+ }) + '\n';
this.push(jsonChunk);
callback();
+ } catch (error) {
+ callback(error);
+ }
}
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const transformStream = new Transform({ | |
| transform(chunk, _, callback) { | |
| const htmlChunk = chunk.toString(); | |
| const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages); | |
| previouslyReplayedConsoleMessages = consoleHistory?.length || 0; | |
| const jsonChunk = JSON.stringify({ | |
| html: htmlChunk, | |
| consoleReplayScript, | |
| }); | |
| this.push(jsonChunk); | |
| callback(); | |
| } | |
| const transformStream = new Transform({ | |
| async transform(chunk, encoding, callback) { | |
| try { | |
| const htmlChunk = chunk.toString(encoding); | |
| const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages); | |
| previouslyReplayedConsoleMessages = consoleHistory?.length || 0; | |
| const jsonChunk = JSON.stringify({ | |
| html: htmlChunk, | |
| consoleReplayScript, | |
| }) + '\n'; | |
| this.push(jsonChunk); | |
| callback(); | |
| } catch (error) { | |
| callback(error); | |
| } | |
| } |
| # js_code MUST RETURN json stringify Object | ||
| # Calling code will probably call 'html_safe' on return value before rendering to the view. | ||
| # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity | ||
| # rubocop:disable Metrics/CyclomaticComplexity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider refactoring to reduce cyclomatic complexity
Disabling RuboCop's Metrics/CyclomaticComplexity for the exec_server_render_js method suggests that it may have become too complex. Refactoring this method could improve maintainability and readability, allowing you to re-enable the RuboCop check.
| return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream? | ||
|
|
||
| # TODO: merge with exec_server_render_js | ||
| def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil) | ||
| js_evaluator ||= self | ||
| js_evaluator.eval_streaming_js(js_code, render_options) | ||
| # Streamed component is returned as stream of strings. | ||
| # We need to parse each chunk and replay the console messages. | ||
| result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify that result responds to transform method
In the streaming case, the code calls result.transform { |chunk| ... }. Ensure that result is an object that responds to the transform method. If transform is not a valid method for result, consider using map or another appropriate enumerable method to process each chunk.
Apply this diff to use map instead of transform:
- result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) }
+ result.map { |chunk| parse_result_and_replay_console_messages(chunk, render_options) }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream? | |
| # TODO: merge with exec_server_render_js | |
| def exec_server_render_streaming_js(js_code, render_options, js_evaluator = nil) | |
| js_evaluator ||= self | |
| js_evaluator.eval_streaming_js(js_code, render_options) | |
| # Streamed component is returned as stream of strings. | |
| # We need to parse each chunk and replay the console messages. | |
| result.transform { |chunk| parse_result_and_replay_console_messages(chunk, render_options) } | |
| return parse_result_and_replay_console_messages(result, render_options) unless render_options.stream? | |
| # Streamed component is returned as stream of strings. | |
| # We need to parse each chunk and replay the console messages. | |
| result.map { |chunk| parse_result_and_replay_console_messages(chunk, render_options) } |
| def parse_result_and_replay_console_messages(result_string, render_options) | ||
| result = nil | ||
| begin | ||
| result = JSON.parse(result_string) | ||
| rescue JSON::ParserError => e | ||
| raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string) | ||
| end | ||
|
|
||
| if render_options.logging_on_server | ||
| console_script = result["consoleReplayScript"] | ||
| console_script_lines = console_script.split("\n") | ||
| # Regular expression to match console.log or console.error calls with SERVER prefix | ||
| re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/ | ||
| console_script_lines&.each do |line| | ||
| match = re.match(line) | ||
| # Log matched messages to Rails logger with react_on_rails prefix | ||
| Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match | ||
| end | ||
| end | ||
| result | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add nil checks for console_script to prevent potential errors
In the parse_result_and_replay_console_messages method, if result["consoleReplayScript"] is nil, calling split on it will raise a NoMethodError. It's advisable to check if console_script is present before proceeding. Similarly, ensure that console_script_lines is not nil before iterating.
Apply this diff to add the necessary nil checks:
def parse_result_and_replay_console_messages(result_string, render_options)
result = nil
begin
result = JSON.parse(result_string)
rescue JSON::ParserError => e
raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string)
end
if render_options.logging_on_server
console_script = result["consoleReplayScript"]
+ return result unless console_script
console_script_lines = console_script.split("\n")
+ return result unless console_script_lines
# Regular expression to match console.log or console.error calls with SERVER prefix
re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
console_script_lines.each do |line|
match = re.match(line)
# Log matched messages to Rails logger with react_on_rails prefix
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
end
end
result
end📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def parse_result_and_replay_console_messages(result_string, render_options) | |
| result = nil | |
| begin | |
| result = JSON.parse(result_string) | |
| rescue JSON::ParserError => e | |
| raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string) | |
| end | |
| if render_options.logging_on_server | |
| console_script = result["consoleReplayScript"] | |
| console_script_lines = console_script.split("\n") | |
| # Regular expression to match console.log or console.error calls with SERVER prefix | |
| re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/ | |
| console_script_lines&.each do |line| | |
| match = re.match(line) | |
| # Log matched messages to Rails logger with react_on_rails prefix | |
| Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match | |
| end | |
| end | |
| result | |
| end | |
| def parse_result_and_replay_console_messages(result_string, render_options) | |
| result = nil | |
| begin | |
| result = JSON.parse(result_string) | |
| rescue JSON::ParserError => e | |
| raise ReactOnRails::JsonParseError.new(parse_error: e, json: result_string) | |
| end | |
| if render_options.logging_on_server | |
| console_script = result["consoleReplayScript"] | |
| return result unless console_script | |
| console_script_lines = console_script.split("\n") | |
| return result unless console_script_lines | |
| # Regular expression to match console.log or console.error calls with SERVER prefix | |
| re = /console\.(?:log|error)\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/ | |
| console_script_lines.each do |line| | |
| match = re.match(line) | |
| # Log matched messages to Rails logger with react_on_rails prefix | |
| Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match | |
| end | |
| end | |
| result | |
| end |
| html_content = <<~HTML | ||
| #{rendered_output} | ||
| #{component_specification_tag} | ||
| #{console_script} | ||
| HTML | ||
| html_content.strip.html_safe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjust indentation in HEREDOC to prevent unintended whitespace
The inconsistent indentation inside the HEREDOC may introduce unwanted whitespace in the generated HTML output. To ensure the rendered HTML is formatted correctly, align the content within the HEREDOC.
Apply this diff to correct the indentation:
html_content = <<~HTML
- #{rendered_output}
- #{component_specification_tag}
- #{console_script}
+#{rendered_output}
+#{component_specification_tag}
+#{console_script}
HTML📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| html_content = <<~HTML | |
| #{rendered_output} | |
| #{component_specification_tag} | |
| #{console_script} | |
| HTML | |
| html_content.strip.html_safe | |
| html_content = <<~HTML | |
| #{rendered_output} | |
| #{component_specification_tag} | |
| #{console_script} | |
| HTML | |
| html_content.strip.html_safe |
| rendered_html_stream:, | ||
| component_specification_tag:, | ||
| render_options: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure required keyword is used for parameter validation and consistency
In the build_react_component_result_for_server_streamed_content method, the parameters are declared without the required method. To ensure that these parameters are not nil and to maintain consistency with other methods in the file, consider using the required helper to validate them.
Apply this diff to enforce parameter validation:
def build_react_component_result_for_server_streamed_content(
- rendered_html_stream:,
- component_specification_tag:,
- render_options:
+ rendered_html_stream: required("rendered_html_stream"),
+ component_specification_tag: required("component_specification_tag"),
+ render_options: required("render_options")
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| rendered_html_stream:, | |
| component_specification_tag:, | |
| render_options: | |
| rendered_html_stream: required("rendered_html_stream"), | |
| component_specification_tag: required("component_specification_tag"), | |
| render_options: required("render_options") |
Summary
This PR introduces the ability to replay console logs that happen in the react components streamed.
It makes every chunk returned from the server has the following format:
{ "html": "<div>chunk html</div>", "consoleReplayScript": "<script>console.log.apply(console, ['hello world']);</script>" }On the ruby side, the console replay script is returned in the HTML chunk to be replayed on the browser.
Description Auto-Generated by code-rabbit
Key Concepts in SSR and Streaming
Server-Side Rendering (SSR):
SSR generates the HTML for React components on the server and sends it to the client. This allows faster initial rendering on the client side because the HTML is already rendered when the page loads.
Streaming:
Instead of sending the entire HTML content at once after it has been fully rendered on the server, streaming allows the server to send partial chunks of HTML as they are rendered. This can improve the perceived load time for the client, as the client can begin processing the initial HTML while the server continues rendering the rest.
Streaming in the Code
The parts related to streaming are primarily in the
streamServerRenderedReactComponentfunction and involve streaming the React component's HTML output back to the client.streamServerRenderedReactComponentRenderParamsas input, which contains the details of the component to be rendered (name, props, DOM node ID, etc.).ReactDOMServer.renderToPipeableStreamReactDOMServer.renderToPipeableStreamis a new React feature introduced in React 18 for streaming SSR.pipemethod connects this stream to another stream (transformStream), allowing the rendered chunks to be processed and eventually sent to the client.Transform StreamTransformstream is used to process each chunk of HTML rendered by React.consoleReplayScript).Error Handling in Streams
handleErrorfunction generates an error message in the form of a string.stringToStream, ensuring that even errors are streamed back to the client.stringToStreamstringToStreamcreates a readable stream from a string.Summary of Streaming Flow
streamServerRenderedReactComponentgets the component to be rendered and begins the rendering process usingReactDOMServer.renderToPipeableStream.Transformstream, which appends console logs and serializes the result as JSON.stringToStream.This approach allows for efficient server-side rendering with streaming, which can improve performance and user experience by delivering parts of the content to the client as soon as they are ready, without waiting for the entire component to render.
This Ruby file is part of the
ReactOnRailsgem, which integrates React with Rails and enables server-side rendering (SSR) of React components. The streaming parts of this Ruby file relate closely to the streaming functionality in the TypeScript code you provided. Here's a breakdown of how the Ruby code handles SSR with a focus on streaming:Key Components of the Ruby File
react_componentMethod:This method renders a React component on the server and returns the HTML to be injected into the Rails view. It supports both client-side rendering (CSR) and server-side rendering (SSR).
stream_react_componentMethod:This method handles server-side rendering with streaming, which corresponds to the
streamServerRenderedReactComponentfunction in the TypeScript code. Instead of waiting for the entire component to be rendered before sending it to the client, it sends chunks of HTML as they are rendered.Streaming Logic in Ruby
stream_react_componentFiber Usage:
The
stream_react_componentmethod creates aFiberto control the streaming of HTML chunks. Fibers in Ruby are lightweight, cooperative threads that allow the code to yield and resume execution, which is perfect for handling streaming data in a controlled way.Streaming Chunks:
The Fiber calls
internal_stream_react_component, which returns a stream of chunks of HTML. The Fiber iterates through each chunk and sends it back to the Rails view as it becomes available.renderToPipeableStreamis used.Fiber Management:
The rendered chunks are managed by storing the rendering Fiber in an instance variable (
@rorp_rendering_fibers). The first chunk is sent by callingrendering_fiber.resume.internal_stream_react_componentOptions Modification:
This method ensures that the
stream?option is set totrue, signaling that the component should be streamed rather than fully rendered before being sent to the client.Streamed Content:
The result of
internal_react_componentis passed tobuild_react_component_result_for_server_streamed_content, which prepares the HTML chunks to be streamed back to the client.build_react_component_result_for_server_streamed_contentTransforming Streamed Chunks:
This method takes the stream of chunks from the rendered React component and processes them one by one. The first chunk is special because it contains the initial HTML along with the component specification tag (metadata about the component).
Subsequent Chunks:
For subsequent chunks, the HTML is processed and appended to the response, similar to how the
Transformstream in the TypeScript code appends the console logs and wraps the HTML in JSON.Console Replay:
Each chunk includes the option to replay console messages (
consoleReplayScript), as in the TypeScript code, where each HTML chunk includes embedded console logs.Relation to TypeScript Streaming Logic
Rendering Flow:
Both the Ruby and TypeScript code follow a similar flow for streaming:
Chunk Processing:
The Ruby method
build_react_component_result_for_server_streamed_contentclosely mirrors the functionality of theTransformstream in the TypeScript code. Both process each chunk of HTML, append necessary metadata (like console logs), and send it to the client.Fiber and Stream Handling:
The use of Fibers in Ruby allows the server to stream the chunks in a non-blocking manner, similar to how the Node.js
renderToPipeableStreamfunction allows React to stream chunks without waiting for the entire component to finish rendering.Summary
The Ruby file is responsible for managing the server-side rendering of React components within a Rails app, including support for streaming the rendered HTML. It parallels the TypeScript streaming logic by using Fibers to control the flow of streamed HTML chunks, ensuring that the client receives content as it becomes available. Both the Ruby and TypeScript implementations focus on improving performance by enabling partial responses to be sent during rendering, reducing the time to the first byte for the client.
Pull Request checklist
Remove this line after checking all the items here. If the item is not applicable to the PR, both check it out and wrap it by
~.Add the CHANGELOG entry at the top of the file.
Other Information
Remove this paragraph and mention any other important and relevant information such as benchmarks.
This change is
Summary by CodeRabbit
Summary by CodeRabbit
New Features
ReactOnRailsmodule with a new helper for processing streamed React components.Bug Fixes
Documentation
Dependency Updates
Chores
.gitignorefile for cleaner version control, targeting IDE-related files.