@bleedingdev/modern-js-prod-server 3.2.0-ultramodern.0
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/cjs/apply.js +97 -0
- package/dist/cjs/index.js +89 -0
- package/dist/cjs/libs/contractGateAutopilot.js +36 -0
- package/dist/cjs/libs/loadConfig.js +72 -0
- package/dist/cjs/libs/metrics.js +41 -0
- package/dist/cjs/libs/render/index.js +125 -0
- package/dist/cjs/libs/render/ssr.js +118 -0
- package/dist/cjs/libs/render/utils.js +72 -0
- package/dist/cjs/libs/runtimeFallbackWorkerLane.js +167 -0
- package/dist/cjs/libs/telemetry.js +87 -0
- package/dist/cjs/netlify.js +56 -0
- package/dist/cjs/server/index.js +579 -0
- package/dist/cjs/server/modernServer.js +472 -0
- package/dist/cjs/server/modernServerSplit.js +38 -0
- package/dist/cjs/types.js +18 -0
- package/dist/cjs/utils.js +38 -0
- package/dist/esm/apply.mjs +63 -0
- package/dist/esm/index.mjs +26 -0
- package/dist/esm/libs/contractGateAutopilot.mjs +1 -0
- package/dist/esm/libs/loadConfig.mjs +22 -0
- package/dist/esm/libs/metrics.mjs +7 -0
- package/dist/esm/libs/render/index.mjs +81 -0
- package/dist/esm/libs/render/ssr.mjs +73 -0
- package/dist/esm/libs/render/utils.mjs +35 -0
- package/dist/esm/libs/runtimeFallbackWorkerLane.mjs +130 -0
- package/dist/esm/libs/telemetry.mjs +1 -0
- package/dist/esm/netlify.mjs +22 -0
- package/dist/esm/rslib-runtime.mjs +18 -0
- package/dist/esm/server/index.mjs +535 -0
- package/dist/esm/server/modernServer.mjs +419 -0
- package/dist/esm/server/modernServerSplit.mjs +4 -0
- package/dist/esm/types.mjs +0 -0
- package/dist/esm/utils.mjs +4 -0
- package/dist/esm-node/apply.mjs +64 -0
- package/dist/esm-node/index.mjs +27 -0
- package/dist/esm-node/libs/contractGateAutopilot.mjs +2 -0
- package/dist/esm-node/libs/loadConfig.mjs +23 -0
- package/dist/esm-node/libs/metrics.mjs +8 -0
- package/dist/esm-node/libs/render/index.mjs +82 -0
- package/dist/esm-node/libs/render/ssr.mjs +75 -0
- package/dist/esm-node/libs/render/utils.mjs +36 -0
- package/dist/esm-node/libs/runtimeFallbackWorkerLane.mjs +131 -0
- package/dist/esm-node/libs/telemetry.mjs +2 -0
- package/dist/esm-node/netlify.mjs +23 -0
- package/dist/esm-node/rslib-runtime.mjs +19 -0
- package/dist/esm-node/server/index.mjs +536 -0
- package/dist/esm-node/server/modernServer.mjs +421 -0
- package/dist/esm-node/server/modernServerSplit.mjs +5 -0
- package/dist/esm-node/types.mjs +1 -0
- package/dist/esm-node/utils.mjs +5 -0
- package/dist/types/apply.d.ts +6 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/libs/metrics.d.ts +8 -0
- package/dist/types/libs/telemetry.d.ts +2 -0
- package/dist/types/netlify.d.ts +3 -0
- package/dist/types/types.d.ts +15 -0
- package/package.json +79 -0
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +7 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { AppContext, ConfigContext, loadPlugins, serverManager } from "@modern-js/server-core";
|
|
2
|
+
import { INTERNAL_SERVER_PLUGINS, OUTPUT_CONFIG_FILE, SHARED_DIR, createLogger, dotenv, dotenvExpand, ensureAbsolutePath, fs } from "@modern-js/utils";
|
|
3
|
+
import { promises } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { ContractGateAutopilot } from "../libs/contractGateAutopilot.mjs";
|
|
6
|
+
import { getServerConfigPath, loadConfig, requireConfig } from "../libs/loadConfig.mjs";
|
|
7
|
+
import { metrics } from "../libs/metrics.mjs";
|
|
8
|
+
import { DEFAULT_RUNTIME_FALLBACK_WORKER_TIMEOUT_MS, persistRuntimeFallbackContractGateInWorker } from "../libs/runtimeFallbackWorkerLane.mjs";
|
|
9
|
+
import { DEFAULT_RUNTIME_STATUS_ENDPOINT, TelemetryCanaryOrchestrator, TelemetryRegistry, createOtlpTelemetryExporter, createRuntimeFallbackSignalRuntimeState, createTelemetryAwareMetrics, createVictoriaMetricsTelemetryExporter, enforceRuntimeFallbackSignalAuthToken, enforceRuntimeFallbackSignalTrustPolicy, getRuntimeSignalErrorStatusCode, hasEnabledTelemetryExporters, normalizeRuntimeFallbackSignalAuthConfig, normalizeRuntimeFallbackTrustPolicy, parseRuntimeFallbackSignalPayloadFromRawBody, resolveRuntimeFallbackSignalEndpoint } from "../libs/telemetry.mjs";
|
|
10
|
+
import { debug } from "../utils.mjs";
|
|
11
|
+
import { createProdServer } from "./modernServerSplit.mjs";
|
|
12
|
+
const CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
13
|
+
const DEFAULT_RUNTIME_FALLBACK_GATE_NAME = 'runtime-mf-fallback-health';
|
|
14
|
+
const DEFAULT_RUNTIME_FALLBACK_FAILURE_HOLD_MS = 300000;
|
|
15
|
+
const DEFAULT_RUNTIME_FALLBACK_MAX_BODY_BYTES = 16384;
|
|
16
|
+
class Server {
|
|
17
|
+
async init({ disableHttpServer = false } = {
|
|
18
|
+
disableHttpServer: false
|
|
19
|
+
}) {
|
|
20
|
+
const { options } = this;
|
|
21
|
+
await this.loadServerEnv(options);
|
|
22
|
+
this.initServerConfig(options);
|
|
23
|
+
await this.injectContext(this.runner, options);
|
|
24
|
+
this.runner = await this.createHookRunner();
|
|
25
|
+
await this.initConfig(this.runner, options);
|
|
26
|
+
await this.initTelemetry(options);
|
|
27
|
+
await this.injectContext(this.runner, options);
|
|
28
|
+
this.server = this.serverImpl(options);
|
|
29
|
+
await this.runPrepareHook(this.runner);
|
|
30
|
+
if (!disableHttpServer) this.app = await this.server.createHTTPServer(this.getRequestHandler());
|
|
31
|
+
{
|
|
32
|
+
const result = await this.runner.beforeServerInit({
|
|
33
|
+
app: this.app,
|
|
34
|
+
server: this.server
|
|
35
|
+
});
|
|
36
|
+
({ app: this.app = this.app, server: this.server } = result);
|
|
37
|
+
}
|
|
38
|
+
await this.server.onInit(this.runner, this.app);
|
|
39
|
+
{
|
|
40
|
+
const result = await this.runner.afterServerInit({
|
|
41
|
+
app: this.app,
|
|
42
|
+
server: this.server
|
|
43
|
+
});
|
|
44
|
+
({ app: this.app = this.app, server: this.server } = result);
|
|
45
|
+
}
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
runConfigHook(runner, serverConfig) {
|
|
49
|
+
const newServerConfig = runner.config(serverConfig || {});
|
|
50
|
+
return newServerConfig;
|
|
51
|
+
}
|
|
52
|
+
async runPrepareHook(runner) {
|
|
53
|
+
runner.prepare();
|
|
54
|
+
}
|
|
55
|
+
initServerConfig(options) {
|
|
56
|
+
const { pwd, serverConfigFile } = options;
|
|
57
|
+
const distDirectory = path.join(pwd, options.config.output.path || 'dist');
|
|
58
|
+
const serverConfigPath = getServerConfigPath(distDirectory, serverConfigFile);
|
|
59
|
+
const serverConfig = requireConfig(serverConfigPath);
|
|
60
|
+
this.serverConfig = serverConfig;
|
|
61
|
+
}
|
|
62
|
+
async initConfig(runner, options) {
|
|
63
|
+
const { pwd, config } = options;
|
|
64
|
+
const { serverConfig } = this;
|
|
65
|
+
const finalServerConfig = this.runConfigHook(runner, serverConfig);
|
|
66
|
+
const resolvedConfigPath = ensureAbsolutePath(pwd, path.join(config.output.path || 'dist', OUTPUT_CONFIG_FILE));
|
|
67
|
+
options.config = loadConfig({
|
|
68
|
+
cliConfig: config,
|
|
69
|
+
serverConfig: finalServerConfig,
|
|
70
|
+
resolvedConfigPath
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async initTelemetry(options) {
|
|
74
|
+
const telemetryConfig = options.config.server?.telemetry;
|
|
75
|
+
if (!telemetryConfig) return;
|
|
76
|
+
const hasEnabledExporters = hasEnabledTelemetryExporters(telemetryConfig);
|
|
77
|
+
if (true !== telemetryConfig.enabled && !hasEnabledExporters) return;
|
|
78
|
+
const registry = new TelemetryRegistry({
|
|
79
|
+
service: telemetryConfig.service || options.appContext?.metaName || 'modern-js',
|
|
80
|
+
module: telemetryConfig.module || 'server',
|
|
81
|
+
environment: telemetryConfig.environment || process.env.MODERN_ENV || process.env.NODE_ENV || 'development',
|
|
82
|
+
samplingRate: telemetryConfig.samplingRate,
|
|
83
|
+
flushIntervalMs: telemetryConfig.flushIntervalMs,
|
|
84
|
+
maxBatchSize: telemetryConfig.maxBatchSize,
|
|
85
|
+
maxQueueSize: telemetryConfig.maxQueueSize,
|
|
86
|
+
redactionKeys: telemetryConfig.redactionKeys,
|
|
87
|
+
slo: {
|
|
88
|
+
queueUtilizationWarnThreshold: telemetryConfig.slo?.queueUtilizationWarnThreshold,
|
|
89
|
+
queueDroppedWarnThreshold: telemetryConfig.slo?.queueDroppedWarnThreshold,
|
|
90
|
+
alertCooldownMs: telemetryConfig.slo?.alertCooldownMs,
|
|
91
|
+
onAlert: (alert)=>{
|
|
92
|
+
options.logger?.warn(`[telemetry.slo] ${alert.type} threshold=${alert.threshold} value=${alert.value} depth=${alert.queueDepth}/${alert.queueCapacity} dropped=${alert.totalDropped}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (telemetryConfig.exporters?.otlp?.enabled) await registry.register(createOtlpTelemetryExporter(telemetryConfig.exporters.otlp));
|
|
97
|
+
if (telemetryConfig.exporters?.victoriaMetrics?.enabled) await registry.register(createVictoriaMetricsTelemetryExporter(telemetryConfig.exporters.victoriaMetrics));
|
|
98
|
+
try {
|
|
99
|
+
await registry.startupHealthCheck({
|
|
100
|
+
failLoud: telemetryConfig.failLoudStartup ?? true
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
await registry.shutdown();
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
options.metrics = createTelemetryAwareMetrics(options.metrics || metrics, registry);
|
|
107
|
+
this.telemetryRegistry = registry;
|
|
108
|
+
const canaryConfig = telemetryConfig.canary;
|
|
109
|
+
if (canaryConfig?.enabled) {
|
|
110
|
+
const contractGates = canaryConfig.contractGates;
|
|
111
|
+
const orchestrator = new TelemetryCanaryOrchestrator({
|
|
112
|
+
registry,
|
|
113
|
+
evaluationIntervalMs: canaryConfig.evaluationIntervalMs,
|
|
114
|
+
minConsecutiveHealthyEvaluations: canaryConfig.minConsecutiveHealthyEvaluations,
|
|
115
|
+
rollbackConsecutiveFailures: canaryConfig.rollbackConsecutiveFailures,
|
|
116
|
+
maxQueueUtilization: canaryConfig.maxQueueUtilization,
|
|
117
|
+
maxTotalDropped: canaryConfig.maxTotalDropped,
|
|
118
|
+
maxUnhealthyExporters: canaryConfig.maxUnhealthyExporters,
|
|
119
|
+
requiredContractGates: Object.keys(contractGates || {}),
|
|
120
|
+
onPromote: (decision)=>{
|
|
121
|
+
options.logger?.info(`[telemetry.canary] promoted after ${decision.consecutiveHealthy} healthy evaluations`);
|
|
122
|
+
this.emitCanaryDecisionMetric(registry, decision, 'promote');
|
|
123
|
+
},
|
|
124
|
+
onRollback: (decision)=>{
|
|
125
|
+
options.logger?.error(`[telemetry.canary] rollback triggered failures=${decision.failures.map((item)=>item.reason).join(',')}`);
|
|
126
|
+
this.emitCanaryDecisionMetric(registry, decision, 'rollback');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (contractGates) orchestrator.setContractGates(contractGates);
|
|
130
|
+
this.canaryOrchestrator = orchestrator;
|
|
131
|
+
orchestrator.start();
|
|
132
|
+
const autopilotEnabled = canaryConfig.autopilot?.enabled ?? true;
|
|
133
|
+
if (autopilotEnabled) {
|
|
134
|
+
const gateSnapshotPath = this.resolveContractGateSnapshotPath(options, canaryConfig.autopilot?.gateSnapshotPath);
|
|
135
|
+
this.contractGateAutopilot = new ContractGateAutopilot({
|
|
136
|
+
orchestrator,
|
|
137
|
+
gateSnapshotPath,
|
|
138
|
+
pollIntervalMs: canaryConfig.autopilot?.pollIntervalMs,
|
|
139
|
+
gateStaleAfterMs: canaryConfig.autopilot?.gateStaleAfterMs,
|
|
140
|
+
logger: {
|
|
141
|
+
info: (message)=>{
|
|
142
|
+
options.logger?.info(message);
|
|
143
|
+
},
|
|
144
|
+
warn: (message)=>{
|
|
145
|
+
options.logger?.warn(message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
await this.contractGateAutopilot.start();
|
|
150
|
+
const runtimeSignalConfig = canaryConfig.autopilot?.runtimeFallbackSignal;
|
|
151
|
+
const runtimeSignalEnabled = runtimeSignalConfig?.enabled ?? true;
|
|
152
|
+
if (runtimeSignalEnabled) {
|
|
153
|
+
const workerLaneConfig = runtimeSignalConfig?.workerLane;
|
|
154
|
+
const workerLaneEnabledFromEnv = 'true' === process.env.MODERN_RUNTIME_FALLBACK_WORKER_LANE;
|
|
155
|
+
const workerLaneEnabled = 'boolean' == typeof workerLaneConfig?.enabled ? workerLaneConfig.enabled : workerLaneEnabledFromEnv;
|
|
156
|
+
this.runtimeFallbackSignalConfig = {
|
|
157
|
+
endpoint: resolveRuntimeFallbackSignalEndpoint(runtimeSignalConfig?.endpoint),
|
|
158
|
+
gateName: runtimeSignalConfig?.gateName?.trim() || DEFAULT_RUNTIME_FALLBACK_GATE_NAME,
|
|
159
|
+
gateSnapshotPath,
|
|
160
|
+
failureHoldMs: Math.max(1000, runtimeSignalConfig?.failureHoldMs ?? DEFAULT_RUNTIME_FALLBACK_FAILURE_HOLD_MS),
|
|
161
|
+
maxBodyBytes: Math.max(512, runtimeSignalConfig?.maxBodyBytes ?? DEFAULT_RUNTIME_FALLBACK_MAX_BODY_BYTES),
|
|
162
|
+
auth: normalizeRuntimeFallbackSignalAuthConfig(runtimeSignalConfig?.auth),
|
|
163
|
+
trustPolicy: normalizeRuntimeFallbackTrustPolicy(runtimeSignalConfig?.trustPolicy),
|
|
164
|
+
runtimeState: createRuntimeFallbackSignalRuntimeState(),
|
|
165
|
+
workerLane: {
|
|
166
|
+
enabled: workerLaneEnabled,
|
|
167
|
+
timeoutMs: Math.max(25, workerLaneConfig?.timeoutMs ?? DEFAULT_RUNTIME_FALLBACK_WORKER_TIMEOUT_MS),
|
|
168
|
+
workerSuccessCount: 0,
|
|
169
|
+
fallbackToMainThreadCount: 0
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
orchestrator.evaluate();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
resolveContractGateSnapshotPath(options, configuredPath) {
|
|
178
|
+
const rawPath = configuredPath || process.env.MODERN_CONTRACT_GATES_FILE || '.modern/contract-gates.json';
|
|
179
|
+
if (path.isAbsolute(rawPath)) return rawPath;
|
|
180
|
+
return path.resolve(options.pwd, rawPath);
|
|
181
|
+
}
|
|
182
|
+
emitCanaryDecisionMetric(registry, decision, action) {
|
|
183
|
+
try {
|
|
184
|
+
registry.enqueueMetric({
|
|
185
|
+
name: `telemetry.canary.${action}`,
|
|
186
|
+
value: 1,
|
|
187
|
+
unit: 'count',
|
|
188
|
+
tags: {
|
|
189
|
+
action,
|
|
190
|
+
state: decision.state,
|
|
191
|
+
failures: String(decision.failures.length)
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} catch (_error) {}
|
|
195
|
+
}
|
|
196
|
+
async close() {
|
|
197
|
+
if (this.contractGateAutopilot) {
|
|
198
|
+
this.contractGateAutopilot.stop();
|
|
199
|
+
this.contractGateAutopilot = void 0;
|
|
200
|
+
}
|
|
201
|
+
this.runtimeFallbackSignalConfig = void 0;
|
|
202
|
+
if (this.canaryOrchestrator) this.canaryOrchestrator.stop();
|
|
203
|
+
if (this.telemetryRegistry) await this.telemetryRegistry.shutdown();
|
|
204
|
+
if (!this.app) return;
|
|
205
|
+
await new Promise((resolve)=>{
|
|
206
|
+
this.app.close(()=>resolve());
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
listen(options, listener) {
|
|
210
|
+
const callback = ()=>{
|
|
211
|
+
listener?.();
|
|
212
|
+
};
|
|
213
|
+
if ('object' == typeof options) {
|
|
214
|
+
if (process.env.PORT) Object.assign(options, {
|
|
215
|
+
port: process.env.PORT
|
|
216
|
+
});
|
|
217
|
+
this.app.listen(options, callback);
|
|
218
|
+
} else this.app.listen(process.env.PORT || options || 8080, callback);
|
|
219
|
+
}
|
|
220
|
+
getRequestHandler() {
|
|
221
|
+
const requestHandler = this.server.getRequestHandler();
|
|
222
|
+
return (req, res, next)=>{
|
|
223
|
+
if (this.shouldHandleRuntimeStatus(req)) return void this.handleRuntimeStatus(req, res);
|
|
224
|
+
if (this.shouldHandleRuntimeFallbackSignal(req)) return void this.handleRuntimeFallbackSignal(req, res);
|
|
225
|
+
return requestHandler(req, res, next);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
shouldHandleRuntimeFallbackSignal(req) {
|
|
229
|
+
const runtimeSignalConfig = this.runtimeFallbackSignalConfig;
|
|
230
|
+
if (!runtimeSignalConfig) return false;
|
|
231
|
+
if ('POST' !== (req.method || 'GET').toUpperCase()) return false;
|
|
232
|
+
const pathName = this.getRequestPath(req.url);
|
|
233
|
+
return pathName === runtimeSignalConfig.endpoint;
|
|
234
|
+
}
|
|
235
|
+
shouldHandleRuntimeStatus(req) {
|
|
236
|
+
if ('GET' !== (req.method || 'GET').toUpperCase()) return false;
|
|
237
|
+
const pathName = this.getRequestPath(req.url);
|
|
238
|
+
return pathName === this.runtimeStatusEndpoint;
|
|
239
|
+
}
|
|
240
|
+
getRequestPath(urlValue) {
|
|
241
|
+
try {
|
|
242
|
+
const requestUrl = new URL(urlValue || '/', 'http://127.0.0.1');
|
|
243
|
+
return requestUrl.pathname;
|
|
244
|
+
} catch (_error) {
|
|
245
|
+
return '/';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async readRequestBody(req, maxBodyBytes) {
|
|
249
|
+
return new Promise((resolve, reject)=>{
|
|
250
|
+
const chunks = [];
|
|
251
|
+
let totalBytes = 0;
|
|
252
|
+
let done = false;
|
|
253
|
+
const cleanup = ()=>{
|
|
254
|
+
req.off('data', onData);
|
|
255
|
+
req.off('end', onEnd);
|
|
256
|
+
req.off('error', onError);
|
|
257
|
+
};
|
|
258
|
+
const onData = (chunk)=>{
|
|
259
|
+
if (done) return;
|
|
260
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
261
|
+
totalBytes += buffer.length;
|
|
262
|
+
if (totalBytes > maxBodyBytes) {
|
|
263
|
+
const error = new Error('runtime fallback signal payload too large');
|
|
264
|
+
error.code = 'PAYLOAD_TOO_LARGE';
|
|
265
|
+
done = true;
|
|
266
|
+
cleanup();
|
|
267
|
+
reject(error);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
chunks.push(buffer);
|
|
271
|
+
};
|
|
272
|
+
const onEnd = ()=>{
|
|
273
|
+
if (done) return;
|
|
274
|
+
done = true;
|
|
275
|
+
cleanup();
|
|
276
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
277
|
+
};
|
|
278
|
+
const onError = (error)=>{
|
|
279
|
+
if (done) return;
|
|
280
|
+
done = true;
|
|
281
|
+
cleanup();
|
|
282
|
+
reject(error);
|
|
283
|
+
};
|
|
284
|
+
req.on('data', onData);
|
|
285
|
+
req.on('end', onEnd);
|
|
286
|
+
req.on('error', onError);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async handleRuntimeFallbackSignal(req, res) {
|
|
290
|
+
const runtimeSignalConfig = this.runtimeFallbackSignalConfig;
|
|
291
|
+
if (!runtimeSignalConfig) {
|
|
292
|
+
res.statusCode = 404;
|
|
293
|
+
res.end();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
enforceRuntimeFallbackSignalAuthToken(this.getRequestHeader(req, runtimeSignalConfig.auth.headerName), runtimeSignalConfig.auth);
|
|
298
|
+
const rawBody = await this.readRequestBody(req, runtimeSignalConfig.maxBodyBytes);
|
|
299
|
+
const payload = parseRuntimeFallbackSignalPayloadFromRawBody(rawBody, runtimeSignalConfig.maxBodyBytes);
|
|
300
|
+
const trustResult = enforceRuntimeFallbackSignalTrustPolicy(payload, {
|
|
301
|
+
trustPolicy: runtimeSignalConfig.trustPolicy,
|
|
302
|
+
runtimeState: runtimeSignalConfig.runtimeState
|
|
303
|
+
});
|
|
304
|
+
if (trustResult.deduped) {
|
|
305
|
+
res.statusCode = 202;
|
|
306
|
+
res.setHeader('content-type', 'application/json');
|
|
307
|
+
res.end('{"ok":true,"deduped":true}');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
let persistedByWorkerLane = false;
|
|
311
|
+
if (runtimeSignalConfig.workerLane.enabled) {
|
|
312
|
+
const workerResult = await persistRuntimeFallbackContractGateInWorker({
|
|
313
|
+
snapshotPath: runtimeSignalConfig.gateSnapshotPath,
|
|
314
|
+
gateName: runtimeSignalConfig.gateName,
|
|
315
|
+
failureHoldMs: runtimeSignalConfig.failureHoldMs,
|
|
316
|
+
payload: payload,
|
|
317
|
+
schemaVersion: CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION
|
|
318
|
+
}, {
|
|
319
|
+
enabled: true,
|
|
320
|
+
timeoutMs: runtimeSignalConfig.workerLane.timeoutMs
|
|
321
|
+
});
|
|
322
|
+
if (workerResult.ok) {
|
|
323
|
+
persistedByWorkerLane = true;
|
|
324
|
+
runtimeSignalConfig.workerLane.workerSuccessCount += 1;
|
|
325
|
+
runtimeSignalConfig.workerLane.lastError = void 0;
|
|
326
|
+
const payloadRecord = payload;
|
|
327
|
+
const reason = 'string' == typeof payloadRecord.reason ? payloadRecord.reason : 'runtime_fallback';
|
|
328
|
+
const phase = 'string' == typeof payloadRecord.phase ? payloadRecord.phase : 'unknown';
|
|
329
|
+
const appName = 'string' == typeof payloadRecord.appName ? payloadRecord.appName : 'unknown';
|
|
330
|
+
this.options.logger?.warn(`[telemetry.canary.autopilot] runtime fallback signal gate=${runtimeSignalConfig.gateName} reason=${reason} phase=${phase} app=${appName} workerLane=true`);
|
|
331
|
+
} else {
|
|
332
|
+
runtimeSignalConfig.workerLane.fallbackToMainThreadCount += 1;
|
|
333
|
+
runtimeSignalConfig.workerLane.lastError = workerResult.error;
|
|
334
|
+
this.options.logger?.warn(`[telemetry.canary.autopilot] runtime fallback worker lane fallback: ${workerResult.error || 'unknown_error'}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!persistedByWorkerLane) await this.persistRuntimeFallbackContractGate(payload, runtimeSignalConfig);
|
|
338
|
+
res.statusCode = 202;
|
|
339
|
+
res.setHeader('content-type', 'application/json');
|
|
340
|
+
res.end('{"ok":true}');
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const signalError = error;
|
|
343
|
+
res.statusCode = getRuntimeSignalErrorStatusCode(signalError);
|
|
344
|
+
res.setHeader('content-type', 'application/json');
|
|
345
|
+
res.end(`{"ok":false,"error":${JSON.stringify(signalError instanceof Error ? signalError.message : String(signalError))}}`);
|
|
346
|
+
this.options.logger?.warn(`[telemetry.canary.autopilot] runtime fallback signal rejected: ${error instanceof Error ? error.message : String(error)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async handleRuntimeStatus(req, res) {
|
|
350
|
+
try {
|
|
351
|
+
if (this.runtimeFallbackSignalConfig?.auth.enabled) enforceRuntimeFallbackSignalAuthToken(this.getRequestHeader(req, this.runtimeFallbackSignalConfig.auth.headerName), this.runtimeFallbackSignalConfig.auth);
|
|
352
|
+
res.statusCode = 200;
|
|
353
|
+
res.setHeader('content-type', 'application/json');
|
|
354
|
+
res.end(JSON.stringify(this.buildRuntimeStatusPayload()));
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const signalError = error;
|
|
357
|
+
res.statusCode = getRuntimeSignalErrorStatusCode(signalError);
|
|
358
|
+
res.setHeader('content-type', 'application/json');
|
|
359
|
+
res.end(`{"ok":false,"error":${JSON.stringify(signalError instanceof Error ? signalError.message : String(signalError))}}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
buildRuntimeStatusPayload() {
|
|
363
|
+
const telemetry = this.telemetryRegistry ? {
|
|
364
|
+
enabled: true,
|
|
365
|
+
queueStats: this.telemetryRegistry.getQueueStats(),
|
|
366
|
+
exporterHealth: this.telemetryRegistry.getExporterHealth()
|
|
367
|
+
} : {
|
|
368
|
+
enabled: false,
|
|
369
|
+
queueStats: null,
|
|
370
|
+
exporterHealth: []
|
|
371
|
+
};
|
|
372
|
+
const canary = this.canaryOrchestrator ? {
|
|
373
|
+
enabled: true,
|
|
374
|
+
...this.canaryOrchestrator.getStatusSnapshot()
|
|
375
|
+
} : {
|
|
376
|
+
enabled: false
|
|
377
|
+
};
|
|
378
|
+
const runtimeFallbackSignal = this.runtimeFallbackSignalConfig ? {
|
|
379
|
+
enabled: true,
|
|
380
|
+
endpoint: this.runtimeFallbackSignalConfig.endpoint,
|
|
381
|
+
gateName: this.runtimeFallbackSignalConfig.gateName,
|
|
382
|
+
failureHoldMs: this.runtimeFallbackSignalConfig.failureHoldMs,
|
|
383
|
+
maxBodyBytes: this.runtimeFallbackSignalConfig.maxBodyBytes,
|
|
384
|
+
auth: {
|
|
385
|
+
enabled: this.runtimeFallbackSignalConfig.auth.enabled,
|
|
386
|
+
headerName: this.runtimeFallbackSignalConfig.auth.headerName
|
|
387
|
+
},
|
|
388
|
+
trustPolicy: {
|
|
389
|
+
allowedApps: this.runtimeFallbackSignalConfig.trustPolicy.allowedApps,
|
|
390
|
+
allowedEntryOrigins: this.runtimeFallbackSignalConfig.trustPolicy.allowedEntryOrigins,
|
|
391
|
+
enforceRuntimeDigest: this.runtimeFallbackSignalConfig.trustPolicy.enforceRuntimeDigest,
|
|
392
|
+
expectedRuntimeDigestsCount: Object.keys(this.runtimeFallbackSignalConfig.trustPolicy.expectedRuntimeDigests).length,
|
|
393
|
+
maxSignalsPerWindow: this.runtimeFallbackSignalConfig.trustPolicy.maxSignalsPerWindow,
|
|
394
|
+
windowMs: this.runtimeFallbackSignalConfig.trustPolicy.windowMs,
|
|
395
|
+
dedupeWindowMs: this.runtimeFallbackSignalConfig.trustPolicy.dedupeWindowMs
|
|
396
|
+
},
|
|
397
|
+
workerLane: {
|
|
398
|
+
enabled: this.runtimeFallbackSignalConfig.workerLane.enabled,
|
|
399
|
+
timeoutMs: this.runtimeFallbackSignalConfig.workerLane.timeoutMs,
|
|
400
|
+
workerSuccessCount: this.runtimeFallbackSignalConfig.workerLane.workerSuccessCount,
|
|
401
|
+
fallbackToMainThreadCount: this.runtimeFallbackSignalConfig.workerLane.fallbackToMainThreadCount,
|
|
402
|
+
lastError: this.runtimeFallbackSignalConfig.workerLane.lastError
|
|
403
|
+
}
|
|
404
|
+
} : {
|
|
405
|
+
enabled: false
|
|
406
|
+
};
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
timestamp: Date.now(),
|
|
410
|
+
telemetry,
|
|
411
|
+
canary,
|
|
412
|
+
runtimeFallbackSignal
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
getRequestHeader(req, headerName) {
|
|
416
|
+
const raw = req.headers[headerName.toLowerCase()];
|
|
417
|
+
if (Array.isArray(raw)) return raw[0];
|
|
418
|
+
if ('string' == typeof raw) return raw;
|
|
419
|
+
}
|
|
420
|
+
async persistRuntimeFallbackContractGate(payload, runtimeSignalConfig) {
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
const snapshotPath = runtimeSignalConfig.gateSnapshotPath;
|
|
423
|
+
let snapshot = {
|
|
424
|
+
schemaVersion: CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
|
|
425
|
+
updatedAt: now,
|
|
426
|
+
gates: {}
|
|
427
|
+
};
|
|
428
|
+
if (await fs.pathExists(snapshotPath)) try {
|
|
429
|
+
const raw = await promises.readFile(snapshotPath, 'utf8');
|
|
430
|
+
const parsed = JSON.parse(raw);
|
|
431
|
+
if (parsed && 'object' == typeof parsed) snapshot = {
|
|
432
|
+
schemaVersion: 'number' == typeof parsed.schemaVersion ? parsed.schemaVersion : CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
|
|
433
|
+
updatedAt: 'number' == typeof parsed.updatedAt ? parsed.updatedAt : now,
|
|
434
|
+
gates: parsed.gates && 'object' == typeof parsed.gates ? parsed.gates : {}
|
|
435
|
+
};
|
|
436
|
+
} catch (_error) {
|
|
437
|
+
snapshot = {
|
|
438
|
+
schemaVersion: CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION,
|
|
439
|
+
updatedAt: now,
|
|
440
|
+
gates: {}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const reason = 'string' == typeof payload.reason ? payload.reason : 'runtime_fallback';
|
|
444
|
+
const phase = 'string' == typeof payload.phase ? payload.phase : 'unknown';
|
|
445
|
+
const appName = 'string' == typeof payload.appName ? payload.appName : 'unknown';
|
|
446
|
+
const entry = 'string' == typeof payload.entry ? payload.entry : void 0;
|
|
447
|
+
snapshot.schemaVersion = CONTRACT_GATE_SNAPSHOT_SCHEMA_VERSION;
|
|
448
|
+
snapshot.updatedAt = now;
|
|
449
|
+
snapshot.gates = snapshot.gates || {};
|
|
450
|
+
snapshot.gates[runtimeSignalConfig.gateName] = {
|
|
451
|
+
passed: false,
|
|
452
|
+
reason: `runtime_fallback:${reason} phase=${phase} app=${appName}${entry ? ` entry=${entry}` : ''}`,
|
|
453
|
+
updatedAt: now,
|
|
454
|
+
expiresAt: now + runtimeSignalConfig.failureHoldMs,
|
|
455
|
+
source: 'runtime-mf-fallback-signal',
|
|
456
|
+
metadata: payload
|
|
457
|
+
};
|
|
458
|
+
await promises.mkdir(path.dirname(snapshotPath), {
|
|
459
|
+
recursive: true
|
|
460
|
+
});
|
|
461
|
+
await promises.writeFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
462
|
+
this.options.logger?.warn(`[telemetry.canary.autopilot] runtime fallback signal gate=${runtimeSignalConfig.gateName} reason=${reason} phase=${phase} app=${appName}`);
|
|
463
|
+
}
|
|
464
|
+
async render(req, res, url) {
|
|
465
|
+
return this.server.render(req, res, url);
|
|
466
|
+
}
|
|
467
|
+
async createHookRunner() {
|
|
468
|
+
serverManager.clear();
|
|
469
|
+
const { options } = this;
|
|
470
|
+
const { internalPlugins = INTERNAL_SERVER_PLUGINS, pwd, plugins = [] } = options;
|
|
471
|
+
const serverPlugins = this.serverConfig.plugins || [];
|
|
472
|
+
const loadedPlugins = loadPlugins(pwd, [
|
|
473
|
+
...serverPlugins,
|
|
474
|
+
...plugins
|
|
475
|
+
], {
|
|
476
|
+
internalPlugins
|
|
477
|
+
});
|
|
478
|
+
debug('plugins', loadedPlugins);
|
|
479
|
+
loadedPlugins.forEach((p)=>{
|
|
480
|
+
serverManager.usePlugin(p);
|
|
481
|
+
});
|
|
482
|
+
const hooksRunner = await serverManager.init();
|
|
483
|
+
return hooksRunner;
|
|
484
|
+
}
|
|
485
|
+
async injectContext(runner, options) {
|
|
486
|
+
const appContext = this.initAppContext();
|
|
487
|
+
const { config, pwd } = options;
|
|
488
|
+
ConfigContext.set(config);
|
|
489
|
+
AppContext.set({
|
|
490
|
+
...appContext,
|
|
491
|
+
distDirectory: path.join(pwd, config.output.path || 'dist')
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
initAppContext() {
|
|
495
|
+
const { options } = this;
|
|
496
|
+
const { pwd: appDirectory, plugins = [], config, appContext } = options;
|
|
497
|
+
const serverPlugins = plugins.map((p)=>({
|
|
498
|
+
server: p
|
|
499
|
+
}));
|
|
500
|
+
return {
|
|
501
|
+
appDirectory,
|
|
502
|
+
apiDirectory: appContext?.apiDirectory,
|
|
503
|
+
lambdaDirectory: appContext?.lambdaDirectory,
|
|
504
|
+
sharedDirectory: appContext?.sharedDirectory || path.resolve(appDirectory, SHARED_DIR),
|
|
505
|
+
distDirectory: path.join(appDirectory, config.output.path || 'dist'),
|
|
506
|
+
plugins: serverPlugins
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async loadServerEnv(options) {
|
|
510
|
+
const { pwd: appDirectory } = options;
|
|
511
|
+
const serverEnv = process.env.MODERN_ENV;
|
|
512
|
+
const defaultEnvPath = path.resolve(appDirectory, ".env");
|
|
513
|
+
const serverEnvPath = path.resolve(appDirectory, `.env.${serverEnv}`);
|
|
514
|
+
for (const envPath of [
|
|
515
|
+
serverEnvPath,
|
|
516
|
+
defaultEnvPath
|
|
517
|
+
])if (await fs.pathExists(envPath) && !(await fs.stat(envPath)).isDirectory()) {
|
|
518
|
+
const envConfig = dotenv.config({
|
|
519
|
+
path: envPath
|
|
520
|
+
});
|
|
521
|
+
dotenvExpand(envConfig);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
constructor(options){
|
|
525
|
+
this.serverImpl = createProdServer;
|
|
526
|
+
this.runtimeStatusEndpoint = DEFAULT_RUNTIME_STATUS_ENDPOINT;
|
|
527
|
+
options.logger = options.logger || createLogger({
|
|
528
|
+
level: 'warn'
|
|
529
|
+
});
|
|
530
|
+
options.metrics = options.metrics || metrics;
|
|
531
|
+
this.options = options;
|
|
532
|
+
this.serverConfig = {};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
export { Server };
|