react-on-rails-pro-node-renderer 17.0.0-rc.6 → 17.0.0-rc.7
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.
- package/lib/integrations/opentelemetry.js +4 -4
- package/lib/master/restartWorkers.js +82 -28
- package/lib/master.js +20 -3
- package/lib/shared/checkRscPeerCompatibility.d.ts +1 -2
- package/lib/shared/checkRscPeerCompatibility.js +77 -22
- package/lib/shared/configBuilder.js +25 -17
- package/lib/shared/licenseValidator.js +1 -0
- package/lib/shared/rscPeerSupport.d.ts +2 -5
- package/lib/shared/rscPeerSupport.js +10 -10
- package/lib/shared/runRscPeerCompatibilityCheck.js +0 -5
- package/lib/shared/sharedConsoleHistory.js +3 -0
- package/lib/shared/utils.d.ts +2 -0
- package/lib/shared/utils.js +29 -3
- package/lib/worker/handleGracefulShutdown.js +33 -13
- package/lib/worker/handleIncrementalRenderRequest.d.ts +1 -0
- package/lib/worker/handleIncrementalRenderRequest.js +145 -106
- package/lib/worker/handleIncrementalRenderStream.d.ts +3 -0
- package/lib/worker/handleIncrementalRenderStream.js +79 -5
- package/lib/worker/handleRenderRequest.d.ts +5 -1
- package/lib/worker/handleRenderRequest.js +27 -9
- package/lib/worker/shutdownHooks.js +1 -1
- package/lib/worker/streamingUtils.js +9 -2
- package/lib/worker.d.ts +1 -0
- package/lib/worker.js +201 -13
- package/package.json +7 -9
|
@@ -22,10 +22,17 @@ function formatControlMessageChunk(metadata) {
|
|
|
22
22
|
const header = `${serializedMetadata}\t${'0'.padStart(8, '0')}\n`;
|
|
23
23
|
return Buffer.from(header);
|
|
24
24
|
}
|
|
25
|
+
// Control-message type strings: the TS end of the streaming wire protocol. The guarded
|
|
26
|
+
// region below wraps only this single-line declaration (mirroring the Ruby single-line
|
|
27
|
+
// CONTROL_MESSAGE_TYPES arrays) so unrelated literals in the functions cannot leak in.
|
|
28
|
+
// MIRROR VALUES OF: react_on_rails/lib/react_on_rails/length_prefixed_parser.rb
|
|
29
|
+
// MIRROR VALUES OF: react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb
|
|
30
|
+
const CONTROL_MESSAGE_TYPES = { propRequest: 'propRequest', renderComplete: 'renderComplete' };
|
|
31
|
+
// MIRROR VALUES END
|
|
25
32
|
function formatPropRequestChunk(propName) {
|
|
26
|
-
return formatControlMessageChunk({ messageType:
|
|
33
|
+
return formatControlMessageChunk({ messageType: CONTROL_MESSAGE_TYPES.propRequest, propName });
|
|
27
34
|
}
|
|
28
35
|
function formatRenderCompleteChunk() {
|
|
29
|
-
return formatControlMessageChunk({ messageType:
|
|
36
|
+
return formatControlMessageChunk({ messageType: CONTROL_MESSAGE_TYPES.renderComplete });
|
|
30
37
|
}
|
|
31
38
|
//# sourceMappingURL=streamingUtils.js.map
|
package/lib/worker.d.ts
CHANGED
package/lib/worker.js
CHANGED
|
@@ -85,13 +85,16 @@ const fastifyConfig_js_1 = require("./worker/fastifyConfig.js");
|
|
|
85
85
|
const vm_js_1 = require("./worker/vm.js");
|
|
86
86
|
var fastifyConfig_js_2 = require("./worker/fastifyConfig.js");
|
|
87
87
|
Object.defineProperty(exports, "configureFastify", { enumerable: true, get: function () { return fastifyConfig_js_2.configureFastify; } });
|
|
88
|
-
const INCREMENTAL_REQUEST_CLOSE_TIMEOUT_MS =
|
|
88
|
+
const INCREMENTAL_REQUEST_CLOSE_TIMEOUT_MS = 1_000;
|
|
89
89
|
// Standard response streams use this only to release retained renderer context;
|
|
90
90
|
// they are not aborted. Incremental responses may still close after request EOF.
|
|
91
91
|
// These release/finish windows are intentionally equal so both stream paths
|
|
92
92
|
// retain VM source-map registrations for the same idle period.
|
|
93
93
|
const STREAM_CONTEXT_RELEASE_TIMEOUT_MS = constants_js_1.STREAM_CHUNK_TIMEOUT_MS;
|
|
94
94
|
const INCREMENTAL_RESPONSE_FINISH_TIMEOUT_MS = STREAM_CONTEXT_RELEASE_TIMEOUT_MS;
|
|
95
|
+
// Pull-mode clients can legitimately pause longer than the normal chunk window;
|
|
96
|
+
// this coarse deadman only bounds abandoned request/response pairs.
|
|
97
|
+
const INCREMENTAL_PULL_MODE_IDLE_TIMEOUT_MS = constants_js_1.STREAM_CHUNK_TIMEOUT_MS * 15;
|
|
95
98
|
const HEALTH_ENDPOINT_ROUTES = ['/health', '/ready'];
|
|
96
99
|
// TODO: Reassess the duplicated-route message format when upgrading Fastify.
|
|
97
100
|
const FASTIFY_DUPLICATED_ROUTE_ERROR_CODE = 'FST_ERR_DUPLICATED_ROUTE';
|
|
@@ -262,7 +265,11 @@ const setResponseAndReleaseExecutionContext = async (result, res, executionConte
|
|
|
262
265
|
executionContext.release();
|
|
263
266
|
}
|
|
264
267
|
};
|
|
265
|
-
const isAsset = (value) => value
|
|
268
|
+
const isAsset = (value) => typeof value === 'object' &&
|
|
269
|
+
value !== null &&
|
|
270
|
+
value.type === 'asset' &&
|
|
271
|
+
typeof value.savedFilePath === 'string' &&
|
|
272
|
+
typeof value.filename === 'string';
|
|
266
273
|
function conflictingHealthEndpointPath(error) {
|
|
267
274
|
if (typeof error !== 'object' || error === null) {
|
|
268
275
|
return undefined;
|
|
@@ -348,6 +355,17 @@ const errorCode = (error) => {
|
|
|
348
355
|
return typeof code === 'string' ? code : undefined;
|
|
349
356
|
};
|
|
350
357
|
const isValidRenderingRequest = (value) => typeof value === 'string' && value.length > 0;
|
|
358
|
+
const isBooleanField = (value) => typeof value === 'boolean' || value === 'true' || value === 'false';
|
|
359
|
+
const parseOptionalBooleanField = (body, key) => {
|
|
360
|
+
const value = body[key];
|
|
361
|
+
if (value === undefined || value === null) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
if (!isBooleanField(value)) {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
return value === true || value === 'true';
|
|
368
|
+
};
|
|
351
369
|
const invalidRenderingRequestMessage = (body) => {
|
|
352
370
|
const { renderingRequest } = body;
|
|
353
371
|
let renderingRequestType = typeof renderingRequest;
|
|
@@ -379,6 +397,15 @@ const extractBodyArrayField = (body, key) => {
|
|
|
379
397
|
}
|
|
380
398
|
return undefined;
|
|
381
399
|
};
|
|
400
|
+
function discardMultipartFile(part) {
|
|
401
|
+
part.file.resume();
|
|
402
|
+
// eslint-disable-next-line no-param-reassign
|
|
403
|
+
part.value = {
|
|
404
|
+
filename: part.filename,
|
|
405
|
+
savedFilePath: '',
|
|
406
|
+
type: 'asset',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
382
409
|
function run(config) {
|
|
383
410
|
(0, runRscPeerCompatibilityCheck_js_1.runRscPeerCompatibilityCheck)({ proVersion: packageJson_js_1.default.version });
|
|
384
411
|
// Store config in app state. From now it can be loaded by any module using
|
|
@@ -416,6 +443,7 @@ function run(config) {
|
|
|
416
443
|
// from overwriting each other's files (GitHub issue #2449).
|
|
417
444
|
// The directory path is lazily assigned in onFile (only for requests with file uploads).
|
|
418
445
|
app.decorateRequest('uploadDir', '');
|
|
446
|
+
app.decorateRequest('uploadAssetValidationError', null);
|
|
419
447
|
// Clean up the per-request upload directory after the response is sent.
|
|
420
448
|
// Safe from a rate-limiting perspective (CodeQL js/missing-rate-limiting):
|
|
421
449
|
// this is an internal service not exposed to the internet, the path is
|
|
@@ -433,6 +461,7 @@ function run(config) {
|
|
|
433
461
|
// Supports multipart/form-data
|
|
434
462
|
void app.register(multipart_1.default, {
|
|
435
463
|
attachFieldsToBody: 'keyValues',
|
|
464
|
+
preservePath: true,
|
|
436
465
|
limits: {
|
|
437
466
|
fieldSize: constants_js_1.FIELD_SIZE_LIMIT,
|
|
438
467
|
// For bundles and assets
|
|
@@ -446,16 +475,23 @@ function run(config) {
|
|
|
446
475
|
if (typeof this?.uploadDir !== 'string') {
|
|
447
476
|
throw new Error('onFile: expected `this` to be bound to the Fastify request');
|
|
448
477
|
}
|
|
449
|
-
|
|
478
|
+
if (this.uploadAssetValidationError) {
|
|
479
|
+
discardMultipartFile(part);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
let safeFilename;
|
|
483
|
+
try {
|
|
484
|
+
safeFilename = (0, utils_js_1.validateAssetFilename)(part.filename);
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
this.uploadAssetValidationError = err.message;
|
|
488
|
+
discardMultipartFile(part);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
// Lazily assign a per-request upload directory on first valid file upload.
|
|
450
492
|
if (this.uploadDir === '') {
|
|
451
493
|
this.uploadDir = path_1.default.join(serverBundleCachePath, 'uploads', (0, crypto_1.randomUUID)());
|
|
452
494
|
}
|
|
453
|
-
// Use path.basename to strip any directory components from the filename,
|
|
454
|
-
// preventing path traversal attacks (e.g. filename "../../etc/shadow").
|
|
455
|
-
const safeFilename = path_1.default.basename(part.filename);
|
|
456
|
-
if (!safeFilename) {
|
|
457
|
-
throw new Error(`onFile: received file with empty or invalid filename: ${JSON.stringify(part.filename)}`);
|
|
458
|
-
}
|
|
459
495
|
const destinationPath = path_1.default.join(this.uploadDir, safeFilename);
|
|
460
496
|
await (0, utils_js_1.saveMultipartFile)(part, destinationPath);
|
|
461
497
|
// eslint-disable-next-line no-param-reassign
|
|
@@ -500,6 +536,15 @@ function run(config) {
|
|
|
500
536
|
await setResponse((0, utils_js_1.badRequestResponseResult)(invalidRenderingRequestMessage(body)), res);
|
|
501
537
|
return;
|
|
502
538
|
}
|
|
539
|
+
const rscStreamObservability = parseOptionalBooleanField(body, 'rscStreamObservability');
|
|
540
|
+
if (body.rscStreamObservability != null && rscStreamObservability === undefined) {
|
|
541
|
+
await setResponse((0, utils_js_1.badRequestResponseResult)('Invalid "rscStreamObservability" field in render request.'), res);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (req.uploadAssetValidationError) {
|
|
545
|
+
await setResponse((0, utils_js_1.badRequestResponseResult)(req.uploadAssetValidationError), res);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
503
548
|
const { bundleTimestamp } = req.params;
|
|
504
549
|
const { providedNewBundles, assetsToCopy } = extractBundlesAndAssets(body, bundleTimestamp);
|
|
505
550
|
try {
|
|
@@ -512,6 +557,7 @@ function run(config) {
|
|
|
512
557
|
dependencyBundleTimestamps,
|
|
513
558
|
providedNewBundles,
|
|
514
559
|
assetsToCopy,
|
|
560
|
+
rscStreamObservability,
|
|
515
561
|
tracingContext: context,
|
|
516
562
|
});
|
|
517
563
|
await setResponseAndReleaseExecutionContext(result.response, res, result.executionContext);
|
|
@@ -538,7 +584,11 @@ function run(config) {
|
|
|
538
584
|
let incrementalRequestClosed = false;
|
|
539
585
|
let incrementalResponseFinished = false;
|
|
540
586
|
let incrementalExecutionContextReleased = false;
|
|
587
|
+
let pullModeRequestCanIdle = false;
|
|
588
|
+
let incrementalRequestClosePromise;
|
|
541
589
|
let incrementalResponseFinishTimeoutId;
|
|
590
|
+
let incrementalPullModeIdleTimeoutId;
|
|
591
|
+
let stopIncrementalRequestReader;
|
|
542
592
|
let incrementalTracingContext;
|
|
543
593
|
const clearIncrementalResponseFinishTimeout = () => {
|
|
544
594
|
if (!incrementalResponseFinishTimeoutId) {
|
|
@@ -547,6 +597,26 @@ function run(config) {
|
|
|
547
597
|
clearTimeout(incrementalResponseFinishTimeoutId);
|
|
548
598
|
incrementalResponseFinishTimeoutId = undefined;
|
|
549
599
|
};
|
|
600
|
+
const clearIncrementalPullModeIdleTimeout = () => {
|
|
601
|
+
if (!incrementalPullModeIdleTimeoutId) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
clearTimeout(incrementalPullModeIdleTimeoutId);
|
|
605
|
+
incrementalPullModeIdleTimeoutId = undefined;
|
|
606
|
+
};
|
|
607
|
+
const shouldStopReadingIncrementalRequest = () => pullModeRequestCanIdle && responseStarted && incrementalResponseFinished;
|
|
608
|
+
const wakeIncrementalRequestReader = () => {
|
|
609
|
+
stopIncrementalRequestReader?.();
|
|
610
|
+
stopIncrementalRequestReader = undefined;
|
|
611
|
+
};
|
|
612
|
+
const waitForIncrementalRequestReaderStop = () => {
|
|
613
|
+
if (shouldStopReadingIncrementalRequest()) {
|
|
614
|
+
return Promise.resolve();
|
|
615
|
+
}
|
|
616
|
+
return new Promise((resolve) => {
|
|
617
|
+
stopIncrementalRequestReader = resolve;
|
|
618
|
+
});
|
|
619
|
+
};
|
|
550
620
|
const releaseIncrementalExecutionContextWhenDone = () => {
|
|
551
621
|
if (incrementalExecutionContextReleased ||
|
|
552
622
|
!incrementalRequestClosed ||
|
|
@@ -560,7 +630,9 @@ function run(config) {
|
|
|
560
630
|
};
|
|
561
631
|
const markIncrementalResponseFinished = () => {
|
|
562
632
|
clearIncrementalResponseFinishTimeout();
|
|
633
|
+
clearIncrementalPullModeIdleTimeout();
|
|
563
634
|
incrementalResponseFinished = true;
|
|
635
|
+
wakeIncrementalRequestReader();
|
|
564
636
|
releaseIncrementalExecutionContextWhenDone();
|
|
565
637
|
};
|
|
566
638
|
const scheduleIncrementalResponseFinishTimeout = () => {
|
|
@@ -605,8 +677,13 @@ function run(config) {
|
|
|
605
677
|
if (!incrementalSink || incrementalRequestClosed) {
|
|
606
678
|
return;
|
|
607
679
|
}
|
|
680
|
+
clearIncrementalPullModeIdleTimeout();
|
|
681
|
+
if (incrementalRequestClosePromise) {
|
|
682
|
+
await incrementalRequestClosePromise;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
608
685
|
const sink = incrementalSink;
|
|
609
|
-
|
|
686
|
+
incrementalRequestClosePromise = (async () => {
|
|
610
687
|
await waitForIncrementalRequestClose(Promise.resolve()
|
|
611
688
|
.then(() => sink.handleRequestClosed())
|
|
612
689
|
.catch((closeError) => {
|
|
@@ -615,13 +692,104 @@ function run(config) {
|
|
|
615
692
|
error: closeError,
|
|
616
693
|
});
|
|
617
694
|
}), timeoutMs);
|
|
695
|
+
})();
|
|
696
|
+
try {
|
|
697
|
+
await incrementalRequestClosePromise;
|
|
618
698
|
}
|
|
619
699
|
finally {
|
|
700
|
+
incrementalRequestClosePromise = undefined;
|
|
620
701
|
incrementalRequestClosed = true;
|
|
702
|
+
clearIncrementalPullModeIdleTimeout();
|
|
621
703
|
scheduleIncrementalResponseFinishTimeout();
|
|
622
704
|
releaseIncrementalExecutionContextWhenDone();
|
|
623
705
|
}
|
|
624
706
|
};
|
|
707
|
+
const shouldWatchPullModeIdleProgress = () => pullModeRequestCanIdle &&
|
|
708
|
+
responseStarted &&
|
|
709
|
+
!incrementalRequestClosed &&
|
|
710
|
+
!incrementalResponseFinished &&
|
|
711
|
+
!incrementalExecutionContextReleased &&
|
|
712
|
+
!!incrementalSink &&
|
|
713
|
+
!res.raw.destroyed &&
|
|
714
|
+
!res.raw.writableEnded;
|
|
715
|
+
const scheduleIncrementalPullModeIdleTimeout = () => {
|
|
716
|
+
if (incrementalPullModeIdleTimeoutId || !shouldWatchPullModeIdleProgress()) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
incrementalPullModeIdleTimeoutId = setTimeout(() => {
|
|
720
|
+
incrementalPullModeIdleTimeoutId = undefined;
|
|
721
|
+
if (!shouldWatchPullModeIdleProgress()) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
log_js_1.default.warn({
|
|
725
|
+
msg: 'Timed out waiting for pull-mode incremental render progress after response started',
|
|
726
|
+
timeoutMs: INCREMENTAL_PULL_MODE_IDLE_TIMEOUT_MS,
|
|
727
|
+
});
|
|
728
|
+
if (!res.raw.destroyed) {
|
|
729
|
+
res.raw.destroy();
|
|
730
|
+
}
|
|
731
|
+
void closeIncrementalRequest({
|
|
732
|
+
timeoutMs: INCREMENTAL_REQUEST_CLOSE_TIMEOUT_MS,
|
|
733
|
+
});
|
|
734
|
+
markIncrementalResponseFinished();
|
|
735
|
+
}, INCREMENTAL_PULL_MODE_IDLE_TIMEOUT_MS);
|
|
736
|
+
};
|
|
737
|
+
const refreshIncrementalPullModeIdleTimeout = () => {
|
|
738
|
+
if (!shouldWatchPullModeIdleProgress()) {
|
|
739
|
+
clearIncrementalPullModeIdleTimeout();
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
clearIncrementalPullModeIdleTimeout();
|
|
743
|
+
scheduleIncrementalPullModeIdleTimeout();
|
|
744
|
+
};
|
|
745
|
+
const refreshIncrementalResponseFinishTimeout = () => {
|
|
746
|
+
if (!incrementalRequestClosed ||
|
|
747
|
+
incrementalResponseFinished ||
|
|
748
|
+
incrementalExecutionContextReleased ||
|
|
749
|
+
!incrementalSink) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
clearIncrementalResponseFinishTimeout();
|
|
753
|
+
scheduleIncrementalResponseFinishTimeout();
|
|
754
|
+
};
|
|
755
|
+
const trackIncrementalResponseProgress = (stream) => {
|
|
756
|
+
const progressStream = new stream_1.Transform({
|
|
757
|
+
transform(chunk, encoding, callback) {
|
|
758
|
+
refreshIncrementalPullModeIdleTimeout();
|
|
759
|
+
refreshIncrementalResponseFinishTimeout();
|
|
760
|
+
callback(null, chunk);
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
const forwardSourceError = (error) => progressStream.destroy(error);
|
|
764
|
+
const endProgressStream = () => {
|
|
765
|
+
if (!progressStream.writableEnded && !progressStream.destroyed) {
|
|
766
|
+
progressStream.end();
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
stream.once('close', endProgressStream);
|
|
770
|
+
stream.once('error', forwardSourceError);
|
|
771
|
+
progressStream.once('close', () => {
|
|
772
|
+
stream.off('close', endProgressStream);
|
|
773
|
+
stream.off('error', forwardSourceError);
|
|
774
|
+
if (!progressStream.writableEnded && !stream.destroyed) {
|
|
775
|
+
stream.destroy();
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
stream.pipe(progressStream);
|
|
779
|
+
return progressStream;
|
|
780
|
+
};
|
|
781
|
+
const getIncrementalRequestChunkTimeoutMs = () => {
|
|
782
|
+
if (pullModeRequestCanIdle &&
|
|
783
|
+
responseStarted &&
|
|
784
|
+
!incrementalResponseFinished &&
|
|
785
|
+
!res.raw.destroyed &&
|
|
786
|
+
!res.raw.writableEnded) {
|
|
787
|
+
scheduleIncrementalPullModeIdleTimeout();
|
|
788
|
+
return Number.POSITIVE_INFINITY;
|
|
789
|
+
}
|
|
790
|
+
clearIncrementalPullModeIdleTimeout();
|
|
791
|
+
return constants_js_1.STREAM_CHUNK_TIMEOUT_MS;
|
|
792
|
+
};
|
|
625
793
|
try {
|
|
626
794
|
await (0, tracing_js_1.trace)(async (context) => {
|
|
627
795
|
incrementalTracingContext = context;
|
|
@@ -649,6 +817,7 @@ function run(config) {
|
|
|
649
817
|
try {
|
|
650
818
|
const { response, sink } = await (0, handleIncrementalRenderRequest_js_1.handleIncrementalRenderRequest)(initial);
|
|
651
819
|
incrementalSink = sink;
|
|
820
|
+
pullModeRequestCanIdle = !!incrementalSink && tempReqBody.pullEnabled === true;
|
|
652
821
|
return {
|
|
653
822
|
response,
|
|
654
823
|
shouldContinue: !!incrementalSink,
|
|
@@ -667,6 +836,7 @@ function run(config) {
|
|
|
667
836
|
log_js_1.default.error({ msg: 'Unexpected update chunk received after rendering was aborted', obj });
|
|
668
837
|
return;
|
|
669
838
|
}
|
|
839
|
+
refreshIncrementalPullModeIdleTimeout();
|
|
670
840
|
try {
|
|
671
841
|
await incrementalSink.add(obj);
|
|
672
842
|
}
|
|
@@ -678,9 +848,10 @@ function run(config) {
|
|
|
678
848
|
onResponseStart: async (response) => {
|
|
679
849
|
responseStarted = true;
|
|
680
850
|
if (response.stream) {
|
|
681
|
-
const
|
|
851
|
+
const responseStream = trackIncrementalResponseProgress(response.stream);
|
|
852
|
+
const markFinished = runWhenStreamFinishes(responseStream, res, markIncrementalResponseFinished);
|
|
682
853
|
try {
|
|
683
|
-
await setResponse(response, res);
|
|
854
|
+
await setResponse({ ...response, stream: responseStream }, res);
|
|
684
855
|
}
|
|
685
856
|
catch (error) {
|
|
686
857
|
markFinished();
|
|
@@ -698,6 +869,9 @@ function run(config) {
|
|
|
698
869
|
onRequestEnded: async () => {
|
|
699
870
|
await closeIncrementalRequest();
|
|
700
871
|
},
|
|
872
|
+
getChunkTimeoutMs: getIncrementalRequestChunkTimeoutMs,
|
|
873
|
+
shouldStopReading: shouldStopReadingIncrementalRequest,
|
|
874
|
+
waitForStopReading: waitForIncrementalRequestReaderStop,
|
|
701
875
|
});
|
|
702
876
|
}, (0, tracing_js_1.startSsrRequestOptions)({ renderingRequest: 'ReactOnRails.incrementalRender' }));
|
|
703
877
|
}
|
|
@@ -754,6 +928,10 @@ function run(config) {
|
|
|
754
928
|
await setResponse(precheckResult, res);
|
|
755
929
|
return;
|
|
756
930
|
}
|
|
931
|
+
if (req.uploadAssetValidationError) {
|
|
932
|
+
await setResponse((0, utils_js_1.badRequestResponseResult)(req.uploadAssetValidationError), res);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
757
935
|
const { providedNewBundles, assetsToCopy } = extractBundlesAndAssets(req.body);
|
|
758
936
|
if (providedNewBundles.length === 0) {
|
|
759
937
|
const errorMsg = 'No bundle_<hash> fields provided. ' +
|
|
@@ -815,6 +993,16 @@ function run(config) {
|
|
|
815
993
|
await setResponse((0, utils_js_1.errorResponseResult)(message), res);
|
|
816
994
|
return;
|
|
817
995
|
}
|
|
996
|
+
let assetFilename;
|
|
997
|
+
try {
|
|
998
|
+
assetFilename = (0, utils_js_1.validateAssetFilename)(filename);
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
const { message } = err;
|
|
1002
|
+
log_js_1.default.info(message);
|
|
1003
|
+
await setResponse((0, utils_js_1.badRequestResponseResult)(message), res);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
818
1006
|
// Handle targetBundles as either a string or an array
|
|
819
1007
|
const targetBundles = extractBodyArrayField(req.body, 'targetBundles');
|
|
820
1008
|
if (!targetBundles || targetBundles.length === 0) {
|
|
@@ -825,7 +1013,7 @@ function run(config) {
|
|
|
825
1013
|
}
|
|
826
1014
|
// Check if the asset exists in each of the target bundles
|
|
827
1015
|
const results = await Promise.all(targetBundles.map(async (bundleHash) => {
|
|
828
|
-
const assetPath = (0, utils_js_1.getAssetPath)(bundleHash,
|
|
1016
|
+
const assetPath = (0, utils_js_1.getAssetPath)(bundleHash, assetFilename);
|
|
829
1017
|
const exists = await (0, fileExistsAsync_js_1.default)(assetPath);
|
|
830
1018
|
if (exists) {
|
|
831
1019
|
log_js_1.default.info(`/asset-exists Uploaded asset DOES exist in bundle ${bundleHash}: ${assetPath}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-on-rails-pro-node-renderer",
|
|
3
|
-
"version": "17.0.0-rc.
|
|
3
|
+
"version": "17.0.0-rc.7",
|
|
4
4
|
"protocolVersion": "2.0.0",
|
|
5
5
|
"description": "React on Rails Pro Node Renderer for server-side rendering",
|
|
6
6
|
"engines": {
|
|
@@ -44,8 +44,7 @@
|
|
|
44
44
|
"pino": "^9.14.0 || ^10.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@babel/core": "^7.
|
|
48
|
-
"@babel/eslint-parser": "^7.27.0",
|
|
47
|
+
"@babel/core": "^7.29.6",
|
|
49
48
|
"@babel/preset-env": "^7.20.2",
|
|
50
49
|
"@babel/preset-react": "^7.26.3",
|
|
51
50
|
"@babel/preset-typescript": "^7.27.1",
|
|
@@ -66,18 +65,18 @@
|
|
|
66
65
|
"@types/lockfile": "^1.0.4",
|
|
67
66
|
"@types/touch": "^3.1.5",
|
|
68
67
|
"babel-jest": "^29.7.0",
|
|
69
|
-
"form-
|
|
70
|
-
"form-data": "^4.0.1",
|
|
68
|
+
"form-data": "^4.0.6",
|
|
71
69
|
"jest-junit": "^16.0.0",
|
|
72
|
-
"jsdom": "^16.5.0",
|
|
73
70
|
"node-html-parser": "^7.0.1",
|
|
74
|
-
"nps": "^5.9.12",
|
|
75
71
|
"pino-pretty": "^13.0.0",
|
|
72
|
+
"react": "~19.2.7",
|
|
73
|
+
"react-dom": "~19.2.7",
|
|
74
|
+
"react-on-rails-rsc": "19.2.1-rc.0",
|
|
76
75
|
"redis": "^5.0.1",
|
|
77
76
|
"sentry-testkit": "^5.0.6",
|
|
78
77
|
"touch": "^3.1.0",
|
|
79
78
|
"typescript": "^5.4.3",
|
|
80
|
-
"react-on-rails": "17.0.0-rc.
|
|
79
|
+
"react-on-rails": "17.0.0-rc.7"
|
|
81
80
|
},
|
|
82
81
|
"peerDependencies": {
|
|
83
82
|
"@honeybadger-io/js": ">=4.0.0",
|
|
@@ -173,7 +172,6 @@
|
|
|
173
172
|
"build-watch": "pnpm run clean && tsc --watch --project src/tsconfig.json",
|
|
174
173
|
"clean": "rm -rf ./lib",
|
|
175
174
|
"ci": "jest --ci --runInBand --reporters=default --reporters=jest-junit",
|
|
176
|
-
"developing": "nps node-renderer.debug",
|
|
177
175
|
"test": "jest tests",
|
|
178
176
|
"type-check": "tsc --noEmit --noErrorTruncation --project src/tsconfig.json"
|
|
179
177
|
}
|