mikroserve 1.0.0 → 1.1.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/MikroServe.js CHANGED
@@ -74,7 +74,8 @@ var RateLimiter = class {
74
74
  const now = Date.now();
75
75
  const key = ip || "unknown";
76
76
  const entry = this.requests.get(key);
77
- if (!entry || entry.resetTime < now) return Math.floor((now + this.windowMs) / 1e3);
77
+ if (!entry || entry.resetTime < now)
78
+ return Math.floor((now + this.windowMs) / 1e3);
78
79
  return Math.floor(entry.resetTime / 1e3);
79
80
  }
80
81
  cleanup() {
@@ -92,64 +93,70 @@ var Router = class {
92
93
  globalMiddlewares = [];
93
94
  pathPatterns = /* @__PURE__ */ new Map();
94
95
  /**
95
- * Add a global middleware
96
+ * @description Add a global middleware.
96
97
  */
97
98
  use(middleware) {
98
99
  this.globalMiddlewares.push(middleware);
99
100
  return this;
100
101
  }
101
102
  /**
102
- * Register a route with specified method
103
- */
104
- register(method, path, handler, middlewares = []) {
105
- this.routes.push({ method, path, handler, middlewares });
106
- this.pathPatterns.set(path, this.createPathPattern(path));
107
- return this;
108
- }
109
- /**
110
- * Register a GET route
103
+ * @description Register a GET route.
111
104
  */
112
105
  get(path, ...handlers) {
113
106
  const handler = handlers.pop();
114
107
  return this.register("GET", path, handler, handlers);
115
108
  }
116
109
  /**
117
- * Register a POST route
110
+ * @description Register a POST route.
118
111
  */
119
112
  post(path, ...handlers) {
120
113
  const handler = handlers.pop();
121
114
  return this.register("POST", path, handler, handlers);
122
115
  }
123
116
  /**
124
- * Register a PUT route
117
+ * @description Register a PUT route.
125
118
  */
126
119
  put(path, ...handlers) {
127
120
  const handler = handlers.pop();
128
121
  return this.register("PUT", path, handler, handlers);
129
122
  }
130
123
  /**
131
- * Register a DELETE route
124
+ * @description Register a DELETE route.
132
125
  */
133
126
  delete(path, ...handlers) {
134
127
  const handler = handlers.pop();
135
128
  return this.register("DELETE", path, handler, handlers);
136
129
  }
137
130
  /**
138
- * Register a PATCH route
131
+ * @description Register a PATCH route.
139
132
  */
140
133
  patch(path, ...handlers) {
141
134
  const handler = handlers.pop();
142
135
  return this.register("PATCH", path, handler, handlers);
143
136
  }
144
137
  /**
145
- * Register an OPTIONS route
138
+ * @description Register a route for any HTTP method.
139
+ */
140
+ any(path, ...handlers) {
141
+ const handler = handlers.pop();
142
+ const middlewares = handlers;
143
+ this.register("GET", path, handler, middlewares);
144
+ this.register("POST", path, handler, middlewares);
145
+ this.register("PUT", path, handler, middlewares);
146
+ this.register("DELETE", path, handler, middlewares);
147
+ this.register("PATCH", path, handler, middlewares);
148
+ this.register("OPTIONS", path, handler, middlewares);
149
+ return this;
150
+ }
151
+ /**
152
+ * @description Register an OPTIONS route.
146
153
  */
147
154
  options(path, ...handlers) {
148
155
  const handler = handlers.pop();
149
156
  return this.register("OPTIONS", path, handler, handlers);
150
157
  }
151
158
  /**
152
- * Match a request to a route
159
+ * @description Match a request to a route.
153
160
  */
154
161
  match(method, path) {
155
162
  for (const route of this.routes) {
@@ -167,22 +174,7 @@ var Router = class {
167
174
  return null;
168
175
  }
169
176
  /**
170
- * Create a regex pattern for path matching
171
- */
172
- createPathPattern(path) {
173
- const paramNames = [];
174
- const pattern = path.replace(/\/:[^/]+/g, (match) => {
175
- const paramName = match.slice(2);
176
- paramNames.push(paramName);
177
- return "/([^/]+)";
178
- }).replace(/\/$/, "/?");
179
- return {
180
- pattern: new RegExp(`^${pattern}$`),
181
- paramNames
182
- };
183
- }
184
- /**
185
- * Handle a request and find the matching route
177
+ * @description Handle a request and find the matching route.
186
178
  */
187
179
  async handle(req, res) {
188
180
  const method = req.method || "GET";
@@ -200,12 +192,11 @@ var Router = class {
200
192
  res,
201
193
  params,
202
194
  query,
203
- // @ts-ignore
195
+ // @ts-expect-error
204
196
  body: req.body || {},
205
197
  headers: req.headers,
206
198
  path,
207
199
  state: {},
208
- // Add the missing state property
209
200
  raw: () => res,
210
201
  binary: (content, contentType = "application/octet-stream", status = 200) => ({
211
202
  statusCode: status,
@@ -269,7 +260,6 @@ var Router = class {
269
260
  headers: { "Content-Type": "text/html" }
270
261
  }),
271
262
  form: (content) => ({
272
- // Make sure form method is included here
273
263
  statusCode: code,
274
264
  body: content,
275
265
  headers: { "Content-Type": "application/x-www-form-urlencoded" }
@@ -287,7 +277,34 @@ var Router = class {
287
277
  return this.executeMiddlewareChain(context, middlewares, route.handler);
288
278
  }
289
279
  /**
290
- * Execute middleware chain and final handler
280
+ * @description Register a route with specified method.
281
+ */
282
+ register(method, path, handler, middlewares = []) {
283
+ this.routes.push({ method, path, handler, middlewares });
284
+ this.pathPatterns.set(path, this.createPathPattern(path));
285
+ return this;
286
+ }
287
+ /**
288
+ * @description Create a regex pattern for path matching.
289
+ */
290
+ createPathPattern(path) {
291
+ const paramNames = [];
292
+ let pattern = path.replace(/\/:[^/]+/g, (match) => {
293
+ const paramName = match.slice(2);
294
+ paramNames.push(paramName);
295
+ return "/([^/]+)";
296
+ });
297
+ if (pattern.endsWith("/*")) {
298
+ pattern = `${pattern.slice(0, -2)}(?:/(.*))?`;
299
+ paramNames.push("wildcard");
300
+ } else pattern = pattern.replace(/\/$/, "/?");
301
+ return {
302
+ pattern: new RegExp(`^${pattern}$`),
303
+ paramNames
304
+ };
305
+ }
306
+ /**
307
+ * @description Execute middleware chain and final handler.
291
308
  */
292
309
  async executeMiddlewareChain(context, middlewares, finalHandler) {
293
310
  let currentIndex = 0;
@@ -316,6 +333,10 @@ var configDefaults = () => {
316
333
  sslKey: "",
317
334
  sslCa: "",
318
335
  debug: getTruthyValue(process.env.DEBUG) || false,
336
+ maxBodySize: 1024 * 1024,
337
+ // 1MB
338
+ requestTimeout: 3e4,
339
+ // 30 seconds
319
340
  rateLimit: {
320
341
  enabled: true,
321
342
  requestsPerMinute: 100
@@ -336,8 +357,18 @@ var baseConfig = (options) => ({
336
357
  options: [
337
358
  { flag: "--port", path: "port", defaultValue: defaults.port },
338
359
  { flag: "--host", path: "host", defaultValue: defaults.host },
339
- { flag: "--https", path: "useHttps", defaultValue: defaults.useHttps, isFlag: true },
340
- { flag: "--http2", path: "useHttp2", defaultValue: defaults.useHttp2, isFlag: true },
360
+ {
361
+ flag: "--https",
362
+ path: "useHttps",
363
+ defaultValue: defaults.useHttps,
364
+ isFlag: true
365
+ },
366
+ {
367
+ flag: "--http2",
368
+ path: "useHttp2",
369
+ defaultValue: defaults.useHttp2,
370
+ isFlag: true
371
+ },
341
372
  { flag: "--cert", path: "sslCert", defaultValue: defaults.sslCert },
342
373
  { flag: "--key", path: "sslKey", defaultValue: defaults.sslKey },
343
374
  { flag: "--ca", path: "sslCa", defaultValue: defaults.sslCa },
@@ -358,27 +389,148 @@ var baseConfig = (options) => ({
358
389
  defaultValue: defaults.allowedDomains,
359
390
  parser: import_mikroconf.parsers.array
360
391
  },
361
- { flag: "--debug", path: "debug", defaultValue: defaults.debug, isFlag: true }
392
+ {
393
+ flag: "--debug",
394
+ path: "debug",
395
+ defaultValue: defaults.debug,
396
+ isFlag: true
397
+ },
398
+ {
399
+ flag: "--max-body-size",
400
+ path: "maxBodySize",
401
+ defaultValue: defaults.maxBodySize
402
+ },
403
+ {
404
+ flag: "--request-timeout",
405
+ path: "requestTimeout",
406
+ defaultValue: defaults.requestTimeout
407
+ }
362
408
  ],
363
409
  config: options
364
410
  });
365
411
 
412
+ // src/utils/multipartParser.ts
413
+ function parseMultipartFormData(body, contentType) {
414
+ const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
415
+ if (!boundaryMatch) {
416
+ throw new Error("Invalid multipart/form-data: missing boundary");
417
+ }
418
+ const boundary = boundaryMatch[1] || boundaryMatch[2];
419
+ const boundaryBuffer = Buffer.from(`--${boundary}`);
420
+ const endBoundaryBuffer = Buffer.from(`--${boundary}--`);
421
+ const fields = {};
422
+ const files = {};
423
+ const parts = splitByBoundary(body, boundaryBuffer);
424
+ for (const part of parts) {
425
+ if (part.length === 0) continue;
426
+ if (part.equals(endBoundaryBuffer.subarray(boundaryBuffer.length)))
427
+ continue;
428
+ const parsed = parsePart(part);
429
+ if (!parsed) continue;
430
+ const { name, filename, contentType: partContentType, data } = parsed;
431
+ if (filename) {
432
+ const file = {
433
+ filename,
434
+ contentType: partContentType || "application/octet-stream",
435
+ data,
436
+ size: data.length
437
+ };
438
+ if (files[name]) {
439
+ if (Array.isArray(files[name])) {
440
+ files[name].push(file);
441
+ } else {
442
+ files[name] = [files[name], file];
443
+ }
444
+ } else {
445
+ files[name] = file;
446
+ }
447
+ } else {
448
+ const value = data.toString("utf8");
449
+ if (fields[name]) {
450
+ if (Array.isArray(fields[name])) {
451
+ fields[name].push(value);
452
+ } else {
453
+ fields[name] = [fields[name], value];
454
+ }
455
+ } else {
456
+ fields[name] = value;
457
+ }
458
+ }
459
+ }
460
+ return { fields, files };
461
+ }
462
+ function splitByBoundary(buffer, boundary) {
463
+ const parts = [];
464
+ let start = 0;
465
+ while (start < buffer.length) {
466
+ const index = buffer.indexOf(boundary, start);
467
+ if (index === -1) break;
468
+ if (start !== index) {
469
+ parts.push(buffer.subarray(start, index));
470
+ }
471
+ start = index + boundary.length;
472
+ if (start < buffer.length && buffer[start] === 13 && buffer[start + 1] === 10) {
473
+ start += 2;
474
+ }
475
+ }
476
+ return parts;
477
+ }
478
+ function parsePart(part) {
479
+ const doubleCRLF = Buffer.from("\r\n\r\n");
480
+ const headerEndIndex = part.indexOf(doubleCRLF);
481
+ if (headerEndIndex === -1) return null;
482
+ const headersBuffer = part.subarray(0, headerEndIndex);
483
+ const dataBuffer = part.subarray(headerEndIndex + 4);
484
+ const headers = headersBuffer.toString("utf8");
485
+ const headerLines = headers.split("\r\n");
486
+ let disposition = "";
487
+ let name = "";
488
+ let filename;
489
+ let contentType;
490
+ for (const line of headerLines) {
491
+ const lowerLine = line.toLowerCase();
492
+ if (lowerLine.startsWith("content-disposition:")) {
493
+ disposition = line.substring("content-disposition:".length).trim();
494
+ const nameMatch = disposition.match(/name="([^"]+)"/);
495
+ if (nameMatch) {
496
+ name = nameMatch[1];
497
+ }
498
+ const filenameMatch = disposition.match(/filename="([^"]+)"/);
499
+ if (filenameMatch) {
500
+ filename = filenameMatch[1];
501
+ }
502
+ } else if (lowerLine.startsWith("content-type:")) {
503
+ contentType = line.substring("content-type:".length).trim();
504
+ }
505
+ }
506
+ if (!name) return null;
507
+ let data = dataBuffer;
508
+ if (data.length >= 2 && data[data.length - 2] === 13 && data[data.length - 1] === 10) {
509
+ data = data.subarray(0, data.length - 2);
510
+ }
511
+ return { disposition, name, filename, contentType, data };
512
+ }
513
+
366
514
  // src/MikroServe.ts
367
515
  var MikroServe = class {
368
516
  config;
369
517
  rateLimiter;
370
518
  router;
519
+ shutdownHandlers = [];
371
520
  /**
372
521
  * @description Creates a new MikroServe instance.
373
522
  */
374
523
  constructor(options) {
375
- const config = new import_mikroconf2.MikroConf(baseConfig(options || {})).get();
524
+ const config = new import_mikroconf2.MikroConf(
525
+ baseConfig(options || {})
526
+ ).get();
376
527
  if (config.debug) console.log("Using configuration:", config);
377
528
  this.config = config;
378
529
  this.router = new Router();
379
530
  const requestsPerMinute = config.rateLimit.requestsPerMinute || configDefaults().rateLimit.requestsPerMinute;
380
531
  this.rateLimiter = new RateLimiter(requestsPerMinute, 60);
381
- if (config.rateLimit.enabled === true) this.use(this.rateLimitMiddleware.bind(this));
532
+ if (config.rateLimit.enabled === true)
533
+ this.use(this.rateLimitMiddleware.bind(this));
382
534
  }
383
535
  /**
384
536
  * @description Register a global middleware.
@@ -422,6 +574,13 @@ var MikroServe = class {
422
574
  this.router.patch(path, ...handlers);
423
575
  return this;
424
576
  }
577
+ /**
578
+ * @description Register a route that responds to any HTTP method.
579
+ */
580
+ any(path, ...handlers) {
581
+ this.router.any(path, ...handlers);
582
+ return this;
583
+ }
425
584
  /**
426
585
  * @description Register an OPTIONS route.
427
586
  */
@@ -452,7 +611,9 @@ var MikroServe = class {
452
611
  const boundRequestHandler = this.requestHandler.bind(this);
453
612
  if (this.config.useHttp2) {
454
613
  if (!this.config.sslCert || !this.config.sslKey)
455
- throw new Error("SSL certificate and key paths are required when useHttp2 is true");
614
+ throw new Error(
615
+ "SSL certificate and key paths are required when useHttp2 is true"
616
+ );
456
617
  try {
457
618
  const httpsOptions = {
458
619
  key: (0, import_node_fs.readFileSync)(this.config.sslKey),
@@ -462,12 +623,16 @@ var MikroServe = class {
462
623
  return import_node_http2.default.createSecureServer(httpsOptions, boundRequestHandler);
463
624
  } catch (error) {
464
625
  if (error.message.includes("key values mismatch"))
465
- throw new Error(`SSL certificate and key do not match: ${error.message}`);
626
+ throw new Error(
627
+ `SSL certificate and key do not match: ${error.message}`
628
+ );
466
629
  throw error;
467
630
  }
468
631
  } else if (this.config.useHttps) {
469
632
  if (!this.config.sslCert || !this.config.sslKey)
470
- throw new Error("SSL certificate and key paths are required when useHttps is true");
633
+ throw new Error(
634
+ "SSL certificate and key paths are required when useHttps is true"
635
+ );
471
636
  try {
472
637
  const httpsOptions = {
473
638
  key: (0, import_node_fs.readFileSync)(this.config.sslKey),
@@ -477,7 +642,9 @@ var MikroServe = class {
477
642
  return import_node_https.default.createServer(httpsOptions, boundRequestHandler);
478
643
  } catch (error) {
479
644
  if (error.message.includes("key values mismatch"))
480
- throw new Error(`SSL certificate and key do not match: ${error.message}`);
645
+ throw new Error(
646
+ `SSL certificate and key do not match: ${error.message}`
647
+ );
481
648
  throw error;
482
649
  }
483
650
  }
@@ -488,12 +655,18 @@ var MikroServe = class {
488
655
  */
489
656
  async rateLimitMiddleware(context, next) {
490
657
  const ip = context.req.socket.remoteAddress || "unknown";
491
- context.res.setHeader("X-RateLimit-Limit", this.rateLimiter.getLimit().toString());
658
+ context.res.setHeader(
659
+ "X-RateLimit-Limit",
660
+ this.rateLimiter.getLimit().toString()
661
+ );
492
662
  context.res.setHeader(
493
663
  "X-RateLimit-Remaining",
494
664
  this.rateLimiter.getRemainingRequests(ip).toString()
495
665
  );
496
- context.res.setHeader("X-RateLimit-Reset", this.rateLimiter.getResetTime(ip).toString());
666
+ context.res.setHeader(
667
+ "X-RateLimit-Reset",
668
+ this.rateLimiter.getResetTime(ip).toString()
669
+ );
497
670
  if (!this.rateLimiter.isAllowed(ip)) {
498
671
  return {
499
672
  statusCode: 429,
@@ -580,44 +753,87 @@ var MikroServe = class {
580
753
  return new Promise((resolve, reject) => {
581
754
  const bodyChunks = [];
582
755
  let bodySize = 0;
583
- const MAX_BODY_SIZE = 1024 * 1024;
584
- let rejected = false;
756
+ const maxBodySize = this.config.maxBodySize;
757
+ let settled = false;
758
+ let timeoutId = null;
585
759
  const isDebug = this.config.debug;
586
760
  const contentType = req.headers["content-type"] || "";
587
761
  if (isDebug) {
588
762
  console.log("Content-Type:", contentType);
589
763
  }
764
+ if (this.config.requestTimeout > 0) {
765
+ timeoutId = setTimeout(() => {
766
+ if (!settled) {
767
+ settled = true;
768
+ if (isDebug) console.log("Request timeout exceeded");
769
+ reject(new Error("Request timeout"));
770
+ }
771
+ }, this.config.requestTimeout);
772
+ }
773
+ const cleanup = () => {
774
+ if (timeoutId) {
775
+ clearTimeout(timeoutId);
776
+ timeoutId = null;
777
+ }
778
+ };
590
779
  req.on("data", (chunk) => {
780
+ if (settled) {
781
+ return;
782
+ }
591
783
  bodySize += chunk.length;
592
- if (isDebug) console.log(`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`);
593
- if (bodySize > MAX_BODY_SIZE && !rejected) {
594
- rejected = true;
595
- if (isDebug) console.log(`Body size exceeded limit: ${bodySize} > ${MAX_BODY_SIZE}`);
784
+ if (isDebug)
785
+ console.log(
786
+ `Received chunk: ${chunk.length} bytes, total size: ${bodySize}`
787
+ );
788
+ if (bodySize > maxBodySize) {
789
+ settled = true;
790
+ cleanup();
791
+ if (isDebug)
792
+ console.log(
793
+ `Body size exceeded limit: ${bodySize} > ${maxBodySize}`
794
+ );
596
795
  reject(new Error("Request body too large"));
597
796
  return;
598
797
  }
599
- if (!rejected) bodyChunks.push(chunk);
798
+ bodyChunks.push(chunk);
600
799
  });
601
800
  req.on("end", () => {
602
- if (rejected) return;
801
+ if (settled) return;
802
+ settled = true;
803
+ cleanup();
603
804
  if (isDebug) console.log(`Request body complete: ${bodySize} bytes`);
604
805
  try {
605
806
  if (bodyChunks.length > 0) {
606
- const bodyString = Buffer.concat(bodyChunks).toString("utf8");
807
+ const bodyBuffer = Buffer.concat(bodyChunks);
607
808
  if (contentType.includes("application/json")) {
608
809
  try {
810
+ const bodyString = bodyBuffer.toString("utf8");
609
811
  resolve(JSON.parse(bodyString));
610
812
  } catch (error) {
611
- reject(new Error(`Invalid JSON in request body: ${error.message}`));
813
+ reject(
814
+ new Error(`Invalid JSON in request body: ${error.message}`)
815
+ );
612
816
  }
613
817
  } else if (contentType.includes("application/x-www-form-urlencoded")) {
818
+ const bodyString = bodyBuffer.toString("utf8");
614
819
  const formData = {};
615
820
  new URLSearchParams(bodyString).forEach((value, key) => {
616
821
  formData[key] = value;
617
822
  });
618
823
  resolve(formData);
824
+ } else if (contentType.includes("multipart/form-data")) {
825
+ try {
826
+ const parsed = parseMultipartFormData(bodyBuffer, contentType);
827
+ resolve(parsed);
828
+ } catch (error) {
829
+ reject(
830
+ new Error(`Invalid multipart form data: ${error.message}`)
831
+ );
832
+ }
833
+ } else if (this.isBinaryContentType(contentType)) {
834
+ resolve(bodyBuffer);
619
835
  } else {
620
- resolve(bodyString);
836
+ resolve(bodyBuffer.toString("utf8"));
621
837
  }
622
838
  } else {
623
839
  resolve({});
@@ -627,24 +843,61 @@ var MikroServe = class {
627
843
  }
628
844
  });
629
845
  req.on("error", (error) => {
630
- if (!rejected) reject(new Error(`Error reading request body: ${error.message}`));
846
+ if (!settled) {
847
+ settled = true;
848
+ cleanup();
849
+ reject(new Error(`Error reading request body: ${error.message}`));
850
+ }
851
+ });
852
+ req.on("close", () => {
853
+ cleanup();
631
854
  });
632
855
  });
633
856
  }
857
+ /**
858
+ * @description Checks if a content type is binary.
859
+ */
860
+ isBinaryContentType(contentType) {
861
+ const binaryTypes = [
862
+ "application/octet-stream",
863
+ "application/pdf",
864
+ "application/zip",
865
+ "application/gzip",
866
+ "application/x-tar",
867
+ "application/x-rar-compressed",
868
+ "application/x-7z-compressed",
869
+ "image/",
870
+ "video/",
871
+ "audio/",
872
+ "application/vnd.ms-excel",
873
+ "application/vnd.openxmlformats-officedocument",
874
+ "application/msword",
875
+ "application/vnd.ms-powerpoint"
876
+ ];
877
+ return binaryTypes.some((type) => contentType.includes(type));
878
+ }
634
879
  /**
635
880
  * @description CORS middleware.
636
881
  */
637
882
  setCorsHeaders(res, req) {
638
883
  const origin = req.headers.origin;
639
884
  const { allowedDomains = ["*"] } = this.config;
640
- if (!origin || allowedDomains.length === 0) res.setHeader("Access-Control-Allow-Origin", "*");
641
- else if (allowedDomains.includes("*")) res.setHeader("Access-Control-Allow-Origin", "*");
885
+ if (!origin || allowedDomains.length === 0)
886
+ res.setHeader("Access-Control-Allow-Origin", "*");
887
+ else if (allowedDomains.includes("*"))
888
+ res.setHeader("Access-Control-Allow-Origin", "*");
642
889
  else if (allowedDomains.includes(origin)) {
643
890
  res.setHeader("Access-Control-Allow-Origin", origin);
644
891
  res.setHeader("Vary", "Origin");
645
892
  }
646
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
647
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
893
+ res.setHeader(
894
+ "Access-Control-Allow-Methods",
895
+ "GET, POST, PUT, DELETE, PATCH, OPTIONS"
896
+ );
897
+ res.setHeader(
898
+ "Access-Control-Allow-Headers",
899
+ "Content-Type, Authorization"
900
+ );
648
901
  res.setHeader("Access-Control-Max-Age", "86400");
649
902
  }
650
903
  /**
@@ -687,11 +940,15 @@ var MikroServe = class {
687
940
  else if (typeof response.body === "string") res.end(response.body);
688
941
  else res.end(JSON.stringify(response.body));
689
942
  } else {
690
- console.warn("Unexpected response object type without writeHead/end methods");
943
+ console.warn(
944
+ "Unexpected response object type without writeHead/end methods"
945
+ );
691
946
  res.writeHead?.(response.statusCode, headers);
692
- if (response.body === null || response.body === void 0) res.end?.();
947
+ if (response.body === null || response.body === void 0)
948
+ res.end?.();
693
949
  else if (response.isRaw) res.end?.(response.body);
694
- else if (typeof response.body === "string") res.end?.(response.body);
950
+ else if (typeof response.body === "string")
951
+ res.end?.(response.body);
695
952
  else res.end?.(JSON.stringify(response.body));
696
953
  }
697
954
  }
@@ -702,15 +959,46 @@ var MikroServe = class {
702
959
  const shutdown = (error) => {
703
960
  console.log("Shutting down MikroServe server...");
704
961
  if (error) console.error("Error:", error);
962
+ this.cleanupShutdownHandlers();
705
963
  server.close(() => {
706
964
  console.log("Server closed successfully");
707
- setImmediate(() => process.exit(error ? 1 : 0));
965
+ if (process.env.NODE_ENV !== "test" && process.env.VITEST !== "true") {
966
+ setImmediate(() => process.exit(error ? 1 : 0));
967
+ }
708
968
  });
709
969
  };
710
- process.on("SIGINT", () => shutdown());
711
- process.on("SIGTERM", () => shutdown());
712
- process.on("uncaughtException", shutdown);
713
- process.on("unhandledRejection", shutdown);
970
+ const sigintHandler = () => shutdown();
971
+ const sigtermHandler = () => shutdown();
972
+ const uncaughtExceptionHandler = (error) => shutdown(error);
973
+ const unhandledRejectionHandler = (error) => shutdown(error);
974
+ this.shutdownHandlers = [
975
+ sigintHandler,
976
+ sigtermHandler,
977
+ uncaughtExceptionHandler,
978
+ unhandledRejectionHandler
979
+ ];
980
+ process.on("SIGINT", sigintHandler);
981
+ process.on("SIGTERM", sigtermHandler);
982
+ process.on("uncaughtException", uncaughtExceptionHandler);
983
+ process.on("unhandledRejection", unhandledRejectionHandler);
984
+ }
985
+ /**
986
+ * @description Cleans up shutdown event listeners to prevent memory leaks.
987
+ */
988
+ cleanupShutdownHandlers() {
989
+ if (this.shutdownHandlers.length > 0) {
990
+ const [
991
+ sigintHandler,
992
+ sigtermHandler,
993
+ uncaughtExceptionHandler,
994
+ unhandledRejectionHandler
995
+ ] = this.shutdownHandlers;
996
+ process.removeListener("SIGINT", sigintHandler);
997
+ process.removeListener("SIGTERM", sigtermHandler);
998
+ process.removeListener("uncaughtException", uncaughtExceptionHandler);
999
+ process.removeListener("unhandledRejection", unhandledRejectionHandler);
1000
+ this.shutdownHandlers = [];
1001
+ }
714
1002
  }
715
1003
  };
716
1004
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  MikroServe
3
- } from "./chunk-TQN6BEGA.mjs";
4
- import "./chunk-ZFBBESGU.mjs";
5
- import "./chunk-GUYBTPZH.mjs";
6
- import "./chunk-YOHL3T54.mjs";
7
- import "./chunk-JJX5XRNB.mjs";
3
+ } from "./chunk-C4IW4XUH.mjs";
4
+ import "./chunk-OF5DEOIU.mjs";
5
+ import "./chunk-7LU765PG.mjs";
6
+ import "./chunk-ZT2UGCN5.mjs";
7
+ import "./chunk-DMNHVQTU.mjs";
8
+ import "./chunk-VLQ7ZZIU.mjs";
8
9
  export {
9
10
  MikroServe
10
11
  };
@@ -57,7 +57,8 @@ var RateLimiter = class {
57
57
  const now = Date.now();
58
58
  const key = ip || "unknown";
59
59
  const entry = this.requests.get(key);
60
- if (!entry || entry.resetTime < now) return Math.floor((now + this.windowMs) / 1e3);
60
+ if (!entry || entry.resetTime < now)
61
+ return Math.floor((now + this.windowMs) / 1e3);
61
62
  return Math.floor(entry.resetTime / 1e3);
62
63
  }
63
64
  cleanup() {