@forklaunch/hyper-express 0.7.11 → 0.8.1
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/index.d.mts +33 -37
- package/lib/index.d.ts +33 -37
- package/lib/index.js +310 -59
- package/lib/index.mjs +312 -59
- package/package.json +12 -12
package/lib/index.js
CHANGED
@@ -100,9 +100,149 @@ var trace = (schemaValidator, path, contractDetails, ...handlers2) => {
|
|
100
100
|
var import_common3 = require("@forklaunch/common");
|
101
101
|
var import_http13 = require("@forklaunch/core/http");
|
102
102
|
var import_hyper_express_fork = require("@forklaunch/hyper-express-fork");
|
103
|
+
var import_zod = require("@forklaunch/validator/zod");
|
103
104
|
var import_express_api_reference = require("@scalar/express-api-reference");
|
104
105
|
var import_crypto = __toESM(require("crypto"));
|
105
106
|
|
107
|
+
// src/cluster/hyperExpress.cluster.ts
|
108
|
+
var import_node_cluster = __toESM(require("cluster"));
|
109
|
+
var os = __toESM(require("os"));
|
110
|
+
var uWS = __toESM(require("uWebSockets.js"));
|
111
|
+
function startHyperExpressCluster(config) {
|
112
|
+
const PORT = config.port;
|
113
|
+
const HOST = config.host;
|
114
|
+
const WORKERS = config.workerCount;
|
115
|
+
const app = config.expressApp;
|
116
|
+
const openTelemetryCollector = config.openTelemetryCollector;
|
117
|
+
if (WORKERS > os.cpus().length) {
|
118
|
+
throw new Error("Worker count cannot be greater than the number of CPUs");
|
119
|
+
}
|
120
|
+
if (import_node_cluster.default.isPrimary) {
|
121
|
+
let startWorker2 = function(i) {
|
122
|
+
if (isShuttingDown) return;
|
123
|
+
const worker = import_node_cluster.default.fork({
|
124
|
+
WORKER_INDEX: i.toString(),
|
125
|
+
PORT: PORT.toString(),
|
126
|
+
HOST
|
127
|
+
});
|
128
|
+
workers.set(worker.id, worker);
|
129
|
+
worker.on("exit", () => {
|
130
|
+
if (isShuttingDown) {
|
131
|
+
return;
|
132
|
+
}
|
133
|
+
workers.delete(worker.id);
|
134
|
+
openTelemetryCollector.warn(
|
135
|
+
`worker ${worker.process.pid} died; restarting in 2s`
|
136
|
+
);
|
137
|
+
setTimeout(() => {
|
138
|
+
if (!isShuttingDown) {
|
139
|
+
startWorker2(i);
|
140
|
+
}
|
141
|
+
}, 2e3);
|
142
|
+
});
|
143
|
+
worker.on(
|
144
|
+
"message",
|
145
|
+
(message) => {
|
146
|
+
if (message && message.type === "worker-ready") {
|
147
|
+
openTelemetryCollector.info(
|
148
|
+
`Worker ${message.index || i} (PID: ${worker.process.pid}) ready`
|
149
|
+
);
|
150
|
+
}
|
151
|
+
}
|
152
|
+
);
|
153
|
+
};
|
154
|
+
var startWorker = startWorker2;
|
155
|
+
const workers = /* @__PURE__ */ new Map();
|
156
|
+
let isShuttingDown = false;
|
157
|
+
openTelemetryCollector.info(
|
158
|
+
`[primary ${process.pid}] starting ${WORKERS} workers on ${HOST}:${PORT}. Using SO_REUSEPORT kernel level routing.`
|
159
|
+
);
|
160
|
+
for (let i = 0; i < WORKERS; i++) {
|
161
|
+
startWorker2(i);
|
162
|
+
}
|
163
|
+
process.on("SIGINT", () => {
|
164
|
+
if (isShuttingDown) return;
|
165
|
+
isShuttingDown = true;
|
166
|
+
openTelemetryCollector.info(
|
167
|
+
"Received SIGINT, shutting down gracefully..."
|
168
|
+
);
|
169
|
+
workers.forEach((worker) => {
|
170
|
+
worker.send({ type: "shutdown" });
|
171
|
+
worker.kill("SIGTERM");
|
172
|
+
});
|
173
|
+
setTimeout(() => {
|
174
|
+
workers.forEach((worker) => worker.kill("SIGKILL"));
|
175
|
+
process.exit(0);
|
176
|
+
}, 5e3);
|
177
|
+
});
|
178
|
+
} else {
|
179
|
+
const WORKER_PORT = parseInt(process.env.PORT || "0");
|
180
|
+
const IDX = process.env.WORKER_INDEX ?? import_node_cluster.default.worker?.id ?? "0";
|
181
|
+
if (!WORKER_PORT) {
|
182
|
+
openTelemetryCollector.error("Worker port not provided");
|
183
|
+
process.exit(1);
|
184
|
+
}
|
185
|
+
let listenSocket = null;
|
186
|
+
try {
|
187
|
+
app.listen(WORKER_PORT, (socket) => {
|
188
|
+
listenSocket = socket;
|
189
|
+
if (process.send) {
|
190
|
+
process.send({ type: "worker-ready", port: WORKER_PORT, index: IDX });
|
191
|
+
}
|
192
|
+
openTelemetryCollector.info(
|
193
|
+
`[worker ${process.pid}] listening on shared ${HOST}:${WORKER_PORT}`
|
194
|
+
);
|
195
|
+
});
|
196
|
+
} catch (error) {
|
197
|
+
openTelemetryCollector.error("Failed to start worker:", error);
|
198
|
+
process.exit(1);
|
199
|
+
}
|
200
|
+
process.on("message", (msg) => {
|
201
|
+
if (msg?.type === "shutdown") {
|
202
|
+
openTelemetryCollector.info(
|
203
|
+
`[worker ${process.pid}] received shutdown signal from primary`
|
204
|
+
);
|
205
|
+
if (listenSocket) {
|
206
|
+
uWS.us_listen_socket_close(listenSocket);
|
207
|
+
}
|
208
|
+
process.exit(0);
|
209
|
+
}
|
210
|
+
});
|
211
|
+
process.on("SIGINT", () => {
|
212
|
+
openTelemetryCollector.info(
|
213
|
+
`[worker ${process.pid}] shutting down gracefully...`
|
214
|
+
);
|
215
|
+
if (listenSocket) {
|
216
|
+
uWS.us_listen_socket_close(listenSocket);
|
217
|
+
}
|
218
|
+
process.exit(0);
|
219
|
+
});
|
220
|
+
setInterval(() => {
|
221
|
+
const memUsage = process.memoryUsage();
|
222
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
223
|
+
if (heapUsedMB > 100) {
|
224
|
+
openTelemetryCollector.warn(
|
225
|
+
`High memory usage on worker ${process.pid}: ${heapUsedMB} MB`
|
226
|
+
);
|
227
|
+
}
|
228
|
+
}, 3e4);
|
229
|
+
process.on("uncaughtException", (err) => {
|
230
|
+
openTelemetryCollector.error(
|
231
|
+
`Uncaught exception on worker ${process.pid}:`,
|
232
|
+
err
|
233
|
+
);
|
234
|
+
process.exit(1);
|
235
|
+
});
|
236
|
+
process.on("unhandledRejection", (reason) => {
|
237
|
+
openTelemetryCollector.error(
|
238
|
+
`Unhandled rejection on worker ${process.pid}:`,
|
239
|
+
reason
|
240
|
+
);
|
241
|
+
process.exit(1);
|
242
|
+
});
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
106
246
|
// src/middleware/contentParse.middleware.ts
|
107
247
|
var import_common = require("@forklaunch/common");
|
108
248
|
var import_http11 = require("@forklaunch/core/http");
|
@@ -283,6 +423,9 @@ function swagger(path, document, opts, options2, customCss, customfavIcon, swagg
|
|
283
423
|
// src/hyperExpressApplication.ts
|
284
424
|
var Application = class extends import_http13.ForklaunchExpressLikeApplication {
|
285
425
|
docsConfiguration;
|
426
|
+
mcpConfiguration;
|
427
|
+
openapiConfiguration;
|
428
|
+
hostingConfiguration;
|
286
429
|
/**
|
287
430
|
* Creates an instance of the Application class.
|
288
431
|
*
|
@@ -302,7 +445,11 @@ var Application = class extends import_http13.ForklaunchExpressLikeApplication {
|
|
302
445
|
constructor(schemaValidator, openTelemetryCollector, configurationOptions) {
|
303
446
|
super(
|
304
447
|
schemaValidator,
|
305
|
-
new import_hyper_express_fork.Server(
|
448
|
+
new import_hyper_express_fork.Server({
|
449
|
+
key_file_name: configurationOptions?.hosting?.ssl?.keyFile,
|
450
|
+
cert_file_name: configurationOptions?.hosting?.ssl?.certFile,
|
451
|
+
...configurationOptions?.server
|
452
|
+
}),
|
306
453
|
[
|
307
454
|
contentParse(configurationOptions),
|
308
455
|
enrichResponseTransmission
|
@@ -310,7 +457,10 @@ var Application = class extends import_http13.ForklaunchExpressLikeApplication {
|
|
310
457
|
openTelemetryCollector,
|
311
458
|
configurationOptions
|
312
459
|
);
|
460
|
+
this.hostingConfiguration = configurationOptions?.hosting;
|
313
461
|
this.docsConfiguration = configurationOptions?.docs;
|
462
|
+
this.mcpConfiguration = configurationOptions?.mcp;
|
463
|
+
this.openapiConfiguration = configurationOptions?.openapi;
|
314
464
|
}
|
315
465
|
async listen(arg0, arg1, arg2) {
|
316
466
|
if (typeof arg0 === "number") {
|
@@ -330,66 +480,162 @@ Correlation id: ${(0, import_http13.isForklaunchRequest)(req) ? req.context.corr
|
|
330
480
|
[import_http13.ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode ?? 500
|
331
481
|
});
|
332
482
|
});
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
},
|
355
|
-
...Object.entries(openApi).map(([version, spec]) => ({
|
356
|
-
content: spec,
|
357
|
-
title: `API Reference - ${version}`
|
358
|
-
})),
|
359
|
-
...this.docsConfiguration?.sources ?? []
|
360
|
-
]
|
361
|
-
})
|
483
|
+
if (this.schemaValidator instanceof import_zod.ZodSchemaValidator && this.mcpConfiguration !== false) {
|
484
|
+
const {
|
485
|
+
port: mcpPort,
|
486
|
+
options: options2,
|
487
|
+
path: mcpPath,
|
488
|
+
version,
|
489
|
+
additionalTools,
|
490
|
+
contentTypeMapping
|
491
|
+
} = this.mcpConfiguration ?? {};
|
492
|
+
const zodSchemaValidator = this.schemaValidator;
|
493
|
+
const finalMcpPort = mcpPort ?? port + 2e3;
|
494
|
+
const mcpServer = (0, import_http13.generateMcpServer)(
|
495
|
+
zodSchemaValidator,
|
496
|
+
protocol,
|
497
|
+
host,
|
498
|
+
finalMcpPort,
|
499
|
+
version ?? "1.0.0",
|
500
|
+
this,
|
501
|
+
this.mcpConfiguration,
|
502
|
+
options2,
|
503
|
+
contentTypeMapping
|
362
504
|
);
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
505
|
+
if (additionalTools) {
|
506
|
+
additionalTools(mcpServer);
|
507
|
+
}
|
508
|
+
if (this.hostingConfiguration?.workerCount && this.hostingConfiguration.workerCount > 1) {
|
509
|
+
(0, import_http13.isPortBound)(finalMcpPort, host).then((isBound) => {
|
510
|
+
if (!isBound) {
|
511
|
+
mcpServer.start({
|
512
|
+
httpStream: {
|
513
|
+
host,
|
514
|
+
endpoint: mcpPath ?? "/mcp",
|
515
|
+
port: finalMcpPort
|
516
|
+
},
|
517
|
+
transportType: "httpStream"
|
518
|
+
});
|
519
|
+
}
|
520
|
+
});
|
521
|
+
} else {
|
522
|
+
mcpServer.start({
|
523
|
+
httpStream: {
|
524
|
+
host,
|
525
|
+
endpoint: mcpPath ?? "/mcp",
|
526
|
+
port: finalMcpPort
|
527
|
+
},
|
528
|
+
transportType: "httpStream"
|
383
529
|
});
|
384
530
|
}
|
385
|
-
|
386
|
-
this.
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
531
|
+
}
|
532
|
+
if (this.openapiConfiguration !== false) {
|
533
|
+
const openApiServerUrls = (0, import_common3.getEnvVar)("DOCS_SERVER_URLS")?.split(",") ?? [
|
534
|
+
`${protocol}://${host}:${port}`
|
535
|
+
];
|
536
|
+
const openApiServerDescriptions = (0, import_common3.getEnvVar)(
|
537
|
+
"DOCS_SERVER_DESCRIPTIONS"
|
538
|
+
)?.split(",") ?? ["Main Server"];
|
539
|
+
const openApi = (0, import_http13.generateOpenApiSpecs)(
|
540
|
+
this.schemaValidator,
|
541
|
+
openApiServerUrls,
|
542
|
+
openApiServerDescriptions,
|
543
|
+
this,
|
544
|
+
this.openapiConfiguration
|
545
|
+
);
|
546
|
+
if (this.docsConfiguration == null || this.docsConfiguration.type === "scalar") {
|
547
|
+
this.internal.use(
|
548
|
+
`/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`,
|
549
|
+
(0, import_express_api_reference.apiReference)({
|
550
|
+
...this.docsConfiguration,
|
551
|
+
sources: [
|
552
|
+
{
|
553
|
+
content: openApi[import_http13.OPENAPI_DEFAULT_VERSION],
|
554
|
+
title: "API Reference"
|
555
|
+
},
|
556
|
+
...Object.entries(openApi).map(([version, spec]) => ({
|
557
|
+
content: typeof this.openapiConfiguration === "boolean" ? spec : this.openapiConfiguration?.discreteVersions === false ? {
|
558
|
+
...openApi[import_http13.OPENAPI_DEFAULT_VERSION],
|
559
|
+
...spec
|
560
|
+
} : spec,
|
561
|
+
title: `API Reference - ${version}`
|
562
|
+
})),
|
563
|
+
...this.docsConfiguration?.sources ?? []
|
564
|
+
]
|
565
|
+
})
|
566
|
+
);
|
567
|
+
} else if (this.docsConfiguration?.type === "swagger") {
|
568
|
+
const swaggerPath = `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`;
|
569
|
+
Object.entries(openApi).forEach(([version, spec]) => {
|
570
|
+
const versionPath = encodeURIComponent(`${swaggerPath}/${version}`);
|
571
|
+
this.internal.use(versionPath, swaggerRedirect(versionPath));
|
572
|
+
this.internal.get(`${versionPath}/*`, swagger(versionPath, spec));
|
573
|
+
});
|
391
574
|
}
|
392
|
-
|
575
|
+
this.internal.get(
|
576
|
+
`/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi`,
|
577
|
+
(_, res) => {
|
578
|
+
res.type("application/json");
|
579
|
+
res.json({
|
580
|
+
latest: openApi[import_http13.OPENAPI_DEFAULT_VERSION],
|
581
|
+
...Object.fromEntries(
|
582
|
+
Object.entries(openApi).map(([version, spec]) => [
|
583
|
+
`v${version}`,
|
584
|
+
typeof this.openapiConfiguration === "boolean" ? spec : this.openapiConfiguration?.discreteVersions === false ? {
|
585
|
+
...openApi[import_http13.OPENAPI_DEFAULT_VERSION],
|
586
|
+
...spec
|
587
|
+
} : spec
|
588
|
+
])
|
589
|
+
)
|
590
|
+
});
|
591
|
+
}
|
592
|
+
);
|
593
|
+
this.internal.get(
|
594
|
+
`/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi/:id`,
|
595
|
+
async (req, res) => {
|
596
|
+
res.type("application/json");
|
597
|
+
if (req.params.id === "latest") {
|
598
|
+
res.json(openApi[import_http13.OPENAPI_DEFAULT_VERSION]);
|
599
|
+
} else {
|
600
|
+
if (openApi[req.params.id] == null) {
|
601
|
+
res.status(404).send("Not Found");
|
602
|
+
return;
|
603
|
+
}
|
604
|
+
res.json(
|
605
|
+
typeof this.openapiConfiguration === "boolean" ? openApi[req.params.id] : this.openapiConfiguration?.discreteVersions === false ? {
|
606
|
+
...openApi[import_http13.OPENAPI_DEFAULT_VERSION],
|
607
|
+
...openApi[req.params.id]
|
608
|
+
} : openApi[req.params.id]
|
609
|
+
);
|
610
|
+
}
|
611
|
+
}
|
612
|
+
);
|
613
|
+
this.internal.get(
|
614
|
+
`/api/${process.env.VERSION ?? "v1"}/openapi-hash`,
|
615
|
+
async (_, res) => {
|
616
|
+
const hash = await import_crypto.default.createHash("sha256").update((0, import_common3.safeStringify)(openApi)).digest("hex");
|
617
|
+
res.send(hash);
|
618
|
+
}
|
619
|
+
);
|
620
|
+
}
|
621
|
+
this.internal.get("/health", (_, res) => {
|
622
|
+
res.send("OK");
|
623
|
+
});
|
624
|
+
const { workerCount, ssl, routingStrategy } = this.hostingConfiguration ?? {};
|
625
|
+
if (workerCount != null && workerCount > 1) {
|
626
|
+
this.openTelemetryCollector.warn(
|
627
|
+
"Clustering with hyper-express will default to kernel-level routing."
|
628
|
+
);
|
629
|
+
startHyperExpressCluster({
|
630
|
+
expressApp: this.internal,
|
631
|
+
openTelemetryCollector: this.openTelemetryCollector,
|
632
|
+
port,
|
633
|
+
host,
|
634
|
+
workerCount,
|
635
|
+
routingStrategy,
|
636
|
+
ssl
|
637
|
+
});
|
638
|
+
}
|
393
639
|
if (arg1 && typeof arg1 === "string") {
|
394
640
|
return this.internal.listen(port, arg1, arg2);
|
395
641
|
} else if (arg1 && typeof arg1 === "function") {
|
@@ -426,7 +672,8 @@ var Router = class _Router extends import_http14.ForklaunchExpressLikeRouter {
|
|
426
672
|
contentParse(options2),
|
427
673
|
enrichResponseTransmission
|
428
674
|
],
|
429
|
-
openTelemetryCollector
|
675
|
+
openTelemetryCollector,
|
676
|
+
options2
|
430
677
|
);
|
431
678
|
this.basePath = basePath;
|
432
679
|
this.configOptions = options2;
|
@@ -462,7 +709,11 @@ var Router = class _Router extends import_http14.ForklaunchExpressLikeRouter {
|
|
462
709
|
|
463
710
|
// index.ts
|
464
711
|
function forklaunchExpress(schemaValidator, openTelemetryCollector, options2) {
|
465
|
-
return new Application(
|
712
|
+
return new Application(
|
713
|
+
schemaValidator,
|
714
|
+
openTelemetryCollector,
|
715
|
+
options2
|
716
|
+
);
|
466
717
|
}
|
467
718
|
function forklaunchRouter(basePath, schemaValidator, openTelemetryCollector, options2) {
|
468
719
|
const router = new Router(
|