react_on_rails 16.2.0.beta.3 → 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 +4 -4
- data/CHANGELOG.md +42 -5
- data/CLAUDE.md +59 -0
- data/CONTRIBUTING.md +49 -1
- data/Gemfile.development_dependencies +1 -1
- data/Gemfile.lock +25 -10
- data/SWITCHING_CI_CONFIGS.md +55 -6
- data/Steepfile +51 -0
- data/bin/ci-rerun-failures +68 -22
- data/bin/ci-run-failed-specs +26 -2
- data/bin/ci-switch-config +262 -34
- data/bin/lefthook/check-trailing-newlines +2 -12
- data/bin/lefthook/eslint-lint +0 -10
- data/bin/lefthook/prettier-format +0 -10
- data/bin/lefthook/ruby-autofix +3 -6
- data/knip.ts +35 -9
- data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +32 -52
- data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +5 -1
- data/lib/react_on_rails/configuration.rb +56 -12
- data/lib/react_on_rails/controller.rb +3 -3
- data/lib/react_on_rails/dev/server_manager.rb +11 -4
- data/lib/react_on_rails/doctor.rb +249 -2
- data/lib/react_on_rails/helper.rb +12 -3
- data/lib/react_on_rails/pro_helper.rb +2 -44
- data/lib/react_on_rails/react_component/render_options.rb +7 -7
- data/lib/react_on_rails/utils.rb +40 -0
- data/lib/react_on_rails/version.rb +1 -1
- data/react_on_rails_pro/CHANGELOG.md +142 -29
- data/react_on_rails_pro/CONTRIBUTING.md +2 -13
- data/react_on_rails_pro/Gemfile.development_dependencies +1 -0
- data/react_on_rails_pro/Gemfile.lock +24 -3
- data/react_on_rails_pro/README.md +559 -38
- data/react_on_rails_pro/docs/code-splitting-loadable-components.md +1 -1
- data/react_on_rails_pro/docs/contributors-info/releasing.md +2 -2
- data/react_on_rails_pro/docs/installation.md +129 -109
- data/react_on_rails_pro/docs/node-renderer/basics.md +29 -22
- data/react_on_rails_pro/docs/node-renderer/error-reporting-and-tracing.md +8 -8
- data/react_on_rails_pro/docs/node-renderer/js-configuration.md +25 -23
- data/react_on_rails_pro/docs/node-renderer/troubleshooting.md +2 -0
- data/react_on_rails_pro/docs/updating.md +209 -15
- data/react_on_rails_pro/lib/react_on_rails_pro/concerns/stream.rb +58 -4
- data/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb +17 -3
- data/react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb +9 -9
- data/react_on_rails_pro/lib/react_on_rails_pro/request.rb +41 -25
- data/react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb +27 -7
- data/react_on_rails_pro/lib/react_on_rails_pro/utils.rb +3 -3
- data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
- data/react_on_rails_pro/package-scripts.yml +1 -1
- data/react_on_rails_pro/package.json +5 -8
- data/react_on_rails_pro/packages/node-renderer/src/integrations/api.ts +1 -1
- data/react_on_rails_pro/packages/node-renderer/src/master/restartWorkers.ts +39 -17
- data/react_on_rails_pro/packages/node-renderer/src/master.ts +15 -4
- data/react_on_rails_pro/packages/node-renderer/src/shared/configBuilder.ts +44 -5
- data/react_on_rails_pro/packages/node-renderer/src/shared/utils.ts +4 -2
- data/react_on_rails_pro/packages/node-renderer/src/worker/handleGracefulShutdown.ts +49 -0
- data/react_on_rails_pro/packages/node-renderer/src/worker/vm.ts +3 -3
- data/react_on_rails_pro/packages/node-renderer/src/worker.ts +5 -2
- data/react_on_rails_pro/packages/node-renderer/tests/helper.ts +8 -8
- data/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.js +1 -1
- data/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts +19 -19
- data/react_on_rails_pro/rakelib/public_key_management.rake +6 -5
- data/react_on_rails_pro/rakelib/rbs.rake +47 -0
- data/react_on_rails_pro/react_on_rails_pro.gemspec +1 -0
- data/react_on_rails_pro/sig/react_on_rails_pro/cache.rbs +13 -0
- data/react_on_rails_pro/sig/react_on_rails_pro/configuration.rbs +100 -0
- data/react_on_rails_pro/sig/react_on_rails_pro/error.rbs +4 -0
- data/react_on_rails_pro/sig/react_on_rails_pro/utils.rbs +7 -0
- data/react_on_rails_pro/sig/react_on_rails_pro.rbs +5 -0
- data/react_on_rails_pro/spec/dummy/Gemfile.lock +23 -3
- data/react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb +3 -3
- data/react_on_rails_pro/spec/dummy/bin/dev +4 -8
- data/react_on_rails_pro/spec/dummy/client/node-renderer.js +4 -4
- data/react_on_rails_pro/spec/dummy/config/environments/production.rb +1 -1
- data/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb +28 -12
- data/react_on_rails_pro/spec/dummy/config.ru +1 -1
- data/react_on_rails_pro/spec/dummy/package.json +2 -2
- data/react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb +40 -11
- data/react_on_rails_pro/spec/dummy/spec/rails_helper.rb +1 -1
- data/react_on_rails_pro/spec/dummy/spec/requests/renderer_console_logging_spec.rb +5 -5
- data/react_on_rails_pro/spec/dummy/spec/system/integration_spec.rb +15 -10
- data/react_on_rails_pro/spec/dummy/spec/system/renderer_integration_spec.rb +3 -3
- data/react_on_rails_pro/spec/dummy/yarn.lock +4 -4
- data/react_on_rails_pro/spec/execjs-compatible-dummy/config/environments/production.rb +1 -1
- data/react_on_rails_pro/spec/execjs-compatible-dummy/config/initializers/react_on_rails.rb +16 -43
- data/react_on_rails_pro/spec/react_on_rails_pro/assets_precompile_spec.rb +15 -18
- data/react_on_rails_pro/spec/react_on_rails_pro/cache_spec.rb +1 -1
- data/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb +5 -3
- data/react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb +27 -12
- data/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb +0 -27
- data/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb +1 -1
- data/react_on_rails_pro/spec/react_on_rails_pro/stream_decorator_spec.rb +89 -0
- data/react_on_rails_pro/spec/react_on_rails_pro/stream_spec.rb +144 -0
- data/react_on_rails_pro/spec/react_on_rails_pro/support/caching.rb +1 -1
- data/react_on_rails_pro/spec/react_on_rails_pro/support/mock_block_helper.rb +4 -2
- data/sig/react_on_rails/controller.rbs +1 -1
- data/sig/react_on_rails/error.rbs +4 -0
- data/sig/react_on_rails/helper.rbs +2 -2
- data/sig/react_on_rails/json_parse_error.rbs +10 -0
- data/sig/react_on_rails/prerender_error.rbs +21 -0
- data/sig/react_on_rails/smart_error.rbs +28 -0
- data/sig/react_on_rails.rbs +3 -24
- metadata +14 -4
- data/lib/react_on_rails/pro_utils.rb +0 -37
- data/react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/TestingStreamableComponent.jsx +0 -15
|
@@ -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(
|
|
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).
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 {
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
159
|
-
return path.join(
|
|
160
|
+
const { serverBundleCachePath } = getConfig();
|
|
161
|
+
return path.join(serverBundleCachePath, `${bundleTimestamp}`);
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
export function getRequestBundleFilePath(bundleTimestamp: string | number) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import cluster from 'cluster';
|
|
2
|
+
import { FastifyInstance } from './types';
|
|
3
|
+
import { SHUTDOWN_WORKER_MESSAGE } from '../shared/utils';
|
|
4
|
+
import log from '../shared/log';
|
|
5
|
+
|
|
6
|
+
const handleGracefulShutdown = (app: FastifyInstance) => {
|
|
7
|
+
const { worker } = cluster;
|
|
8
|
+
if (!worker) {
|
|
9
|
+
log.error('handleGracefulShutdown is called on master, expected to call it on worker only');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let activeRequestsCount = 0;
|
|
14
|
+
let isShuttingDown = false;
|
|
15
|
+
|
|
16
|
+
process.on('message', (msg) => {
|
|
17
|
+
if (msg === SHUTDOWN_WORKER_MESSAGE) {
|
|
18
|
+
log.debug('Worker #%d received graceful shutdown message', worker.id);
|
|
19
|
+
isShuttingDown = true;
|
|
20
|
+
if (activeRequestsCount === 0) {
|
|
21
|
+
log.debug('Worker #%d has no active requests, killing the worker', worker.id);
|
|
22
|
+
worker.destroy();
|
|
23
|
+
} else {
|
|
24
|
+
log.debug(
|
|
25
|
+
'Worker #%d has "%d" active requests, disconnecting the worker',
|
|
26
|
+
worker.id,
|
|
27
|
+
activeRequestsCount,
|
|
28
|
+
);
|
|
29
|
+
worker.disconnect();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.addHook('onRequest', (_req, _reply, done) => {
|
|
35
|
+
activeRequestsCount += 1;
|
|
36
|
+
done();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.addHook('onResponse', (_req, _reply, done) => {
|
|
40
|
+
activeRequestsCount -= 1;
|
|
41
|
+
if (isShuttingDown && activeRequestsCount === 0) {
|
|
42
|
+
log.debug('Worker #%d served all active requests and going to be killed', worker.id);
|
|
43
|
+
worker.destroy();
|
|
44
|
+
}
|
|
45
|
+
done();
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default handleGracefulShutdown;
|
|
@@ -112,7 +112,7 @@ export async function runInVM(
|
|
|
112
112
|
filePath: string,
|
|
113
113
|
vmCluster?: typeof cluster,
|
|
114
114
|
): Promise<RenderResult> {
|
|
115
|
-
const {
|
|
115
|
+
const { serverBundleCachePath } = getConfig();
|
|
116
116
|
|
|
117
117
|
try {
|
|
118
118
|
// Wait for VM creation if it's in progress
|
|
@@ -137,7 +137,7 @@ export async function runInVM(
|
|
|
137
137
|
const workerId = vmCluster?.worker?.id;
|
|
138
138
|
log.debug(`worker ${workerId ? `${workerId} ` : ''}received render request for bundle ${filePath} with code
|
|
139
139
|
${smartTrim(renderingRequest)}`);
|
|
140
|
-
const debugOutputPathCode = path.join(
|
|
140
|
+
const debugOutputPathCode = path.join(serverBundleCachePath, 'code.js');
|
|
141
141
|
log.debug(`Full code executed written to: ${debugOutputPathCode}`);
|
|
142
142
|
await writeFileAsync(debugOutputPathCode, renderingRequest);
|
|
143
143
|
}
|
|
@@ -165,7 +165,7 @@ ${smartTrim(renderingRequest)}`);
|
|
|
165
165
|
if (log.level === 'debug') {
|
|
166
166
|
log.debug(`result from JS:
|
|
167
167
|
${smartTrim(result)}`);
|
|
168
|
-
const debugOutputPathResult = path.join(
|
|
168
|
+
const debugOutputPathResult = path.join(serverBundleCachePath, 'result.json');
|
|
169
169
|
log.debug(`Wrote result to file: ${debugOutputPathResult}`);
|
|
170
170
|
await writeFileAsync(debugOutputPathResult, result);
|
|
171
171
|
}
|
|
@@ -17,6 +17,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from './worker/typ
|
|
|
17
17
|
import checkProtocolVersion from './worker/checkProtocolVersionHandler';
|
|
18
18
|
import authenticate from './worker/authHandler';
|
|
19
19
|
import { handleRenderRequest, type ProvidedNewBundle } from './worker/handleRenderRequest';
|
|
20
|
+
import handleGracefulShutdown from './worker/handleGracefulShutdown';
|
|
20
21
|
import {
|
|
21
22
|
errorResponseResult,
|
|
22
23
|
formatExceptionMessage,
|
|
@@ -117,7 +118,7 @@ export default function run(config: Partial<Config>) {
|
|
|
117
118
|
// getConfig():
|
|
118
119
|
buildConfig(config);
|
|
119
120
|
|
|
120
|
-
const {
|
|
121
|
+
const { serverBundleCachePath, logHttpLevel, port, fastifyServerOptions, workersCount } = getConfig();
|
|
121
122
|
|
|
122
123
|
const app = fastify({
|
|
123
124
|
http2: useHttp2 as true,
|
|
@@ -127,6 +128,8 @@ export default function run(config: Partial<Config>) {
|
|
|
127
128
|
...fastifyServerOptions,
|
|
128
129
|
});
|
|
129
130
|
|
|
131
|
+
handleGracefulShutdown(app);
|
|
132
|
+
|
|
130
133
|
// We shouldn't have unhandled errors here, but just in case
|
|
131
134
|
app.addHook('onError', (req, res, err, done) => {
|
|
132
135
|
// Not errorReporter.error so that integrations can decide how to log the errors.
|
|
@@ -148,7 +151,7 @@ export default function run(config: Partial<Config>) {
|
|
|
148
151
|
fileSize: Infinity,
|
|
149
152
|
},
|
|
150
153
|
onFile: async (part) => {
|
|
151
|
-
const destinationPath = path.join(
|
|
154
|
+
const destinationPath = path.join(serverBundleCachePath, 'uploads', part.filename);
|
|
152
155
|
// TODO: inline here
|
|
153
156
|
await saveMultipartFile(part, destinationPath);
|
|
154
157
|
// eslint-disable-next-line no-param-reassign
|
|
@@ -35,23 +35,23 @@ export function getOtherFixtureAsset() {
|
|
|
35
35
|
return path.resolve(__dirname, `./fixtures/${ASSET_UPLOAD_OTHER_FILE}`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function
|
|
38
|
+
export function serverBundleCachePath(testName: string) {
|
|
39
39
|
return path.resolve(__dirname, 'tmp', testName);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function setConfig(testName: string) {
|
|
43
43
|
buildConfig({
|
|
44
|
-
|
|
44
|
+
serverBundleCachePath: serverBundleCachePath(testName),
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export function vmBundlePath(testName: string) {
|
|
49
|
-
return path.resolve(
|
|
49
|
+
return path.resolve(serverBundleCachePath(testName), `${BUNDLE_TIMESTAMP}`, `${BUNDLE_TIMESTAMP}.js`);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function vmSecondaryBundlePath(testName: string) {
|
|
53
53
|
return path.resolve(
|
|
54
|
-
|
|
54
|
+
serverBundleCachePath(testName),
|
|
55
55
|
`${SECONDARY_BUNDLE_TIMESTAMP}`,
|
|
56
56
|
`${SECONDARY_BUNDLE_TIMESTAMP}.js`,
|
|
57
57
|
);
|
|
@@ -76,7 +76,7 @@ export function secondaryLockfilePath(testName: string) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function uploadedBundleDir(testName: string) {
|
|
79
|
-
return path.resolve(
|
|
79
|
+
return path.resolve(serverBundleCachePath(testName), 'uploads');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function uploadedBundlePath(testName: string) {
|
|
@@ -96,11 +96,11 @@ export function uploadedAssetOtherPath(testName: string) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export function assetPath(testName: string, bundleTimestamp: string) {
|
|
99
|
-
return path.resolve(
|
|
99
|
+
return path.resolve(serverBundleCachePath(testName), bundleTimestamp, ASSET_UPLOAD_FILE);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
export function assetPathOther(testName: string, bundleTimestamp: string) {
|
|
103
|
-
return path.resolve(
|
|
103
|
+
return path.resolve(serverBundleCachePath(testName), bundleTimestamp, ASSET_UPLOAD_OTHER_FILE);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export async function createUploadedBundle(testName: string) {
|
|
@@ -129,7 +129,7 @@ export async function createAsset(testName: string, bundleTimestamp: string) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export async function resetForTest(testName: string) {
|
|
132
|
-
await fsExtra.emptyDir(
|
|
132
|
+
await fsExtra.emptyDir(serverBundleCachePath(testName));
|
|
133
133
|
resetVM();
|
|
134
134
|
setConfig(testName);
|
|
135
135
|
}
|
|
@@ -8,7 +8,7 @@ if (fs.existsSync(BUNDLE_PATH)) {
|
|
|
8
8
|
|
|
9
9
|
const config = {
|
|
10
10
|
// This is the default but avoids searching for the Rails root
|
|
11
|
-
|
|
11
|
+
serverBundleCachePath: BUNDLE_PATH,
|
|
12
12
|
port: env.RENDERER_PORT || 3800, // Listen at RENDERER_PORT env value or default port 3800
|
|
13
13
|
logLevel: env.RENDERER_LOG_LEVEL || 'info',
|
|
14
14
|
|
|
@@ -15,14 +15,14 @@ import {
|
|
|
15
15
|
getFixtureAsset,
|
|
16
16
|
getOtherFixtureAsset,
|
|
17
17
|
createAsset,
|
|
18
|
-
|
|
18
|
+
serverBundleCachePath,
|
|
19
19
|
assetPath,
|
|
20
20
|
assetPathOther,
|
|
21
21
|
} from './helper';
|
|
22
22
|
|
|
23
23
|
const testName = 'worker';
|
|
24
24
|
const createVmBundleForTest = () => createVmBundle(testName);
|
|
25
|
-
const
|
|
25
|
+
const serverBundleCachePathForTest = () => serverBundleCachePath(testName);
|
|
26
26
|
|
|
27
27
|
const gemVersion = packageJson.version;
|
|
28
28
|
const { protocolVersion } = packageJson;
|
|
@@ -41,7 +41,7 @@ describe('worker', () => {
|
|
|
41
41
|
|
|
42
42
|
test('POST /bundles/:bundleTimestamp/render/:renderRequestDigest when bundle is provided and did not yet exist', async () => {
|
|
43
43
|
const app = worker({
|
|
44
|
-
|
|
44
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
const form = formAutoContent({
|
|
@@ -69,7 +69,7 @@ describe('worker', () => {
|
|
|
69
69
|
|
|
70
70
|
test('POST /bundles/:bundleTimestamp/render/:renderRequestDigest', async () => {
|
|
71
71
|
const app = worker({
|
|
72
|
-
|
|
72
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
const form = formAutoContent({
|
|
@@ -105,7 +105,7 @@ describe('worker', () => {
|
|
|
105
105
|
await createVmBundleForTest();
|
|
106
106
|
|
|
107
107
|
const app = worker({
|
|
108
|
-
|
|
108
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
109
109
|
password: 'password',
|
|
110
110
|
});
|
|
111
111
|
|
|
@@ -132,7 +132,7 @@ describe('worker', () => {
|
|
|
132
132
|
await createVmBundleForTest();
|
|
133
133
|
|
|
134
134
|
const app = worker({
|
|
135
|
-
|
|
135
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
136
136
|
password: 'password',
|
|
137
137
|
});
|
|
138
138
|
|
|
@@ -159,7 +159,7 @@ describe('worker', () => {
|
|
|
159
159
|
await createVmBundleForTest();
|
|
160
160
|
|
|
161
161
|
const app = worker({
|
|
162
|
-
|
|
162
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
163
163
|
password: 'my_password',
|
|
164
164
|
});
|
|
165
165
|
|
|
@@ -187,7 +187,7 @@ describe('worker', () => {
|
|
|
187
187
|
await createVmBundleForTest();
|
|
188
188
|
|
|
189
189
|
const app = worker({
|
|
190
|
-
|
|
190
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
const res = await app
|
|
@@ -211,7 +211,7 @@ describe('worker', () => {
|
|
|
211
211
|
await createAsset(testName, bundleHash);
|
|
212
212
|
|
|
213
213
|
const app = worker({
|
|
214
|
-
|
|
214
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
215
215
|
password: 'my_password',
|
|
216
216
|
});
|
|
217
217
|
|
|
@@ -237,7 +237,7 @@ describe('worker', () => {
|
|
|
237
237
|
await createAsset(testName, bundleHash);
|
|
238
238
|
|
|
239
239
|
const app = worker({
|
|
240
|
-
|
|
240
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
241
241
|
password: 'my_password',
|
|
242
242
|
});
|
|
243
243
|
|
|
@@ -261,7 +261,7 @@ describe('worker', () => {
|
|
|
261
261
|
test('post /asset-exists requires targetBundles (protocol version 2.0.0)', async () => {
|
|
262
262
|
await createAsset(testName, String(BUNDLE_TIMESTAMP));
|
|
263
263
|
const app = worker({
|
|
264
|
-
|
|
264
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
265
265
|
password: 'my_password',
|
|
266
266
|
});
|
|
267
267
|
|
|
@@ -283,7 +283,7 @@ describe('worker', () => {
|
|
|
283
283
|
const bundleHash = 'some-bundle-hash';
|
|
284
284
|
|
|
285
285
|
const app = worker({
|
|
286
|
-
|
|
286
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
287
287
|
password: 'my_password',
|
|
288
288
|
});
|
|
289
289
|
|
|
@@ -307,7 +307,7 @@ describe('worker', () => {
|
|
|
307
307
|
const bundleHashOther = 'some-other-bundle-hash';
|
|
308
308
|
|
|
309
309
|
const app = worker({
|
|
310
|
-
|
|
310
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
311
311
|
password: 'my_password',
|
|
312
312
|
});
|
|
313
313
|
|
|
@@ -334,7 +334,7 @@ describe('worker', () => {
|
|
|
334
334
|
await createVmBundleForTest();
|
|
335
335
|
|
|
336
336
|
const app = worker({
|
|
337
|
-
|
|
337
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
338
338
|
});
|
|
339
339
|
|
|
340
340
|
const res = await app
|
|
@@ -355,7 +355,7 @@ describe('worker', () => {
|
|
|
355
355
|
await createVmBundleForTest();
|
|
356
356
|
|
|
357
357
|
const app = worker({
|
|
358
|
-
|
|
358
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
359
359
|
});
|
|
360
360
|
|
|
361
361
|
const res = await app
|
|
@@ -379,7 +379,7 @@ describe('worker', () => {
|
|
|
379
379
|
await createVmBundleForTest();
|
|
380
380
|
|
|
381
381
|
const app = worker({
|
|
382
|
-
|
|
382
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
383
383
|
});
|
|
384
384
|
|
|
385
385
|
const res = await app
|
|
@@ -400,7 +400,7 @@ describe('worker', () => {
|
|
|
400
400
|
await createVmBundleForTest();
|
|
401
401
|
|
|
402
402
|
const app = worker({
|
|
403
|
-
|
|
403
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
404
404
|
});
|
|
405
405
|
|
|
406
406
|
// If package version is 4.0.0, this tests that 4.0.0.rc.1 gets normalized to 4.0.0-rc.1
|
|
@@ -426,7 +426,7 @@ describe('worker', () => {
|
|
|
426
426
|
await createVmBundleForTest();
|
|
427
427
|
|
|
428
428
|
const app = worker({
|
|
429
|
-
|
|
429
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
const gemVersionUpperCase = packageJson.version.toUpperCase();
|
|
@@ -449,7 +449,7 @@ describe('worker', () => {
|
|
|
449
449
|
await createVmBundleForTest();
|
|
450
450
|
|
|
451
451
|
const app = worker({
|
|
452
|
-
|
|
452
|
+
serverBundleCachePath: serverBundleCachePathForTest(),
|
|
453
453
|
});
|
|
454
454
|
|
|
455
455
|
const gemVersionWithWhitespace = ` ${packageJson.version} `;
|
|
@@ -13,9 +13,9 @@ require "uri"
|
|
|
13
13
|
# rake react_on_rails_pro:verify_public_key # Verify current configuration
|
|
14
14
|
# rake react_on_rails_pro:public_key_help # Show help
|
|
15
15
|
|
|
16
|
-
namespace :react_on_rails_pro do
|
|
16
|
+
namespace :react_on_rails_pro do # rubocop:disable Metrics/BlockLength
|
|
17
17
|
desc "Update the public key for React on Rails Pro license validation"
|
|
18
|
-
task :update_public_key, [:source] do |_task, args|
|
|
18
|
+
task :update_public_key, [:source] do |_task, args| # rubocop:disable Metrics/BlockLength
|
|
19
19
|
source = args[:source] || "production"
|
|
20
20
|
|
|
21
21
|
# Determine the API URL based on the source
|
|
@@ -68,7 +68,7 @@ namespace :react_on_rails_pro do
|
|
|
68
68
|
# ShakaCode's public key for React on Rails Pro license verification
|
|
69
69
|
# The private key corresponding to this public key is held by ShakaCode
|
|
70
70
|
# and is never committed to the repository
|
|
71
|
-
# Last updated: #{Time.now.utc.strftime(
|
|
71
|
+
# Last updated: #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
72
72
|
# Source: #{api_url}
|
|
73
73
|
#
|
|
74
74
|
# You can update this public key by running the rake task:
|
|
@@ -86,12 +86,13 @@ namespace :react_on_rails_pro do
|
|
|
86
86
|
puts "✅ Updated Ruby public key: #{ruby_file_path}"
|
|
87
87
|
|
|
88
88
|
# Update Node/TypeScript public key file
|
|
89
|
-
node_file_path = File.join(File.dirname(__FILE__), "..", "packages", "node-renderer", "src", "shared",
|
|
89
|
+
node_file_path = File.join(File.dirname(__FILE__), "..", "packages", "node-renderer", "src", "shared",
|
|
90
|
+
"licensePublicKey.ts")
|
|
90
91
|
node_content = <<~TYPESCRIPT
|
|
91
92
|
// ShakaCode's public key for React on Rails Pro license verification
|
|
92
93
|
// The private key corresponding to this public key is held by ShakaCode
|
|
93
94
|
// and is never committed to the repository
|
|
94
|
-
// Last updated: #{Time.now.utc.strftime(
|
|
95
|
+
// Last updated: #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
95
96
|
// Source: #{api_url}
|
|
96
97
|
//
|
|
97
98
|
// You can update this public key by running the rake task:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
# NOTE: Pro package does not include Steep tasks (:steep, :all) as it does not
|
|
7
|
+
# use Steep type checker. Only RBS validation is performed.
|
|
8
|
+
# rubocop:disable Metrics/BlockLength
|
|
9
|
+
namespace :rbs do
|
|
10
|
+
desc "Validate RBS type signatures"
|
|
11
|
+
task :validate do
|
|
12
|
+
require "rbs"
|
|
13
|
+
require "rbs/cli"
|
|
14
|
+
|
|
15
|
+
puts "Validating RBS type signatures..."
|
|
16
|
+
|
|
17
|
+
# Use Open3 for better error handling - captures stdout, stderr, and exit status separately
|
|
18
|
+
# This allows us to distinguish between actual validation errors and warnings
|
|
19
|
+
# Note: Must use bundle exec even though rake runs in bundle context because
|
|
20
|
+
# spawned shell commands via Open3.capture3() do NOT inherit bundle context
|
|
21
|
+
# Wrap in Timeout to prevent hung processes in CI environments (60 second timeout)
|
|
22
|
+
stdout, stderr, status = Timeout.timeout(60) do
|
|
23
|
+
Open3.capture3("bundle exec rbs -I sig validate")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if status.success?
|
|
27
|
+
puts "✓ RBS validation passed"
|
|
28
|
+
else
|
|
29
|
+
puts "✗ RBS validation failed"
|
|
30
|
+
puts stdout unless stdout.empty?
|
|
31
|
+
warn stderr unless stderr.empty?
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Check RBS type signatures (alias for validate)"
|
|
37
|
+
task check: :validate
|
|
38
|
+
|
|
39
|
+
desc "List all RBS files"
|
|
40
|
+
task :list do
|
|
41
|
+
sig_files = Dir.glob("sig/**/*.rbs")
|
|
42
|
+
puts "RBS type signature files:"
|
|
43
|
+
sig_files.each { |f| puts " #{f}" }
|
|
44
|
+
puts "\nTotal: #{sig_files.count} files"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
# rubocop:enable Metrics/BlockLength
|