@forklaunch/hyper-express 0.7.10 → 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.mjs CHANGED
@@ -83,16 +83,158 @@ import { getEnvVar, safeStringify as safeStringify2 } from "@forklaunch/common";
83
83
  import {
84
84
  ATTR_HTTP_RESPONSE_STATUS_CODE,
85
85
  ForklaunchExpressLikeApplication,
86
+ generateMcpServer,
86
87
  generateOpenApiSpecs,
87
88
  isForklaunchRequest,
89
+ isPortBound,
88
90
  OPENAPI_DEFAULT_VERSION
89
91
  } from "@forklaunch/core/http";
90
92
  import {
91
93
  Server
92
94
  } from "@forklaunch/hyper-express-fork";
95
+ import { ZodSchemaValidator } from "@forklaunch/validator/zod";
93
96
  import { apiReference } from "@scalar/express-api-reference";
94
97
  import crypto from "crypto";
95
98
 
99
+ // src/cluster/hyperExpress.cluster.ts
100
+ import cluster from "cluster";
101
+ import * as os from "os";
102
+ import * as uWS from "uWebSockets.js";
103
+ function startHyperExpressCluster(config) {
104
+ const PORT = config.port;
105
+ const HOST = config.host;
106
+ const WORKERS = config.workerCount;
107
+ const app = config.expressApp;
108
+ const openTelemetryCollector = config.openTelemetryCollector;
109
+ if (WORKERS > os.cpus().length) {
110
+ throw new Error("Worker count cannot be greater than the number of CPUs");
111
+ }
112
+ if (cluster.isPrimary) {
113
+ let startWorker2 = function(i) {
114
+ if (isShuttingDown) return;
115
+ const worker = cluster.fork({
116
+ WORKER_INDEX: i.toString(),
117
+ PORT: PORT.toString(),
118
+ HOST
119
+ });
120
+ workers.set(worker.id, worker);
121
+ worker.on("exit", () => {
122
+ if (isShuttingDown) {
123
+ return;
124
+ }
125
+ workers.delete(worker.id);
126
+ openTelemetryCollector.warn(
127
+ `worker ${worker.process.pid} died; restarting in 2s`
128
+ );
129
+ setTimeout(() => {
130
+ if (!isShuttingDown) {
131
+ startWorker2(i);
132
+ }
133
+ }, 2e3);
134
+ });
135
+ worker.on(
136
+ "message",
137
+ (message) => {
138
+ if (message && message.type === "worker-ready") {
139
+ openTelemetryCollector.info(
140
+ `Worker ${message.index || i} (PID: ${worker.process.pid}) ready`
141
+ );
142
+ }
143
+ }
144
+ );
145
+ };
146
+ var startWorker = startWorker2;
147
+ const workers = /* @__PURE__ */ new Map();
148
+ let isShuttingDown = false;
149
+ openTelemetryCollector.info(
150
+ `[primary ${process.pid}] starting ${WORKERS} workers on ${HOST}:${PORT}. Using SO_REUSEPORT kernel level routing.`
151
+ );
152
+ for (let i = 0; i < WORKERS; i++) {
153
+ startWorker2(i);
154
+ }
155
+ process.on("SIGINT", () => {
156
+ if (isShuttingDown) return;
157
+ isShuttingDown = true;
158
+ openTelemetryCollector.info(
159
+ "Received SIGINT, shutting down gracefully..."
160
+ );
161
+ workers.forEach((worker) => {
162
+ worker.send({ type: "shutdown" });
163
+ worker.kill("SIGTERM");
164
+ });
165
+ setTimeout(() => {
166
+ workers.forEach((worker) => worker.kill("SIGKILL"));
167
+ process.exit(0);
168
+ }, 5e3);
169
+ });
170
+ } else {
171
+ const WORKER_PORT = parseInt(process.env.PORT || "0");
172
+ const IDX = process.env.WORKER_INDEX ?? cluster.worker?.id ?? "0";
173
+ if (!WORKER_PORT) {
174
+ openTelemetryCollector.error("Worker port not provided");
175
+ process.exit(1);
176
+ }
177
+ let listenSocket = null;
178
+ try {
179
+ app.listen(WORKER_PORT, (socket) => {
180
+ listenSocket = socket;
181
+ if (process.send) {
182
+ process.send({ type: "worker-ready", port: WORKER_PORT, index: IDX });
183
+ }
184
+ openTelemetryCollector.info(
185
+ `[worker ${process.pid}] listening on shared ${HOST}:${WORKER_PORT}`
186
+ );
187
+ });
188
+ } catch (error) {
189
+ openTelemetryCollector.error("Failed to start worker:", error);
190
+ process.exit(1);
191
+ }
192
+ process.on("message", (msg) => {
193
+ if (msg?.type === "shutdown") {
194
+ openTelemetryCollector.info(
195
+ `[worker ${process.pid}] received shutdown signal from primary`
196
+ );
197
+ if (listenSocket) {
198
+ uWS.us_listen_socket_close(listenSocket);
199
+ }
200
+ process.exit(0);
201
+ }
202
+ });
203
+ process.on("SIGINT", () => {
204
+ openTelemetryCollector.info(
205
+ `[worker ${process.pid}] shutting down gracefully...`
206
+ );
207
+ if (listenSocket) {
208
+ uWS.us_listen_socket_close(listenSocket);
209
+ }
210
+ process.exit(0);
211
+ });
212
+ setInterval(() => {
213
+ const memUsage = process.memoryUsage();
214
+ const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
215
+ if (heapUsedMB > 100) {
216
+ openTelemetryCollector.warn(
217
+ `High memory usage on worker ${process.pid}: ${heapUsedMB} MB`
218
+ );
219
+ }
220
+ }, 3e4);
221
+ process.on("uncaughtException", (err) => {
222
+ openTelemetryCollector.error(
223
+ `Uncaught exception on worker ${process.pid}:`,
224
+ err
225
+ );
226
+ process.exit(1);
227
+ });
228
+ process.on("unhandledRejection", (reason) => {
229
+ openTelemetryCollector.error(
230
+ `Unhandled rejection on worker ${process.pid}:`,
231
+ reason
232
+ );
233
+ process.exit(1);
234
+ });
235
+ }
236
+ }
237
+
96
238
  // src/middleware/contentParse.middleware.ts
