react_on_rails 16.2.0.beta.3 → 16.2.0.beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +59 -0
  4. data/CONTRIBUTING.md +48 -0
  5. data/Gemfile.development_dependencies +1 -0
  6. data/Gemfile.lock +25 -2
  7. data/SWITCHING_CI_CONFIGS.md +55 -6
  8. data/Steepfile +51 -0
  9. data/bin/ci-rerun-failures +34 -11
  10. data/bin/ci-run-failed-specs +25 -1
  11. data/bin/ci-switch-config +254 -32
  12. data/bin/lefthook/check-trailing-newlines +2 -12
  13. data/bin/lefthook/eslint-lint +0 -10
  14. data/bin/lefthook/prettier-format +0 -10
  15. data/bin/lefthook/ruby-autofix +1 -5
  16. data/lib/react_on_rails/configuration.rb +56 -12
  17. data/lib/react_on_rails/controller.rb +3 -3
  18. data/lib/react_on_rails/doctor.rb +4 -2
  19. data/lib/react_on_rails/helper.rb +3 -3
  20. data/lib/react_on_rails/pro_helper.rb +2 -44
  21. data/lib/react_on_rails/react_component/render_options.rb +7 -7
  22. data/lib/react_on_rails/utils.rb +40 -0
  23. data/lib/react_on_rails/version.rb +1 -1
  24. data/react_on_rails_pro/CHANGELOG.md +135 -29
  25. data/react_on_rails_pro/Gemfile.development_dependencies +1 -0
  26. data/react_on_rails_pro/Gemfile.lock +6 -3
  27. data/react_on_rails_pro/README.md +559 -38
  28. data/react_on_rails_pro/docs/installation.md +40 -22
  29. data/react_on_rails_pro/docs/node-renderer/basics.md +26 -19
  30. data/react_on_rails_pro/docs/node-renderer/js-configuration.md +24 -22
  31. data/react_on_rails_pro/docs/node-renderer/troubleshooting.md +2 -0
  32. data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
  33. data/react_on_rails_pro/package.json +1 -1
  34. data/react_on_rails_pro/packages/node-renderer/src/master/restartWorkers.ts +39 -17
  35. data/react_on_rails_pro/packages/node-renderer/src/master.ts +15 -4
  36. data/react_on_rails_pro/packages/node-renderer/src/shared/configBuilder.ts +44 -5
  37. data/react_on_rails_pro/packages/node-renderer/src/shared/utils.ts +4 -2
  38. data/react_on_rails_pro/packages/node-renderer/src/worker/handleGracefulShutdown.ts +49 -0
  39. data/react_on_rails_pro/packages/node-renderer/src/worker/vm.ts +3 -3
  40. data/react_on_rails_pro/packages/node-renderer/src/worker.ts +5 -2
  41. data/react_on_rails_pro/packages/node-renderer/tests/helper.ts +8 -8
  42. data/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.js +1 -1
  43. data/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts +19 -19
  44. data/react_on_rails_pro/rakelib/rbs.rake +47 -0
  45. data/react_on_rails_pro/sig/react_on_rails_pro/cache.rbs +13 -0
  46. data/react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs +100 -0
  47. data/react_on_rails_pro/sig/react_on_rails_pro/error.rbs +4 -0
  48. data/react_on_rails_pro/sig/react_on_rails_pro/utils.rbs +7 -0
  49. data/react_on_rails_pro/sig/react_on_rails_pro.rbs +5 -0
  50. data/react_on_rails_pro/spec/dummy/Gemfile.lock +6 -3
  51. data/react_on_rails_pro/spec/dummy/client/node-renderer.js +1 -1
  52. data/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb +16 -17
  53. data/sig/react_on_rails/controller.rbs +1 -1
  54. data/sig/react_on_rails/error.rbs +4 -0
  55. data/sig/react_on_rails/helper.rbs +2 -2
  56. data/sig/react_on_rails/json_parse_error.rbs +10 -0
  57. data/sig/react_on_rails/prerender_error.rbs +21 -0
  58. data/sig/react_on_rails/smart_error.rbs +28 -0
  59. data/sig/react_on_rails.rbs +3 -24
  60. metadata +14 -3
  61. data/lib/react_on_rails/pro_utils.rb +0 -37
@@ -1,4 +1,5 @@
1
1
  # Installation
2
+
2
3
  Since the repository is private, you will get a **GitHub Personal Access Token** and an account that can access the packages. Substitute that value in the commands below. If you dont' have this, ask [justin@shakacode.com](mailto:justin@shakacode.com) to give you one.
3
4
 
