@forklaunch/hyper-express 0.7.11 → 0.8.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/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(configurationOptions?.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
- const openApiServerUrls = (0, import_common3.getEnvVar)("DOCS_SERVER_URLS")?.split(",") ?? [
334
- `${protocol}://${host}:${port}`
335
- ];
336
- const openApiServerDescriptions = (0, import_common3.getEnvVar)(
337
- "DOCS_SERVER_DESCRIPTIONS"
338
- )?.split(",") ?? ["Main Server"];
339
- const openApi = (0, import_http13.generateOpenApiSpecs)(
340
- this.schemaValidator,
341
- openApiServerUrls,
342
- openApiServerDescriptions,
343
- this
344
- );
345
- if (this.docsConfiguration == null || this.docsConfiguration.type === "scalar") {
346
- this.internal.use(
347
- `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`,
348
- (0, import_express_api_reference.apiReference)({
349
- ...this.docsConfiguration,
350
- sources: [
351
- {
352
- content: openApi[import_http13.OPENAPI_DEFAULT_VERSION],
353
- title: "API Reference"
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
- } else if (this.docsConfiguration?.type === "swagger") {
364
- const swaggerPath = `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`;
365
- Object.entries(openApi).forEach(([version, spec]) => {
366
- const versionPath = encodeURIComponent(`${swaggerPath}/${version}`);
367
- this.internal.use(versionPath, swaggerRedirect(versionPath));
368
- this.internal.get(`${versionPath}/*`, swagger(versionPath, spec));
369
- });
370
- }
371
- this.internal.get(
372
- `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi`,
373
- (_, res) => {
374
- res.type("application/json");
375
- res.json({
376
- latest: openApi[import_http13.OPENAPI_DEFAULT_VERSION],
377
- ...Object.fromEntries(
378
- Object.entries(openApi).map(([version, spec]) => [
379
- `v${version}`,
380
- spec
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.internal.get(
387
- `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi-hash`,
388
- async (_, res) => {
389
- const hash = await import_crypto.default.createHash("sha256").update((0, import_common3.safeStringify)(openApi)).digest("hex");
390
- res.send(hash);
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(schemaValidator, openTelemetryCollector, options2);
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(