97
239
  import { isNever } from "@forklaunch/common";
98
240
  import { discriminateBody } from "@forklaunch/core/http";
@@ -275,6 +417,9 @@ function swagger(path, document, opts, options2, customCss, customfavIcon, swagg
275
417
  // src/hyperExpressApplication.ts
276
418
  var Application = class extends ForklaunchExpressLikeApplication {
277
419
  docsConfiguration;
420
+ mcpConfiguration;
421
+ openapiConfiguration;
422
+ hostingConfiguration;
278
423
  /**
279
424
  * Creates an instance of the Application class.
280
425
  *
@@ -294,7 +439,11 @@ var Application = class extends ForklaunchExpressLikeApplication {
294
439
  constructor(schemaValidator, openTelemetryCollector, configurationOptions) {
295
440
  super(
296
441
  schemaValidator,
297
- new Server(configurationOptions?.server),
442
+ new Server({
443
+ key_file_name: configurationOptions?.hosting?.ssl?.keyFile,
444
+ cert_file_name: configurationOptions?.hosting?.ssl?.certFile,
445
+ ...configurationOptions?.server
446
+ }),
298
447
  [
299
448
  contentParse(configurationOptions),
300
449
  enrichResponseTransmission
@@ -302,7 +451,10 @@ var Application = class extends ForklaunchExpressLikeApplication {
302
451
  openTelemetryCollector,
303
452
  configurationOptions
304
453
  );
454
+ this.hostingConfiguration = configurationOptions?.hosting;
305
455
  this.docsConfiguration = configurationOptions?.docs;
456
+ this.mcpConfiguration = configurationOptions?.mcp;
457
+ this.openapiConfiguration = configurationOptions?.openapi;
306
458
  }
307
459
  async listen(arg0, arg1, arg2) {
308
460
  if (typeof arg0 === "number") {
@@ -322,66 +474,162 @@ Correlation id: ${isForklaunchRequest(req) ? req.context.correlationId : "No cor
322
474
  [ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode ?? 500
323
475
  });
324
476
  });
325
- const openApiServerUrls = getEnvVar("DOCS_SERVER_URLS")?.split(",") ?? [
326
- `${protocol}://${host}:${port}`
327
- ];
328
- const openApiServerDescriptions = getEnvVar(
329
- "DOCS_SERVER_DESCRIPTIONS"
330
- )?.split(",") ?? ["Main Server"];
331
- const openApi = generateOpenApiSpecs(
332
- this.schemaValidator,
333
- openApiServerUrls,
334
- openApiServerDescriptions,
335
- this
336
- );
337
- if (this.docsConfiguration == null || this.docsConfiguration.type === "scalar") {
338
- this.internal.use(
339
- `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`,
340
- apiReference({
341
- ...this.docsConfiguration,
342
- sources: [
343
- {
344
- content: openApi[OPENAPI_DEFAULT_VERSION],
345
- title: "API Reference"
346
- },
347
- ...Object.entries(openApi).map(([version, spec]) => ({
348
- content: spec,
349
- title: `API Reference - ${version}`
350
- })),
351
- ...this.docsConfiguration?.sources ?? []
352
- ]
353
- })
477
+ if (this.schemaValidator instanceof ZodSchemaValidator && this.mcpConfiguration !== false) {
478
+ const {
479
+ port: mcpPort,
480
+ options: options2,
481
+ path: mcpPath,
482
+ version,
483
+ additionalTools,
484
+ contentTypeMapping
485
+ } = this.mcpConfiguration ?? {};
486
+ const zodSchemaValidator = this.schemaValidator;
487
+ const finalMcpPort = mcpPort ?? port + 2e3;
488
+ const mcpServer = generateMcpServer(
489
+ zodSchemaValidator,
490
+ protocol,
491
+ host,
492
+ finalMcpPort,
493
+ version ?? "1.0.0",
494
+ this,
495
+ this.mcpConfiguration,
496
+ options2,
497
+ contentTypeMapping
354
498
  );
355
- } else if (this.docsConfiguration?.type === "swagger") {
356
- const swaggerPath = `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`;
357
- Object.entries(openApi).forEach(([version, spec]) => {
358
- const versionPath = encodeURIComponent(`${swaggerPath}/${version}`);
359
- this.internal.use(versionPath, swaggerRedirect(versionPath));
360
- this.internal.get(`${versionPath}/*`, swagger(versionPath, spec));
361
- });
362
- }
363
- this.internal.get(
364
- `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi`,
365
- (_, res) => {
366
- res.type("application/json");
367
- res.json({
368
- latest: openApi[OPENAPI_DEFAULT_VERSION],
369
- ...Object.fromEntries(
370
- Object.entries(openApi).map(([version, spec]) => [
371
- `v${version}`,
372
- spec
373
- ])
374
- )
499
+ if (additionalTools) {
500
+ additionalTools(mcpServer);
501
+ }
502
+ if (this.hostingConfiguration?.workerCount && this.hostingConfiguration.workerCount > 1) {
503
+ isPortBound(finalMcpPort, host).then((isBound) => {
504
+ if (!isBound) {
505
+ mcpServer.start({
506
+ httpStream: {
507
+ host,
508
+ endpoint: mcpPath ?? "/mcp",
509
+ port: finalMcpPort
510
+ },
511
+ transportType: "httpStream"
512
+ });
513
+ }
514
+ });
515
+ } else {
516
+ mcpServer.start({
517
+ httpStream: {
518
+ host,
519
+ endpoint: mcpPath ?? "/mcp",
520
+ port: finalMcpPort
521
+ },
522
+ transportType: "httpStream"
375
523
  });
376
524
  }
377
- );
378
- this.internal.get(
379
- `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi-hash`,
380
- async (_, res) => {
381
- const hash = await crypto.createHash("sha256").update(safeStringify2(openApi)).digest("hex");
382
- res.send(hash);
525
+ }
526
+ if (this.openapiConfiguration !== false) {
527
+ const openApiServerUrls = getEnvVar("DOCS_SERVER_URLS")?.split(",") ?? [
528
+ `${protocol}://${host}:${port}`
529
+ ];
530
+ const openApiServerDescriptions = getEnvVar(
531
+ "DOCS_SERVER_DESCRIPTIONS"
532
+ )?.split(",") ?? ["Main Server"];
533
+ const openApi = generateOpenApiSpecs(
534
+ this.schemaValidator,
535
+ openApiServerUrls,
536
+ openApiServerDescriptions,
537
+ this,
538
+ this.openapiConfiguration
539
+ );
540
+ if (this.docsConfiguration == null || this.docsConfiguration.type === "scalar") {
541
+ this.internal.use(
542
+ `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`,
543
+ apiReference({
544
+ ...this.docsConfiguration,
545
+ sources: [
546
+ {
547
+ content: openApi[OPENAPI_DEFAULT_VERSION],
548
+ title: "API Reference"
549
+ },
550
+ ...Object.entries(openApi).map(([version, spec]) => ({
551
+ content: typeof this.openapiConfiguration === "boolean" ? spec : this.openapiConfiguration?.discreteVersions === false ? {
552
+ ...openApi[OPENAPI_DEFAULT_VERSION],
553
+ ...spec
554
+ } : spec,
555
+ title: `API Reference - ${version}`
556
+ })),
557
+ ...this.docsConfiguration?.sources ?? []
558
+ ]
559
+ })
560
+ );
561
+ } else if (this.docsConfiguration?.type === "swagger") {
562
+ const swaggerPath = `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}${process.env.DOCS_PATH ?? "/docs"}`;
563
+ Object.entries(openApi).forEach(([version, spec]) => {
564
+ const versionPath = encodeURIComponent(`${swaggerPath}/${version}`);
565
+ this.internal.use(versionPath, swaggerRedirect(versionPath));
566
+ this.internal.get(`${versionPath}/*`, swagger(versionPath, spec));
567
+ });
383
568
  }
384
- );
569
+ this.internal.get(
570
+ `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi`,
571
+ (_, res) => {
572
+ res.type("application/json");
573
+ res.json({
574
+ latest: openApi[OPENAPI_DEFAULT_VERSION],
575
+ ...Object.fromEntries(
576
+ Object.entries(openApi).map(([version, spec]) => [
577
+ `v${version}`,
578
+ typeof this.openapiConfiguration === "boolean" ? spec : this.openapiConfiguration?.discreteVersions === false ? {
579
+ ...openApi[OPENAPI_DEFAULT_VERSION],
580
+ ...spec
581
+ } : spec
582
+ ])
583
+ )
584
+ });
585
+ }
586
+ );
587
+ this.internal.get(
588
+ `/api${process.env.VERSION ? `/${process.env.VERSION}` : ""}/openapi/:id`,
589
+ async (req, res) => {
590
+ res.type("application/json");
591
+ if (req.params.id === "latest") {
592
+ res.json(openApi[OPENAPI_DEFAULT_VERSION]);
593
+ } else {
594
+ if (openApi[req.params.id] == null) {
595
+ res.status(404).send("Not Found");
596
+ return;
597
+ }
598
+ res.json(
599
+ typeof this.openapiConfiguration === "boolean" ? openApi[req.params.id] : this.openapiConfiguration?.discreteVersions === false ? {
600
+ ...openApi[OPENAPI_DEFAULT_VERSION],
601
+ ...openApi[req.params.id]
602
+ } : openApi[req.params.id]
603
+ );
604
+ }
605
+ }
606
+ );
607
+ this.internal.get(
608
+ `/api/${process.env.VERSION ?? "v1"}/openapi-hash`,
609
+ async (_, res) => {
610
+ const hash = await crypto.createHash("sha256").update(safeStringify2(openApi)).digest("hex");
611
+ res.send(hash);
612
+ }
613
+ );
614
+ }
615
+ this.internal.get("/health", (_, res) => {
616
+ res.send("OK");
617
+ });
618
+ const { workerCount, ssl, routingStrategy } = this.hostingConfiguration ?? {};
619
+ if (workerCount != null && workerCount > 1) {
620
+ this.openTelemetryCollector.warn(
621
+ "Clustering with hyper-express will default to kernel-level routing."
622
+ );
623
+ startHyperExpressCluster({
624
+ expressApp: this.internal,
625
+ openTelemetryCollector: this.openTelemetryCollector,
626
+ port,
627
+ host,
628
+ workerCount,
629
+ routingStrategy,
630
+ ssl
631
+ });
632
+ }
385
633
  if (arg1 && typeof arg1 === "string") {
386
634
  return this.internal.listen(port, arg1, arg2);
387
635
  } else if (arg1 && typeof arg1 === "function") {
@@ -422,7 +670,8 @@ var Router = class _Router extends ForklaunchExpressLikeRouter {
422
670
  contentParse(options2),
423
671
  enrichResponseTransmission
424
672
  ],
425
- openTelemetryCollector
673
+ openTelemetryCollector,
674
+ options2
426
675
  );
427
676
  this.basePath = basePath;
428
677
  this.configOptions = options2;
@@ -458,7 +707,11 @@ var Router = class _Router extends ForklaunchExpressLikeRouter {
458
707
 
459
708
  // index.ts
460
709
  function forklaunchExpress(schemaValidator, openTelemetryCollector, options2) {
461
- return new Application(schemaValidator, openTelemetryCollector, options2);
710
+ return new Application(
711
+ schemaValidator,
712
+ openTelemetryCollector,
713
+ options2
714
+ );
462
715
  }
463
716
  function forklaunchRouter(basePath, schemaValidator, openTelemetryCollector, options2) {
464
717
  const router = new Router(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forklaunch/hyper-express",
3
- "version": "0.7.10",
3
+ "version": "0.8.0",
4
4
  "description": "Forklaunch framework for hyper-express.",
5
5
  "homepage": "https://github.com/forklaunch/forklaunch-js#readme",
6
6
  "bugs": {
@@ -25,7 +25,7 @@
25
25
  "lib/**"
26
26
  ],
27
27
  "dependencies": {
28
- "@forklaunch/hyper-express-fork": "^6.17.34",
28
+ "@forklaunch/hyper-express-fork": "^6.17.35",
29
29
  "@scalar/express-api-reference": "^0.8.13",
30
30
  "cors": "^2.8.5",
31
31
  "live-directory": "^3.0.3",
@@ -33,30 +33,30 @@
33
33
  "qs": "^6.14.0",
34
34
  "swagger-ui-dist": "^5.27.1",
35
35
  "swagger-ui-express": "^5.0.1",
36
- "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.44.0",
37
- "@forklaunch/common": "0.4.6",
38
- "@forklaunch/validator": "0.8.0",
39
- "@forklaunch/core": "0.12.2"
36
+ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0",
37
+ "@forklaunch/common": "0.5.0",
38
+ "@forklaunch/validator": "0.9.0",
39
+ "@forklaunch/core": "0.13.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@eslint/js": "^9.32.0",
42
+ "@eslint/js": "^9.33.0",
43
43
  "@types/busboy": "^1.5.4",
44
44
  "@types/cors": "^2.8.19",
45
45
  "@types/jest": "^30.0.0",
46
46
  "@types/qs": "^6.14.0",
47
47
  "@types/swagger-ui-dist": "^3.30.6",
48
48
  "@types/swagger-ui-express": "^4.1.8",
49
- "@typescript/native-preview": "7.0.0-dev.20250802.1",
49
+ "@typescript/native-preview": "7.0.0-dev.20250820.1",
50
50
  "jest": "^30.0.5",
51
51
  "kill-port-process": "^3.2.1",
52
52
  "prettier": "^3.6.2",
53
- "ts-jest": "^29.4.0",
53
+ "ts-jest": "^29.4.1",
54
54
  "ts-node": "^10.9.2",
55
55
  "tsup": "^8.5.0",
56
- "tsx": "^4.20.3",
57
- "typedoc": "^0.28.9",
56
+ "tsx": "^4.20.4",
57
+ "typedoc": "^0.28.10",
58
58
  "typescript": "^5.9.2",
59
- "typescript-eslint": "^8.38.0"
59
+ "typescript-eslint": "^8.40.0"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsgo --noEmit && tsup index.ts --format cjs,esm --no-splitting --dts --tsconfig tsconfig.json --out-dir lib --clean",