4
5
  Check the [CHANGELOG](https://github.com/shakacode/react_on_rails_pro/blob/master/CHANGELOG.md) to see what version you want.
@@ -8,13 +9,16 @@ Check the [CHANGELOG](https://github.com/shakacode/react_on_rails_pro/blob/maste
8
9
  For the below docs, find the desired `<version>` in the CHANGELOG. Note, for pre-release versions, gems have all periods, and node packages uses a dash, like gem `3.0.0.rc.0` and node package `3.0.0-rc.0`.
9
10
 
10
11
  # Ruby
12
+
11
13
  ## Gem Installation
14
+
12
15
  1. Ensure your **Rails** app is using the **react_on_rails** gem, version greater than 11.0.7.
13
- 1. Add the `react_on_rails_pro` gem to your **Gemfile**. Substitute the appropriate version number.
14
-
16
+ 1. Add the `react_on_rails_pro` gem to your **Gemfile**. Substitute the appropriate version number.
17
+
15
18
  ## Gemfile Change
16
19
 
17
20
  Replace the following in the snippet for the Gemfile
21
+
18
22
  1. `<account>` for the api key
19
23
  2. `<api-key>`
20
24
  3. `<version>` desired
@@ -33,6 +37,7 @@ source "https://rubygems.pkg.github.com/shakacode-tools" do
33
37
  gem "react_on_rails_pro", "<version>"
34
38
  end
35
39
  ```
40
+
36
41
  Or use the `gem install` command:
37
42
 
38
43
  ```bash
@@ -46,23 +51,27 @@ bundle config set rubygems.pkg.github.com <username>:<token>
46
51
  ```
47
52
 
48
53
  ## Using a branch in your Gemfile
54
+
49
55
  Note, you should probably use an ENV value for the token so that you don't check this into your source code.
50
- ```ruby
51
- gem "react_on_rails_pro", version: "<version>", git: "https://[your-github-token]:x-oauth-basic@github.com/shakacode/react_on_rails_pro.git", tag: "<version>"
52
- ```
56
+
57
+ ```ruby
58
+ gem "react_on_rails_pro", version: "<version>", git: "https://[your-github-token]:x-oauth-basic@github.com/shakacode/react_on_rails_pro.git", tag: "<version>"
59
+ ```
53
60
 
54
61
  ## Rails Configuration
55
- You don't need to create an initializer if you are satisfied with the default as described in
62
+
63
+ You don't need to create an initializer if you are satisfied with the default as described in
56
64
  [Configuration](./configuration.md)
57
65
 
58
66
  # Node Package
67
+
59
68
  Note, you only need to install the Node Package if you are using the standalone node renderer, `NodeRenderer`.
60
69
 
61
70
  ## Installation
62
71
 
63
72
  1. Create a subdirectory of your rails project for the Node renderer. Let's use `react-on-rails-pro`.
64
-
65
73
  2. Create a file `react-on-rails-pro/.npmrc` with the following
74
+
66
75
  ```
67
76
  always-auth=true
68
77
  //npm.pkg.github.com/:_authToken=<token>
@@ -70,6 +79,7 @@ always-auth=true
70
79
  ```
71
80
 
72
81
  3. Create a `react-on-rails-pro/package.json`
82
+
73
83
  ```json
74
84
  {
75
85
  "private": true,
@@ -86,20 +96,18 @@ always-auth=true
86
96
 
87
97
  If you really want to use yarn, see [Yarn can't find private Github npm registry](https://stackoverflow.com/questions/58316109/yarn-cant-find-private-github-npm-registry)
88
98
 
89
- 5. You can start the renderer with either the executable `node-renderer` or, preferably, with
99
+ 5. You can start the renderer with either the executable `node-renderer` or, preferably, with
90
100
  a startup JS file, say called `react-on-rails-pro/react-on-rails-pro-node-renderer.js` with
91
- these contents. _Note the use of the namespaced **`@shakacode-tools/react-on-rails-pro-node-renderer`** for the package.
101
+ these contents. \_Note the use of the namespaced **`@shakacode-tools/react-on-rails-pro-node-renderer`** for the package.
92
102
 
93
103
  ```js
94
- const path = require('path')
95
- const {
96
- reactOnRailsProNodeRenderer,
97
- } = require('@shakacode-tools/react-on-rails-pro-node-renderer')
104
+ const path = require('path');
105
+ const { reactOnRailsProNodeRenderer } = require('@shakacode-tools/react-on-rails-pro-node-renderer');
98
106
 
99
- const env = process.env
107
+ const env = process.env;
100
108
 
101
109
  const config = {
102
- bundlePath: path.resolve(__dirname, '../.node-renderer-bundles'),
110
+ serverBundleCachePath: path.resolve(__dirname, '../.node-renderer-bundles'),
103
111
 
104
112
  // Listen at RENDERER_PORT env value or default port 3800
105
113
  logLevel: env.RENDERER_LOG_LEVEL || 'debug', // show all logs
@@ -129,21 +137,25 @@ const config = {
129
137
  // allWorkersRestartInterval: 15,
130
138
  // time in minutes between each worker restarting when restarting all workers
131
139
  // delayBetweenIndividualWorkerRestarts: 2,
132
- }
140
+ // Also, you can set he parameter gracefulWorkerRestartTimeout to force the worker to restart
141
+ // If it's the time for the worker to restart, the worker waits until it serves all active requests before restarting
142
+ // If a worker stuck because of a memory leakage or an infinite loop, you can set a timeout that master waits for it before killing the worker
143
+ };
133
144
 
134
145
  // Renderer detects a total number of CPUs on virtual hostings like Heroku
135
146
  // or CircleCI instead of CPUs number allocated for current container. This
136
147
  // results in spawning many workers while only 1-2 of them really needed.
137
148
  if (env.CI) {
138
- config.workersCount = 2
149
+ config.workersCount = 2;
139
150
  }
140
151
 
141
- reactOnRailsProNodeRenderer(config)
152
+ reactOnRailsProNodeRenderer(config);
142
153
  ```
143
154
 
144
155
  ## Instructions for using a branch
145
156
 
146
157
  Install the node-renderer executable, possibly globally. Substitute the branch name or tag for `master`
158
+
147
159
  ```
148
160
  yarn global add https://<your-github-token>:x-oauth-basic@github.com/shakacode/react_on_rails_pro.git\#master
149
161
  ```
@@ -156,28 +168,34 @@ Login into npm
156
168
 
157
169
  ```bash
158
170
  npm install @shakacode-tools/react-on-rails-pro-node-renderer@<version>
159
- ```
171
+ ```
160
172
 
161
173
  or edit package.json directly
174
+
162
175
  ```json
163
176
  "@shakacode-tools/react-on-rails-pro-node-renderer": "<version>"
164
- ```
177
+ ```
165
178
 
166
179
  ### Configuration
180
+
167
181
  See [NodeRenderer JavaScript Configuration](./node-renderer/js-configuration.md).
168
182
 
169
183
  #### Webpack Configuration
184
+
170
185
  Set your server bundle webpack configuration to use a target of `node` per the [Webpack docs](https://webpack.js.org/concepts/targets/#usage).
171
186
 
172
187
  ## Authentication when using Github packages
188
+
173
189
  [Auth for the npm package](https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-npm-for-use-with-github-packages#authenticating-to-github-packages)
174
190
 
175
191
  Create a new ~/.npmrc file if one doesn't exist.
192
+
176
193
  ```
177
194
  //npm.pkg.github.com/:_authToken=TOKEN
178
- ```
195
+ ```
179
196
 
180
197
  To configure bundler if you don't want the token in your Gemfile:
198
+
181
199
  ```
182
200
  bundle config https://rubygems.pkg.github.com/OWNER USERNAME:TOKEN
183
- ```
201
+ ```
@@ -1,23 +1,26 @@
1
1
  # Requirements
2
- * You must use React on Rails v11.0.7 or higher.
2
+
3
+ - You must use React on Rails v11.0.7 or higher.
3
4
 
4
5
  # Install the Gem and the Node Module
6
+
5
7
  See [Installation](../installation.md).
6
8
 
7
9
  # Setup Node Renderer Server
10
+
8
11
  **node-renderer** is a standalone Node application to serve React SSR requests from a **Rails** client. You don't need any **Ruby** code to setup and launch it. You can configure with the command line or with a launch file.
9
12
 
10
13
  ## Simple Command Line for node-renderer
11
14
 
12
15
  1. ENV values for the default config are (See [JS Configuration](./js-configuration.md) for more details):
13
- * `RENDERER_PORT`
14
- * `RENDERER_LOG_LEVEL`
15
- * `RENDERER_BUNDLE_PATH`
16
- * `RENDERER_WORKERS_COUNT`
17
- * `RENDERER_PASSWORD`
18
- * `RENDERER_ALL_WORKERS_RESTART_INTERVAL`
19
- * `RENDERER_DELAY_BETWEEN_INDIVIDUAL_WORKER_RESTARTS`
20
- * `RENDERER_SUPPORT_MODULES`
16
+ - `RENDERER_PORT`
17
+ - `RENDERER_LOG_LEVEL`
18
+ - `RENDERER_BUNDLE_PATH`
19
+ - `RENDERER_WORKERS_COUNT`
20
+ - `RENDERER_PASSWORD`
21
+ - `RENDERER_ALL_WORKERS_RESTART_INTERVAL`
22
+ - `RENDERER_DELAY_BETWEEN_INDIVIDUAL_WORKER_RESTARTS`
23
+ - `RENDERER_SUPPORT_MODULES`
21
24
  2. Configure ENV values and run the command. Note, you can set port with args `-p <PORT>`. For example, assuming node-renderer is in your path:
22
25
  ```
23
26
  RENDERER_BUNDLE_PATH=/app/.node-renderer-bundles node-renderer
@@ -25,6 +28,7 @@ See [Installation](../installation.md).
25
28
  3. You can use a command line argument of `-p SOME_PORT` to override any ENV value for the PORT.
26
29
 
27
30
  ## JavaScript Configuration File
31
+
28
32
  For the most control over the setup, create a JavaScript file to start the NodeRenderer.
29
33
 
30
34
  1. Create some project directory, let's say `renderer-app`:
@@ -38,22 +42,24 @@ For the most control over the setup, create a JavaScript file to start the NodeR
38
42
  yarn init
39
43
  yarn add https://[your-github-token]:x-oauth-basic@github.com/shakacode/react_on_rails_pro.git\#master
40
44
  ```
41
- 4. Configure a JavaScript file that will launch the rendering server per the docs in [Node Renderer JavaScript Configuration](./js-configuration.md). For example, create a file `node-renderer.js`. Here is a simple example that uses all the defaults except for bundlePath:
45
+ 4. Configure a JavaScript file that will launch the rendering server per the docs in [Node Renderer JavaScript Configuration](./js-configuration.md). For example, create a file `node-renderer.js`. Here is a simple example that uses all the defaults except for serverBundleCachePath:
42
46
 
43
47
  ```javascript
44
48
  import path from 'path';
45
49
  import reactOnRailsProNodeRenderer from '@shakacode-tools/react-on-rails-pro-node-renderer';
46
50
 
47
51
  const config = {
48
- bundlePath: path.resolve(__dirname, '../.node-renderer-bundles'),
52
+ serverBundleCachePath: path.resolve(__dirname, '../.node-renderer-bundles'),
49
53
  };
50
54
 
51
55
  reactOnRailsProNodeRenderer(config);
52
56
  ```
57
+
53
58
  5. Now you can launch your renderer server with `node node-renderer.js`. You will probably add a script to your `package.json`.
54
59
  6. You can use a command line argument of `-p SOME_PORT` to override any configured or ENV value for the port.
55
60
 
56
61
  # Setup Rails Application
62
+
57
63
  Create `config/initializers/react_on_rails_pro.rb` and configure the **renderer server**. See configuration values in [Configuration](../configuration.md). Pay attention to:
58
64
 
59
65
  1. Set `config.server_renderer = "NodeRenderer"`
@@ -61,25 +67,26 @@ Create `config/initializers/react_on_rails_pro.rb` and configure the **renderer
61
67
  3. Configure values beginning with `renderer_`
62
68
  4. Use ENV values for values like `renderer_url` so that your deployed server is properly configured. If the ENV value is unset, the default for the renderer_url is `localhost:3800`.
63
69
  5. Here's a tiny example using mostly defaults:
70
+
64
71
  ```ruby
65
72
  ReactOnRailsPro.configure do |config|
66
73
  config.server_renderer = "NodeRenderer"
67
-
68
- # when this ENV value is not defined, the local server at localhost:3800 is used
69
- config.renderer_url = ENV["REACT_RENDERER_URL"]
74
+
75
+ # when this ENV value is not defined, the local server at localhost:3800 is used
76
+ config.renderer_url = ENV["REACT_RENDERER_URL"]
70
77
  end
71
78
  ```
72
79
 
73
80
  ## Troublshooting
74
81
 
75
- * See [JS Memory Leaks](../js-memory-leaks.md).
76
-
82
+ - See [JS Memory Leaks](../js-memory-leaks.md).
83
+
77
84
  ## Upgrading
78
85
 
79
86
  The NodeRenderer has a protocol version on both the Rails and Node sides. If the Rails server sends a protocol version that does not match the Node side, an error is returned. Ideally, you want to keep both the Rails and Node sides at the same version.
80
87
 
81
88
  ## References
82
89
 
83
- * [Installation](../installation.md).
84
- * [Rails Options for node-renderer](../configuration.md)
85
- * [JS Options for node-renderer](./js-configuration.md)
90
+ - [Installation](../installation.md).
91
+ - [Rails Options for node-renderer](../configuration.md)
92
+ - [JS Options for node-renderer](./js-configuration.md)
@@ -8,39 +8,41 @@ The values in this file must be kept in sync with with the `config/initializers/
8
8
 
9
9
  Here are the options available for the JavaScript renderer configuration object, as well as the available default ENV values if using the command line program node-renderer.
10
10
 
11
- [//]: # (If you change text here, you may want to update comments in packages/node-renderer/src/shared/configBuilder.ts as well.)
11
+ [//]: # 'If you change text here, you may want to update comments in packages/node-renderer/src/shared/configBuilder.ts as well.'
12
12
 
13
13
  1. **port** (default: `process.env.RENDERER_PORT || 3800`) - The port the renderer should listen to.
14
- [On Heroku](https://devcenter.heroku.com/articles/dyno-startup-behavior#port-binding-of-web-dynos) or [ControlPlane](https://docs.controlplane.com/reference/workload/containers#port-variable) you may want to use `process.env.PORT`.
14
+ [On Heroku](https://devcenter.heroku.com/articles/dyno-startup-behavior#port-binding-of-web-dynos) or [ControlPlane](https://docs.controlplane.com/reference/workload/containers#port-variable) you may want to use `process.env.PORT`.
15
15
  1. **logLevel** (default: `process.env.RENDERER_LOG_LEVEL || 'info'`) - The renderer log level. Set it to `silent` to turn logging off.
16
- [Available levels](https://getpino.io/#/docs/api?id=levels): `{ fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 }`. `silent` can be used as well.
16
+ [Available levels](https://getpino.io/#/docs/api?id=levels): `{ fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 }`. `silent` can be used as well.
17
17
  1. **logHttpLevel** (default: `process.env.RENDERER_LOG_HTTP_LEVEL || 'error'`) - The HTTP server log level (same allowed values as `logLevel`).
18
18
  1. **fastifyServerOptions** (default: `{}`) - Additional options to pass to the Fastify server factory. See [Fastify documentation](https://fastify.dev/docs/latest/Reference/Server/#factory).
19
- 1. **bundlePath** (default: `process.env.RENDERER_BUNDLE_PATH || '/tmp/react-on-rails-pro-node-renderer-bundles'` ) - path to a temp directory where uploaded bundle files will be stored. For example you can set it to `path.resolve(__dirname, './.node-renderer-bundles')` if you configured renderer from the `/` directory of your app.
19
+ 1. **serverBundleCachePath** (default: `process.env.RENDERER_SERVER_BUNDLE_CACHE_PATH || process.env.RENDERER_BUNDLE_PATH || '/tmp/react-on-rails-pro-node-renderer-bundles'` ) - Path to a cache directory where uploaded server bundle files will be stored. This is distinct from Shakapacker's public asset directory. For example you can set it to `path.resolve(__dirname, './.node-renderer-bundles')` if you configured renderer from the `/` directory of your app.
20
20
  1. **workersCount** (default: `process.env.RENDERER_WORKERS_COUNT || defaultWorkersCount()` where default is your CPUs count - 1) - Number of workers that will be forked to serve rendering requests. If you set this manually make sure that value is a **Number** and is `>= 0`. Setting this to `0` will run the renderer in a single process mode without forking any workers, which is useful for debugging purposes. For production use, the value should be `>= 1`.
21
21
  1. **password** (default: `env.RENDERER_PASSWORD`) - The password expected to receive from the **Rails client** to authenticate rendering requests.
22
- If no password is set, no authentication will be required.
22
+ If no password is set, no authentication will be required.
23
23
  1. **allWorkersRestartInterval** (default: `env.RENDERER_ALL_WORKERS_RESTART_INTERVAL`) - Interval in minutes between scheduled restarts of all workers. By default restarts are not enabled. If restarts are enabled, `delayBetweenIndividualWorkerRestarts` should also be set.
24
24
  1. **delayBetweenIndividualWorkerRestarts** (default: `env.RENDERER_DELAY_BETWEEN_INDIVIDUAL_WORKER_RESTARTS`) - Interval in minutes between individual worker restarts (when cluster restart is triggered). By default restarts are not enabled. If restarts are enabled, `allWorkersRestartInterval` should also be set.
25
+ 1. **gracefulWorkerRestartTimeout**: (default: `env.GRACEFUL_WORKER_RESTART_TIMEOUT`) - Time in seconds that the master waits for a worker to gracefully restart (after serving all active requests) before killing it. Use this when you want to avoid situations where a worker gets stuck in an infinite loop and never restarts. This config is only usable if worker restart is enabled. The timeout starts when the worker should restart; if it elapses without a restart, the worker is killed.
25
26
  1. **maxDebugSnippetLength** (default: 1000) - If the rendering request is longer than this, it will be truncated in exception and logging messages.
26
- 1. **supportModules** - (default: `env.RENDERER_SUPPORT_MODULES || null`) - If set to true, `supportModules` enables the server-bundle code to call a default set of NodeJS global objects and functions that get added to the VM context:
27
- `{ Buffer, TextDecoder, TextEncoder, URLSearchParams, ReadableStream, process, setTimeout, setInterval, setImmediate, clearTimeout, clearInterval, clearImmediate, queueMicrotask }`.
28
- This option is required to equal `true` if you want to use loadable components.
29
- Setting this value to false causes the NodeRenderer to behave like ExecJS.
30
- See also `stubTimers`.
31
- 1. **additionalContext** - (default: `null`) - additionalContext enables you to specify additional NodeJS objects (usually from https://nodejs.org/api/globals.html) to add to the VM context in addition to our `supportModules` defaults.
32
- Object shorthand notation may be used, but is not required.
33
- Example: `{ URL, Crypto }`
34
- 1. **stubTimers** - (default: `env.RENDERER_STUB_TIMERS` if that environment variable is set, `true` otherwise) - With this option set, use of functions `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, and `queueMicrotask` will do nothing during server-rendering.
35
- This is useful when using dependencies like [react-virtuoso](https://github.com/petyosi/react-virtuoso) that use these functions during hydration.
36
- In RORP, hydration typically is synchronous and single-task (unless you use streaming) and thus callbacks passed to task-scheduling functions should never run during server-side rendering.
37
- Because these functions are valid client-side, they are ignored on server-side rendering without errors or warnings.
38
- See also `supportModules`.
27
+ 1. **supportModules** - (default: `env.RENDERER_SUPPORT_MODULES || null`) - If set to true, `supportModules` enables the server-bundle code to call a default set of NodeJS global objects and functions that get added to the VM context:
28
+ `{ Buffer, TextDecoder, TextEncoder, URLSearchParams, ReadableStream, process, setTimeout, setInterval, setImmediate, clearTimeout, clearInterval, clearImmediate, queueMicrotask }`.
29
+ This option is required to equal `true` if you want to use loadable components.
30
+ Setting this value to false causes the NodeRenderer to behave like ExecJS.
31
+ See also `stubTimers`.
32
+ 1. **additionalContext** - (default: `null`) - additionalContext enables you to specify additional NodeJS objects (usually from https://nodejs.org/api/globals.html) to add to the VM context in addition to our `supportModules` defaults.
33
+ Object shorthand notation may be used, but is not required.
34
+ Example: `{ URL, Crypto }`
35
+ 1. **stubTimers** - (default: `env.RENDERER_STUB_TIMERS` if that environment variable is set, `true` otherwise) - With this option set, use of functions `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, and `queueMicrotask` will do nothing during server-rendering.
36
+ This is useful when using dependencies like [react-virtuoso](https://github.com/petyosi/react-virtuoso) that use these functions during hydration.
37
+ In RORP, hydration typically is synchronous and single-task (unless you use streaming) and thus callbacks passed to task-scheduling functions should never run during server-side rendering.
38
+ Because these functions are valid client-side, they are ignored on server-side rendering without errors or warnings.
39
+ See also `supportModules`.
39
40
 
40
41
  Deprecated options:
41
42
 
42
- 1. **honeybadgerApiKey**, **sentryDsn**, **sentryTracing**, **sentryTracesSampleRate** - Deprecated and have no effect.
43
- If you have any of them set, see [Error Reporting and Tracing](./error-reporting-and-tracing.md) for the new way to set up error reporting and tracing.
43
+ 1. **bundlePath** - Renamed to `serverBundleCachePath`. The old name will continue to work but will log a deprecation warning.
44
+ 1. **honeybadgerApiKey**, **sentryDsn**, **sentryTracing**, **sentryTracesSampleRate** - Deprecated and have no effect.
45
+ If you have any of them set, see [Error Reporting and Tracing](./error-reporting-and-tracing.md) for the new way to set up error reporting and tracing.
44
46
  1. **includeTimerPolyfills** - Renamed to `stubTimers`.
45
47
 
46
48
  ## Example Launch Files
@@ -52,13 +54,14 @@ If you have any of them set, see [Error Reporting and Tracing](./error-reporting
52
54
  ### Simple example:
53
55
 
54
56
  Create a file './node-renderer.js'
57
+
55
58
  ```js
56
59
  import path from 'path';
57
60
  import { reactOnRailsProNodeRenderer } from '@shakacode-tools/react-on-rails-pro-node-renderer';
58
61
 
59
62
  const config = {
60
63
  // Save bundles to relative "./.node-renderer-bundles" dir of our app
61
- bundlePath: path.resolve(__dirname, './.node-renderer-bundles'),
64
+ serverBundleCachePath: path.resolve(__dirname, './.node-renderer-bundles'),
62
65
 
63
66
  // All other values are the defaults, as described above
64
67
  };
@@ -75,7 +78,6 @@ else if (process.env.CI) {
75
78
  }
76
79
 
77
80
  reactOnRailsProNodeRenderer(config);
78
-
79
81
  ```
80
82
 
81
83
  And add this line to your `scripts` section of `package.json`
@@ -1,3 +1,5 @@
1
1
  # Node Renderer Troubleshooting
2
2
 
3
3
  - If you enabled restarts (having `allWorkersRestartInterval` and `delayBetweenIndividualWorkerRestarts`), you should set it with a high number to avoid the app from crashing because all Node renderer workers are stopped/killed.
4
+
5
+ - If your app contains streamed pages that take too much time to be streamed to the client, ensure to not set the `gracefulWorkerRestartTimeout` parameter or set to a high number, so the worker is not killed while it's still serving an active request.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRailsPro
4
- VERSION = "16.2.0.beta.3"
4
+ VERSION = "16.2.0.beta.4"
5
5
  PROTOCOL_VERSION = "2.0.0"
6
6
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shakacode-tools/react-on-rails-pro-node-renderer",
3
- "version": "16.2.0-beta.3",
3
+ "version": "16.2.0-beta.4",
4
4
  "protocolVersion": "2.0.0",
5
5
  "description": "react-on-rails-pro JavaScript for react_on_rails_pro Ruby gem",
6
6
  "exports": {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import cluster from 'cluster';
7
7
  import log from '../shared/log';
8
+ import { SHUTDOWN_WORKER_MESSAGE } from '../shared/utils';
8
9
 
9
10
  const MILLISECONDS_IN_MINUTE = 60000;
10
11
 
@@ -14,26 +15,47 @@ declare module 'cluster' {
14
15
  }
15
16
  }
16
17
 
17
- export = function restartWorkers(delayBetweenIndividualWorkerRestarts: number) {
18
+ export = async function restartWorkers(
19
+ delayBetweenIndividualWorkerRestarts: number,
20
+ gracefulWorkerRestartTimeout: number | undefined,
21
+ ) {
18
22
  log.info('Started scheduled restart of workers');
19
23
 
20
- let delay = 0;
21
24
  if (!cluster.workers) {
22
25
  throw new Error('No workers to restart');
23
26
  }
24
- Object.values(cluster.workers).forEach((worker) => {
25
- const killWorker = () => {
26
- if (!worker) return;
27
- log.debug('Kill worker #%d', worker.id);
28
- // eslint-disable-next-line no-param-reassign -- necessary change
29
- worker.isScheduledRestart = true;
30
- worker.destroy();
31
- };
32
- setTimeout(killWorker, delay);
33
- delay += delayBetweenIndividualWorkerRestarts * MILLISECONDS_IN_MINUTE;
34
- });
35
-
36
- setTimeout(() => {
37
- log.info('Finished scheduled restart of workers');
38
- }, delay);
27
+ for (const worker of Object.values(cluster.workers).filter((w) => !!w)) {
28
+ log.debug('Kill worker #%d', worker.id);
29
+ worker.isScheduledRestart = true;
30
+
31
+ worker.send(SHUTDOWN_WORKER_MESSAGE);
32
+
33
+ // It's inteded to restart worker in sequence, it shouldn't happens in parallel
34
+ // eslint-disable-next-line no-await-in-loop
35
+ await new Promise<void>((resolve) => {
36
+ let timeout: NodeJS.Timeout;
37
+
38
+ const onExit = () => {
39
+ clearTimeout(timeout);
40
+ resolve();
41
+ };
42
+ worker.on('exit', onExit);
43
+
44
+ // Zero means no timeout
45
+ if (gracefulWorkerRestartTimeout) {
46
+ timeout = setTimeout(() => {
47
+ log.debug('Worker #%d timed out, forcing kill it', worker.id);
48
+ worker.destroy();
49
+ worker.off('exit', onExit);
50
+ resolve();
51
+ }, gracefulWorkerRestartTimeout);
52
+ }
53
+ });
54
+ // eslint-disable-next-line no-await-in-loop
55
+ await new Promise((resolve) => {
56
+ setTimeout(resolve, delayBetweenIndividualWorkerRestarts * MILLISECONDS_IN_MINUTE);
57
+ });
58
+ }
59
+
60
+ log.info('Finished scheduled restart of workers');
39
61
  };
@@ -19,7 +19,12 @@ export = function masterRun(runningConfig?: Partial<Config>) {
19
19
 
20
20
  // Store config in app state. From now it can be loaded by any module using getConfig():
21
21
  const config = buildConfig(runningConfig);
22
- const { workersCount, allWorkersRestartInterval, delayBetweenIndividualWorkerRestarts } = config;
22
+ const {
23
+ workersCount,
24
+ allWorkersRestartInterval,
25
+ delayBetweenIndividualWorkerRestarts,
26
+ gracefulWorkerRestartTimeout,
27
+ } = config;
23
28
 
24
29
  logSanitizedConfig();
25
30
 
@@ -48,9 +53,15 @@ export = function masterRun(runningConfig?: Partial<Config>) {
48
53
  allWorkersRestartInterval,
49
54
  delayBetweenIndividualWorkerRestarts,
50
55
  );
51
- setInterval(() => {
52
- restartWorkers(delayBetweenIndividualWorkerRestarts);
53
- }, allWorkersRestartInterval * MILLISECONDS_IN_MINUTE);
56
+
57
+ const allWorkersRestartIntervalMS = allWorkersRestartInterval * MILLISECONDS_IN_MINUTE;
58
+ const scheduleWorkersRestart = () => {
59
+ void restartWorkers(delayBetweenIndividualWorkerRestarts, gracefulWorkerRestartTimeout).finally(() => {
60
+ setTimeout(scheduleWorkersRestart, allWorkersRestartIntervalMS);
61
+ });
62
+ };
63
+
64
+ setTimeout(scheduleWorkersRestart, allWorkersRestartIntervalMS);
54
65
  } else if (allWorkersRestartInterval || delayBetweenIndividualWorkerRestarts) {
55
66
  log.error(
56
67
  "Misconfiguration, please provide both 'allWorkersRestartInterval' and " +
@@ -34,8 +34,11 @@ export interface Config {
34
34
  // Additional options to pass to the Fastify server factory.
35
35
  // See https://fastify.dev/docs/latest/Reference/Server/#factory.
36
36
  fastifyServerOptions: FastifyServerOptions<http2.Http2Server>;
37
- // Path to a temp directory where uploaded bundle files will be stored.
38
- bundlePath: string;
37
+ // Path to a cache directory where uploaded server bundle files will be stored.
38
+ // This is distinct from Shakapacker's public asset directory.
39
+ serverBundleCachePath: string;
40
+ // @deprecated Use serverBundleCachePath instead. This will be removed in a future version.
41
+ bundlePath?: string;
39
42
  // If set to true, `supportModules` enables the server-bundle code to call a default set of NodeJS
40
43
  // global objects and functions that get added to the VM context:
41
44
  // `{ Buffer, TextDecoder, TextEncoder, URLSearchParams, ReadableStream, process, setTimeout, setInterval, setImmediate, clearTimeout, clearInterval, clearImmediate, queueMicrotask }`.
@@ -58,6 +61,9 @@ export interface Config {
58
61
  allWorkersRestartInterval: number | undefined;
59
62
  // Time in minutes between each worker restarting when restarting all workers
60
63
  delayBetweenIndividualWorkerRestarts: number | undefined;
64
+ // Time in seconds to wait for worker to restart before killing it
65
+ // Set it to 0 or undefined to never kill the worker
66
+ gracefulWorkerRestartTimeout: number | undefined;
61
67
  // If the rendering request is longer than this, it will be truncated in exception and logging messages
62
68
  maxDebugSnippetLength: number;
63
69
  // @deprecated See https://www.shakacode.com/react-on-rails-pro/docs/node-renderer/error-reporting-and-tracing.
@@ -99,7 +105,7 @@ function defaultWorkersCount() {
99
105
  }
100
106
 
101
107
  // Find the .node-renderer-bundles folder if it exists, otherwise use /tmp
102
- function defaultBundlePath() {
108
+ function defaultServerBundleCachePath() {
103
109
  let currentDir = process.cwd();
104
110
  const maxDepth = 10;
105
111
  for (let i = 0; i < maxDepth; i += 1) {
@@ -145,7 +151,8 @@ const defaultConfig: Config = {
145
151
 
146
152
  fastifyServerOptions: {},
147
153
 
148
- bundlePath: env.RENDERER_BUNDLE_PATH || defaultBundlePath(),
154
+ serverBundleCachePath:
155
+ env.RENDERER_SERVER_BUNDLE_CACHE_PATH || env.RENDERER_BUNDLE_PATH || defaultServerBundleCachePath(),
149
156
 
150
157
  supportModules: truthy(env.RENDERER_SUPPORT_MODULES),
151
158
 
@@ -165,6 +172,10 @@ const defaultConfig: Config = {
165
172
  ? parseInt(env.RENDERER_DELAY_BETWEEN_INDIVIDUAL_WORKER_RESTARTS, 10)
166
173
  : undefined,
167
174
 
175
+ gracefulWorkerRestartTimeout: env.GRACEFUL_WORKER_RESTART_TIMEOUT
176
+ ? parseInt(env.GRACEFUL_WORKER_RESTART_TIMEOUT, 10)
177
+ : undefined,
178
+
168
179
  maxDebugSnippetLength: MAX_DEBUG_SNIPPET_LENGTH,
169
180
 
170
181
  // default to true if empty, otherwise it is set to false
@@ -185,7 +196,10 @@ function envValuesUsed() {
185
196
  RENDERER_PORT: !userConfig.port && env.RENDERER_PORT,
186
197
  RENDERER_LOG_LEVEL: !userConfig.logLevel && env.RENDERER_LOG_LEVEL,
187
198
  RENDERER_LOG_HTTP_LEVEL: !userConfig.logHttpLevel && env.RENDERER_LOG_HTTP_LEVEL,
188
- RENDERER_BUNDLE_PATH: !userConfig.bundlePath && env.RENDERER_BUNDLE_PATH,
199
+ RENDERER_SERVER_BUNDLE_CACHE_PATH:
200
+ !userConfig.serverBundleCachePath && env.RENDERER_SERVER_BUNDLE_CACHE_PATH,
201
+ RENDERER_BUNDLE_PATH:
202
+ !userConfig.serverBundleCachePath && !userConfig.bundlePath && env.RENDERER_BUNDLE_PATH,
189
203
  RENDERER_WORKERS_COUNT: !userConfig.workersCount && env.RENDERER_WORKERS_COUNT,
190
204
  RENDERER_PASSWORD: !userConfig.password && env.RENDERER_PASSWORD && '<MASKED>',
191
205
  RENDERER_SUPPORT_MODULES: !('supportModules' in userConfig) && env.RENDERER_SUPPORT_MODULES,
@@ -195,6 +209,8 @@ function envValuesUsed() {
195
209
  RENDERER_DELAY_BETWEEN_INDIVIDUAL_WORKER_RESTARTS:
196
210
  !userConfig.delayBetweenIndividualWorkerRestarts &&
197
211
  env.RENDERER_DELAY_BETWEEN_INDIVIDUAL_WORKER_RESTARTS,
212
+ GRACEFUL_WORKER_RESTART_TIMEOUT:
213
+ !userConfig.gracefulWorkerRestartTimeout && env.GRACEFUL_WORKER_RESTART_TIMEOUT,
198
214
  INCLUDE_TIMER_POLYFILLS: !('includeTimerPolyfills' in userConfig) && env.INCLUDE_TIMER_POLYFILLS,
199
215
  REPLAY_SERVER_ASYNC_OPERATION_LOGS:
200
216
  !userConfig.replayServerAsyncOperationLogs && env.REPLAY_SERVER_ASYNC_OPERATION_LOGS,
@@ -209,6 +225,7 @@ function sanitizedSettings(aConfig: Partial<Config> | undefined, defaultValue?:
209
225
  password: aConfig.password != null ? '<MASKED>' : defaultValue,
210
226
  allWorkersRestartInterval: aConfig.allWorkersRestartInterval || defaultValue,
211
227
  delayBetweenIndividualWorkerRestarts: aConfig.delayBetweenIndividualWorkerRestarts || defaultValue,
228
+ gracefulWorkerRestartTimeout: aConfig.gracefulWorkerRestartTimeout || defaultValue,
212
229
  }
213
230
  : {};
214
231
  }
@@ -231,6 +248,28 @@ export function buildConfig(providedUserConfig?: Partial<Config>): Config {
231
248
  userConfig = providedUserConfig || {};
232
249
  config = { ...defaultConfig, ...userConfig };
233
250
 
251
+ // Handle bundlePath deprecation
252
+ if ('bundlePath' in userConfig) {
253
+ log.warn(
254
+ 'bundlePath is deprecated and will be removed in a future version. ' +
255
+ 'Use serverBundleCachePath instead. This path stores uploaded server bundles for the node renderer, ' +
256
+ 'not client-side webpack assets from Shakapacker.',
257
+ );
258
+ // If serverBundleCachePath is not set, use bundlePath as fallback
259
+ if (
260
+ userConfig.bundlePath &&
261
+ (!config.serverBundleCachePath || config.serverBundleCachePath === defaultConfig.serverBundleCachePath)
262
+ ) {
263
+ config.serverBundleCachePath = userConfig.bundlePath;
264
+ }
265
+ }
266
+ if (env.RENDERER_BUNDLE_PATH && !env.RENDERER_SERVER_BUNDLE_CACHE_PATH) {
267
+ log.warn(
268
+ 'RENDERER_BUNDLE_PATH environment variable is deprecated and will be removed in a future version. ' +
269
+ 'Use RENDERER_SERVER_BUNDLE_CACHE_PATH instead.',
270
+ );
271
+ }
272
+
234
273
  config.supportModules = truthy(config.supportModules);
235
274
 
236
275
  if (config.maxVMPoolSize <= 0 || !Number.isInteger(config.maxVMPoolSize)) {
@@ -11,6 +11,8 @@ import type { RenderResult } from '../worker/vm';
11
11
 
12
12
  export const TRUNCATION_FILLER = '\n... TRUNCATED ...\n';
13
13
 
14
+ export const SHUTDOWN_WORKER_MESSAGE = 'NODE_RENDERER_SHUTDOWN_WORKER';
15
+
14
16
  export function workerIdLabel() {
15
17
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- worker is nullable in the primary process
16
18
  return cluster?.worker?.id || 'NO WORKER ID';
@@ -155,8 +157,8 @@ export const delay = (milliseconds: number) =>
155
157
  });
156
158
 
157
159
  export function getBundleDirectory(bundleTimestamp: string | number) {
158
- const { bundlePath } = getConfig();
159
- return path.join(bundlePath, `${bundleTimestamp}`);
160
+ const { serverBundleCachePath } = getConfig();
161
+ return path.join(serverBundleCachePath, `${bundleTimestamp}`);
160
162
  }
161
163
 
162
164
  export function getRequestBundleFilePath(bundleTimestamp: string | number) {