react-on-rails-pro-node-renderer 16.7.0-rc.2 → 16.7.0-rc.3
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/api.d.ts +2 -2
- package/lib/integrations/api.js +5 -3
- package/lib/integrations/opentelemetry.d.ts +34 -0
- package/lib/integrations/opentelemetry.js +305 -0
- package/lib/shared/opentelemetryState.d.ts +4 -0
- package/lib/shared/opentelemetryState.js +12 -0
- package/lib/shared/tracing.d.ts +80 -1
- package/lib/shared/tracing.js +109 -1
- package/lib/testUtils/opentelemetry.d.ts +2 -0
- package/lib/testUtils/opentelemetry.js +66 -0
- package/lib/worker/fastifyConfig.d.ts +19 -0
- package/lib/worker/fastifyConfig.js +41 -0
- package/lib/worker/handleGracefulShutdown.js +50 -3
- package/lib/worker/handleIncrementalRenderRequest.js +22 -5
- package/lib/worker/handleIncrementalRenderStream.js +104 -101
- package/lib/worker/handleRenderRequest.d.ts +2 -1
- package/lib/worker/handleRenderRequest.js +82 -29
- package/lib/worker/shutdownHooks.d.ts +12 -0
- package/lib/worker/shutdownHooks.js +41 -0
- package/lib/worker.d.ts +1 -9
- package/lib/worker.js +84 -77
- package/package.json +48 -3
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resetOpenTelemetryForTest = resetOpenTelemetryForTest;
|
|
37
|
+
const opentelemetryState_js_1 = require("../shared/opentelemetryState.js");
|
|
38
|
+
const tracing_js_1 = require("../shared/tracing.js");
|
|
39
|
+
const fastifyConfig = __importStar(require("../worker/fastifyConfig.js"));
|
|
40
|
+
const shutdownHooks = __importStar(require("../worker/shutdownHooks.js"));
|
|
41
|
+
async function resetOpenTelemetryForTest() {
|
|
42
|
+
const tracerProvider = (0, opentelemetryState_js_1.getOpenTelemetryTracerProvider)();
|
|
43
|
+
if (tracerProvider) {
|
|
44
|
+
await tracerProvider.shutdown();
|
|
45
|
+
(0, opentelemetryState_js_1.setOpenTelemetryTracerProvider)(null);
|
|
46
|
+
}
|
|
47
|
+
(0, tracing_js_1.resetSubSpan)();
|
|
48
|
+
(0, tracing_js_1.resetTracing)();
|
|
49
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
50
|
+
fastifyConfig.__resetFastifyConfigFunctionsForTest();
|
|
51
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
52
|
+
shutdownHooks.__resetWorkerShutdownHooksForTest();
|
|
53
|
+
/* eslint-disable @typescript-eslint/no-require-imports, global-require */
|
|
54
|
+
try {
|
|
55
|
+
const otelApi = require('@opentelemetry/api');
|
|
56
|
+
otelApi.trace.disable();
|
|
57
|
+
otelApi.context.disable();
|
|
58
|
+
otelApi.propagation.disable();
|
|
59
|
+
otelApi.diag.disable();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// OTel API not installed - nothing to disable.
|
|
63
|
+
}
|
|
64
|
+
/* eslint-enable @typescript-eslint/no-require-imports, global-require */
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=opentelemetry.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FastifyInstance } from './types.js';
|
|
2
|
+
export type FastifyConfigFunction = (app: FastifyInstance) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Configures the Fastify instance before starting the server.
|
|
5
|
+
*
|
|
6
|
+
* This module intentionally has no runtime dependency on `worker.ts` or
|
|
7
|
+
* `fastify`, so integrations can register instrumentation before Fastify is
|
|
8
|
+
* required by the worker module graph.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerFastifyConfigFunction(configFunction: FastifyConfigFunction): () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Public one-way registration API for custom entrypoints and integrations.
|
|
13
|
+
* Internal callers use registerFastifyConfigFunction() when they need the
|
|
14
|
+
* unregister callback during failed initialization or shutdown cleanup.
|
|
15
|
+
*/
|
|
16
|
+
export declare function configureFastify(configFunction: FastifyConfigFunction): void;
|
|
17
|
+
export declare function applyFastifyConfigFunctions(app: FastifyInstance): void;
|
|
18
|
+
export declare function __resetFastifyConfigFunctionsForTest(): void;
|
|
19
|
+
//# sourceMappingURL=fastifyConfig.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerFastifyConfigFunction = registerFastifyConfigFunction;
|
|
4
|
+
exports.configureFastify = configureFastify;
|
|
5
|
+
exports.applyFastifyConfigFunctions = applyFastifyConfigFunctions;
|
|
6
|
+
exports.__resetFastifyConfigFunctionsForTest = __resetFastifyConfigFunctionsForTest;
|
|
7
|
+
const fastifyConfigFunctions = [];
|
|
8
|
+
/**
|
|
9
|
+
* Configures the Fastify instance before starting the server.
|
|
10
|
+
*
|
|
11
|
+
* This module intentionally has no runtime dependency on `worker.ts` or
|
|
12
|
+
* `fastify`, so integrations can register instrumentation before Fastify is
|
|
13
|
+
* required by the worker module graph.
|
|
14
|
+
*/
|
|
15
|
+
function registerFastifyConfigFunction(configFunction) {
|
|
16
|
+
fastifyConfigFunctions.push(configFunction);
|
|
17
|
+
return () => {
|
|
18
|
+
const index = fastifyConfigFunctions.indexOf(configFunction);
|
|
19
|
+
if (index >= 0) {
|
|
20
|
+
fastifyConfigFunctions.splice(index, 1);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Public one-way registration API for custom entrypoints and integrations.
|
|
26
|
+
* Internal callers use registerFastifyConfigFunction() when they need the
|
|
27
|
+
* unregister callback during failed initialization or shutdown cleanup.
|
|
28
|
+
*/
|
|
29
|
+
function configureFastify(configFunction) {
|
|
30
|
+
registerFastifyConfigFunction(configFunction);
|
|
31
|
+
}
|
|
32
|
+
function applyFastifyConfigFunctions(app) {
|
|
33
|
+
fastifyConfigFunctions.forEach((configFunction) => {
|
|
34
|
+
configFunction(app);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
38
|
+
function __resetFastifyConfigFunctionsForTest() {
|
|
39
|
+
fastifyConfigFunctions.length = 0;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=fastifyConfig.js.map
|
|
@@ -6,6 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const cluster_1 = __importDefault(require("cluster"));
|
|
7
7
|
const utils_js_1 = require("../shared/utils.js");
|
|
8
8
|
const log_js_1 = __importDefault(require("../shared/log.js"));
|
|
9
|
+
const shutdownHooks_js_1 = require("./shutdownHooks.js");
|
|
10
|
+
function errorCode(error) {
|
|
11
|
+
const code = error?.code;
|
|
12
|
+
return typeof code === 'string' ? code : undefined;
|
|
13
|
+
}
|
|
9
14
|
const handleGracefulShutdown = (app) => {
|
|
10
15
|
const { worker } = cluster_1.default;
|
|
11
16
|
if (!worker) {
|
|
@@ -14,12 +19,54 @@ const handleGracefulShutdown = (app) => {
|
|
|
14
19
|
}
|
|
15
20
|
let activeRequestsCount = 0;
|
|
16
21
|
let isShuttingDown = false;
|
|
22
|
+
let isDestroying = false;
|
|
23
|
+
const destroyWorkerAfterShutdownHooks = (context) => {
|
|
24
|
+
if (isDestroying) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
isDestroying = true;
|
|
28
|
+
log_js_1.default.debug('Worker #%d running shutdown hooks before shutdown after %s', worker.id, context);
|
|
29
|
+
let workerDestroyed = false;
|
|
30
|
+
const destroyWorker = () => {
|
|
31
|
+
if (workerDestroyed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
workerDestroyed = true;
|
|
35
|
+
worker.destroy();
|
|
36
|
+
};
|
|
37
|
+
const shutdownTimeout = setTimeout(() => {
|
|
38
|
+
log_js_1.default.warn('Worker #%d: shutdown hooks timed out, forcing worker.destroy()', worker.id);
|
|
39
|
+
destroyWorker();
|
|
40
|
+
}, shutdownHooks_js_1.WORKER_SHUTDOWN_HOOKS_TIMEOUT_MS);
|
|
41
|
+
const shutdownHooksPromise = (0, shutdownHooks_js_1.runWorkerShutdownHooks)();
|
|
42
|
+
void shutdownHooksPromise
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
log_js_1.default.warn({ msg: 'Error running worker shutdown hooks before worker shutdown', error });
|
|
45
|
+
})
|
|
46
|
+
.finally(() => {
|
|
47
|
+
clearTimeout(shutdownTimeout);
|
|
48
|
+
destroyWorker();
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const disconnectWorker = () => {
|
|
52
|
+
try {
|
|
53
|
+
worker.disconnect();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (errorCode(error) === 'ERR_IPC_DISCONNECTED') {
|
|
57
|
+
log_js_1.default.debug('Worker #%d IPC channel was already disconnected during graceful shutdown', worker.id);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
log_js_1.default.warn({ msg: 'Error disconnecting worker during graceful shutdown', error });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
17
64
|
// Helper to decrement counter and potentially kill worker
|
|
18
65
|
const decrementAndMaybeShutdown = (context) => {
|
|
19
66
|
activeRequestsCount -= 1;
|
|
20
67
|
if (isShuttingDown && activeRequestsCount === 0) {
|
|
21
68
|
log_js_1.default.debug('Worker #%d has no active requests after %s, killing the worker', worker.id, context);
|
|
22
|
-
|
|
69
|
+
destroyWorkerAfterShutdownHooks(context);
|
|
23
70
|
}
|
|
24
71
|
};
|
|
25
72
|
process.on('message', (msg) => {
|
|
@@ -28,11 +75,11 @@ const handleGracefulShutdown = (app) => {
|
|
|
28
75
|
isShuttingDown = true;
|
|
29
76
|
if (activeRequestsCount === 0) {
|
|
30
77
|
log_js_1.default.debug('Worker #%d has no active requests, killing the worker', worker.id);
|
|
31
|
-
|
|
78
|
+
destroyWorkerAfterShutdownHooks('shutdown message');
|
|
32
79
|
}
|
|
33
80
|
else {
|
|
34
81
|
log_js_1.default.debug('Worker #%d has "%d" active requests, disconnecting the worker', worker.id, activeRequestsCount);
|
|
35
|
-
|
|
82
|
+
disconnectWorker();
|
|
36
83
|
}
|
|
37
84
|
}
|
|
38
85
|
});
|
|
@@ -7,6 +7,13 @@ exports.handleIncrementalRenderRequest = handleIncrementalRenderRequest;
|
|
|
7
7
|
const handleRenderRequest_1 = require("./handleRenderRequest");
|
|
8
8
|
const log_1 = __importDefault(require("../shared/log"));
|
|
9
9
|
const utils_1 = require("../shared/utils");
|
|
10
|
+
const tracing_js_1 = require("../shared/tracing.js");
|
|
11
|
+
class InvalidIncrementalRenderChunkError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super('Invalid incremental render chunk received, missing properties');
|
|
14
|
+
this.name = 'InvalidIncrementalRenderChunkError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
10
17
|
function assertIsUpdateChunk(value) {
|
|
11
18
|
if (typeof value !== 'object' ||
|
|
12
19
|
value === null ||
|
|
@@ -14,7 +21,7 @@ function assertIsUpdateChunk(value) {
|
|
|
14
21
|
!('updateChunk' in value) ||
|
|
15
22
|
(typeof value.bundleTimestamp !== 'string' && typeof value.bundleTimestamp !== 'number') ||
|
|
16
23
|
typeof value.updateChunk !== 'string') {
|
|
17
|
-
throw new
|
|
24
|
+
throw new InvalidIncrementalRenderChunkError();
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
function assertFirstIncrementalRenderRequestChunk(chunk) {
|
|
@@ -63,6 +70,8 @@ async function handleIncrementalRenderRequest(initial) {
|
|
|
63
70
|
const { renderingRequest, onRequestClosedUpdateChunk } = firstRequestChunk;
|
|
64
71
|
try {
|
|
65
72
|
// Call handleRenderRequest internally to handle all validation and VM execution
|
|
73
|
+
// handleRenderRequest is called directly without a TracingContext from worker.ts's
|
|
74
|
+
// trace() wrapper, so there is no tracingContext to forward for its error path.
|
|
66
75
|
const { response, executionContext } = await (0, handleRenderRequest_1.handleRenderRequest)({
|
|
67
76
|
renderingRequest,
|
|
68
77
|
bundleTimestamp,
|
|
@@ -82,13 +91,21 @@ async function handleIncrementalRenderRequest(initial) {
|
|
|
82
91
|
add: async (chunk) => {
|
|
83
92
|
try {
|
|
84
93
|
assertIsUpdateChunk(chunk);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
await (0, tracing_js_1.subSpan)({ name: 'ror.incremental.process_chunk' }, async () => {
|
|
95
|
+
const bundlePath = (0, utils_1.getRequestBundleFilePath)(chunk.bundleTimestamp);
|
|
96
|
+
const result = await executionContext.runInVM(chunk.updateChunk, bundlePath);
|
|
97
|
+
if ((0, utils_1.isErrorRenderResult)(result)) {
|
|
98
|
+
throw new Error(result.exceptionMessage);
|
|
99
|
+
}
|
|
88
100
|
});
|
|
89
101
|
}
|
|
90
102
|
catch (err) {
|
|
91
|
-
|
|
103
|
+
if (err instanceof InvalidIncrementalRenderChunkError) {
|
|
104
|
+
log_1.default.error({ msg: 'Invalid incremental render chunk', err, chunk });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
log_1.default.error({ msg: 'Error running incremental render chunk', err, chunk });
|
|
108
|
+
}
|
|
92
109
|
}
|
|
93
110
|
},
|
|
94
111
|
handleRequestClosed: () => {
|
|
@@ -38,6 +38,7 @@ exports.handleIncrementalRenderStream = handleIncrementalRenderStream;
|
|
|
38
38
|
const string_decoder_1 = require("string_decoder");
|
|
39
39
|
const errorReporter = __importStar(require("../shared/errorReporter"));
|
|
40
40
|
const constants_1 = require("../shared/constants");
|
|
41
|
+
const tracing_js_1 = require("../shared/tracing.js");
|
|
41
42
|
/**
|
|
42
43
|
* Error thrown when waiting for a stream chunk times out.
|
|
43
44
|
*/
|
|
@@ -87,116 +88,118 @@ async function* withChunkTimeout(iterator, timeoutMs) {
|
|
|
87
88
|
* The first object triggers rendering, subsequent objects provide incremental updates.
|
|
88
89
|
*/
|
|
89
90
|
async function handleIncrementalRenderStream(options) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
`
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
parsed = JSON.parse(rawObject);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
const errorMessage = `Invalid JSON chunk: ${err instanceof Error ? err.message : String(err)}`;
|
|
125
|
-
if (!hasReceivedFirstObject) {
|
|
126
|
-
// Error in first chunk - throw error to stop processing
|
|
127
|
-
throw new Error(errorMessage);
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
// Error in subsequent chunks - log and report but continue processing
|
|
131
|
-
const reportedMessage = `JSON parsing error in update chunk: ${err instanceof Error ? err.message : String(err)}`;
|
|
132
|
-
errorReporter.message(reportedMessage);
|
|
133
|
-
// Skip this malformed chunk and continue with next ones
|
|
134
|
-
// eslint-disable-next-line no-continue
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (!hasReceivedFirstObject) {
|
|
139
|
-
hasReceivedFirstObject = true;
|
|
91
|
+
return (0, tracing_js_1.subSpan)({ name: 'ror.incremental.stream' }, async () => {
|
|
92
|
+
const { request, onRenderRequestReceived, onResponseStart, onUpdateReceived, onRequestEnded } = options;
|
|
93
|
+
let hasReceivedFirstObject = false;
|
|
94
|
+
const decoder = new string_decoder_1.StringDecoder('utf8');
|
|
95
|
+
let buffer = '';
|
|
96
|
+
let totalBytesReceived = 0;
|
|
97
|
+
let onResponseStartPromise = null;
|
|
98
|
+
try {
|
|
99
|
+
for await (const chunk of withChunkTimeout(request.raw, constants_1.STREAM_CHUNK_TIMEOUT_MS)) {
|
|
100
|
+
const chunkBuffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
|
|
101
|
+
totalBytesReceived += chunkBuffer.length;
|
|
102
|
+
// Check total request size limit
|
|
103
|
+
if (totalBytesReceived > constants_1.BODY_SIZE_LIMIT) {
|
|
104
|
+
throw new Error(`NDJSON request exceeds maximum size of ${constants_1.BODY_SIZE_LIMIT} bytes (${Math.round(constants_1.BODY_SIZE_LIMIT / 1024 / 1024)}MB). ` +
|
|
105
|
+
`Received ${totalBytesReceived} bytes.`);
|
|
106
|
+
}
|
|
107
|
+
const str = decoder.write(chunkBuffer);
|
|
108
|
+
buffer += str;
|
|
109
|
+
// Check single line size limit (protects against missing newlines)
|
|
110
|
+
if (buffer.length > constants_1.FIELD_SIZE_LIMIT) {
|
|
111
|
+
throw new Error(`NDJSON line exceeds maximum size of ${constants_1.FIELD_SIZE_LIMIT} bytes (${Math.round(constants_1.FIELD_SIZE_LIMIT / 1024 / 1024)}MB). ` +
|
|
112
|
+
`Current buffer: ${buffer.length} bytes. Ensure each JSON object is followed by a newline.`);
|
|
113
|
+
}
|
|
114
|
+
// Process all complete JSON objects in the buffer
|
|
115
|
+
let boundary = buffer.indexOf('\n');
|
|
116
|
+
while (boundary !== -1) {
|
|
117
|
+
const rawObject = buffer.slice(0, boundary).trim();
|
|
118
|
+
buffer = buffer.slice(boundary + 1);
|
|
119
|
+
boundary = buffer.indexOf('\n');
|
|
120
|
+
if (rawObject) {
|
|
121
|
+
let parsed;
|
|
140
122
|
try {
|
|
141
|
-
|
|
142
|
-
const result = await onRenderRequestReceived(parsed);
|
|
143
|
-
const { response, shouldContinue: continueFlag } = result;
|
|
144
|
-
onResponseStartPromise = Promise.resolve(onResponseStart(response));
|
|
145
|
-
if (!continueFlag) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
123
|
+
parsed = JSON.parse(rawObject);
|
|
148
124
|
}
|
|
149
125
|
catch (err) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
126
|
+
const errorMessage = `Invalid JSON chunk: ${err instanceof Error ? err.message : String(err)}`;
|
|
127
|
+
if (!hasReceivedFirstObject) {
|
|
128
|
+
// Error in first chunk - throw error to stop processing
|
|
129
|
+
throw new Error(errorMessage);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Error in subsequent chunks - log and report but continue processing
|
|
133
|
+
const reportedMessage = `JSON parsing error in update chunk: ${err instanceof Error ? err.message : String(err)}`;
|
|
134
|
+
errorReporter.message(reportedMessage);
|
|
135
|
+
// Skip this malformed chunk and continue with next ones
|
|
136
|
+
// eslint-disable-next-line no-continue
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
154
139
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
// Note: on the error path (catch block below), the yield is
|
|
174
|
-
// skipped — intentional, as there's no React work to process
|
|
175
|
-
// when the update failed.
|
|
176
|
-
// eslint-disable-next-line no-await-in-loop
|
|
177
|
-
await new Promise((resolve) => {
|
|
178
|
-
setImmediate(resolve);
|
|
179
|
-
});
|
|
140
|
+
if (!hasReceivedFirstObject) {
|
|
141
|
+
hasReceivedFirstObject = true;
|
|
142
|
+
try {
|
|
143
|
+
// eslint-disable-next-line no-await-in-loop
|
|
144
|
+
const result = await onRenderRequestReceived(parsed);
|
|
145
|
+
const { response, shouldContinue: continueFlag } = result;
|
|
146
|
+
onResponseStartPromise = Promise.resolve(onResponseStart(response));
|
|
147
|
+
if (!continueFlag) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
// Error in first chunk processing - throw error to stop processing
|
|
153
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
154
|
+
error.message = `Error processing initial render request: ${error.message}`;
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
180
157
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
158
|
+
else {
|
|
159
|
+
try {
|
|
160
|
+
// eslint-disable-next-line no-await-in-loop
|
|
161
|
+
await onUpdateReceived(parsed);
|
|
162
|
+
// Yield to the event loop after each update chunk so React's
|
|
163
|
+
// setImmediate(performWork) can fire. Without this, all setProp()
|
|
164
|
+
// calls batch into React's pingedTasks and are processed in a
|
|
165
|
+
// single performWork → single flushCompletedQueues, merging all
|
|
166
|
+
// resolved Suspense boundaries into one chunk.
|
|
167
|
+
// With this yield, each setProp triggers its own performWork →
|
|
168
|
+
// flushCompletedQueues → destination.flush(), producing a
|
|
169
|
+
// separate output chunk per resolved Suspense boundary.
|
|
170
|
+
//
|
|
171
|
+
// Tradeoff: adds one event-loop tick (~0.1ms) per update chunk.
|
|
172
|
+
// For N async props this costs N extra ticks, but the streaming
|
|
173
|
+
// granularity gain far outweighs it (see PR #3196 benchmarks).
|
|
174
|
+
//
|
|
175
|
+
// Note: on the error path (catch block below), the yield is
|
|
176
|
+
// skipped — intentional, as there's no React work to process
|
|
177
|
+
// when the update failed.
|
|
178
|
+
// eslint-disable-next-line no-await-in-loop
|
|
179
|
+
await new Promise((resolve) => {
|
|
180
|
+
setImmediate(resolve);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
// Error in update chunk processing - log and report but continue processing
|
|
185
|
+
const errorMessage = `Error processing update chunk: ${err instanceof Error ? err.message : String(err)}`;
|
|
186
|
+
errorReporter.message(errorMessage);
|
|
187
|
+
// Continue processing other chunks
|
|
188
|
+
}
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
catch (err) {
|
|
195
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
196
|
+
// Update the error message in place to retain the original stack trace, rather than creating a new error object
|
|
197
|
+
error.message = `Error while handling the request stream: ${error.message}`;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
// Stream ended normally
|
|
201
|
+
await onRequestEnded();
|
|
202
|
+
await onResponseStartPromise;
|
|
203
|
+
});
|
|
201
204
|
}
|
|
202
205
|
//# sourceMappingURL=handleIncrementalRenderStream.js.map
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* @module worker/handleRenderRequest
|
|
6
6
|
*/
|
|
7
7
|
import { Asset, ResponseResult, type RequestInfo } from '../shared/utils.js';
|
|
8
|
-
import type
|
|
8
|
+
import { type TracingContext } from '../shared/tracing.js';
|
|
9
9
|
import { ExecutionContext } from './vm.js';
|
|
10
|
+
export declare function sumUploadedBytes(assets: Asset[]): Promise<number>;
|
|
10
11
|
export type ProvidedNewBundle = {
|
|
11
12
|
timestamp: string | number;
|
|
12
13
|
bundle: Asset;
|