From f2cf3821f26551b998099fdb3ec015b40fee9450 Mon Sep 17 00:00:00 2001
From: Flummi <git@srv.fail>
Date: Sat, 15 Mar 2025 16:41:11 +0100
Subject: [PATCH] Erweitere die Flummpress-Klasse um Response- und
 Middleware-Typen, verbessere die Fehlerbehandlung und optimiere die
 Anfrageverarbeitung

---
 src/index.ts | 261 +++++++++++++++++++++++----------------------------
 1 file changed, 119 insertions(+), 142 deletions(-)

diff --git a/src/index.ts b/src/index.ts
index 7d39b48..c37e740 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,9 +6,9 @@ import querystring from "node:querystring";
 import Router from "./router.js";
 import Tpl from "./template.js";
 
-export { Router, Tpl, Request };
+export { Router, Tpl, Request, Response, Middleware };
 
-type Middleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
+type Middleware = (req: Request, res: Response, next: () => void) => void;
 
 interface Request extends IncomingMessage {
   parsedUrl: {
@@ -22,16 +22,27 @@ interface Request extends IncomingMessage {
   post?: Record<string, string>;
 };
 
+interface Response extends ServerResponse {
+  reply: (options: { code?: number; type?: string; body: string }) => void;
+  json: (body: string, code?: number) => void;
+  html: (body: string, code?: number) => void;
+  redirect: (target: string, code?: number) => void;
+}
+
 export default class Flummpress {
   private server?: http.Server;
+  private errorHandler: Middleware;
   router: Router;
   tpl: Tpl;
-  middleware: Set<Middleware>;
+  middleware: Middleware[];
 
   constructor() {
     this.router = new Router();
     this.tpl = new Tpl();
-    this.middleware = new Set();
+    this.middleware = [];
+    this.errorHandler = async (req: Request, res: Response) => {
+      res.writeHead(500).end("500 - Internal Server Error");
+    };
   }
 
   /**
@@ -39,15 +50,18 @@ export default class Flummpress {
    * @param {any} obj - An instance of Router, Tpl, or a middleware function.
    * @returns {this} - The current instance for chaining.
    */
-  use(obj: Router | Tpl): this {
+  use(obj: Router | Tpl | Middleware): this {
     if(obj instanceof Router)
       this.router.use(obj);
     else if(obj instanceof Tpl)
       this.tpl = obj;
-    else {
-      if(!this.middleware.has(obj))
-        this.middleware.add(obj);
-    }
+    else
+      this.middleware.push(obj);
+    return this;
+  }
+
+  setErrorHandler(handler: Middleware): this {
+    this.errorHandler = handler;
     return this;
   }
 
@@ -57,169 +71,132 @@ export default class Flummpress {
    * @returns {this} - The current instance for chaining.
    */
   listen(...args: any[]): this {
-    this.server = http.createServer(async (request: IncomingMessage, res: ServerResponse) => {
-      const req = request as Request;
-      const t_start = process.hrtime();
+    this.server = http
+      .createServer(async (request: IncomingMessage, response: ServerResponse) => {
+        const req = request as Request;
+        const res = this.createResponse(req, response);
+        const start = process.hrtime();
 
-      // URL and query parsing
-      const _url = new URL(req.url!.replace(/(?!^.)(\/+)?$/, ''), "http://localhost");
-      req.parsedUrl = {
-        pathname: _url.pathname,
-        split: _url.pathname.split("/").slice(1),
-        searchParams: _url.searchParams,
-        qs: Object.fromEntries(Object.entries(querystring.parse(_url.search.substring(1))).filter(([_, v]) => typeof v === 'string')) as Record<string, string>,
-      };
+        try {
+          this.parseRequest(req);
 
-      // extract cookies
-      req['cookies'] = {};
-      if(req.headers.cookie) {
-        req.headers.cookie.split("; ").forEach((c: string) => {
-          const parts = c.split('=');
-          req.cookies[parts.shift()?.trim() ?? ""] = decodeURI(parts.join('='));
-        });
-      }
+          for(const mw of this.middleware) {
+            await mw(req, res, () => {});
+          }
 
-      // run middleware
-      await Promise.all([...this.middleware].map(m => m(req, res, () => {})));
+          const route = this.router.getRoute(req.parsedUrl.pathname, req.method!);
 
-      // route and method handling
-      const method = req.method === 'HEAD' ? 'get' : req.method?.toLowerCase();
-      const route = this.router.getRoute(req.parsedUrl.pathname, req.method === 'HEAD' ? 'GET' : req.method!);
+          if(route) {
+            const [pathPattern, methods] = route;
+            const method = req.method?.toLowerCase();
+            const handler = methods[method!];
+            const middleware = methods[`${method}mw`];
 
-      if(route) {
-        // route found
-        const cb = route[1][method!];
-        const middleware = route[1][`${method}mw`];
+            if(middleware) {
+              for(const mw of middleware) {
+                await mw(req, res, () => {});
+              }
+            }
 
-        req['params'] = req.parsedUrl.pathname.match(new RegExp(route[0]))?.groups;
-        req['post'] = await this.readBody(req);
+            if(handler) {
+              req.params = req.parsedUrl.pathname.match(new RegExp(pathPattern))?.groups || {};
+              req.post = await this.readBody(req);
+              handler(req, res);
+            }
+            else {
+              res.writeHead(405).end("405 - Method Not Allowed");
+            }
+          }
+          else {
+            res.writeHead(404).end("404 - Not Found");
+          }
+        }
+        catch(err: any) {
+          this.errorHandler(req, res, () => {});
+        }
 
-        const result = await this.processMiddleware(middleware, req, this.createResponse(req, res));
-        if(result)
-          cb(req, res);
-      }
-      else {
-        // route not found
-        res.writeHead(404).end(`404 - ${req.method} ${req.parsedUrl.pathname}`);
-      }
-
-      console.log([
-        `[${new Date().toLocaleTimeString()}]`,
-        `${(process.hrtime(t_start)[1] / 1e6).toFixed(2)}ms`,
-        `${req.method} ${res.statusCode}`,
-        req.parsedUrl.pathname
-      ].map(e => e.toString().padEnd(15)).join(""));
-    }).listen(...args);
+        console.log([
+          `[${new Date().toISOString()}]`,
+          `${(process.hrtime(start)[1] / 1e6).toFixed(2)}ms`,
+          `${req.method} ${res.statusCode}`,
+          req.parsedUrl.pathname,
+        ].join(" | "));
+      }).listen(...args);
 
     return this;
   }
 
+  private parseRequest(req: Request): void {
+    const url = new URL(req.url!.replace(/(?!^.)(\/+)?$/, ""), "http://localhost");
+    req.parsedUrl = {
+      pathname: url.pathname,
+      split: url.pathname.split("/").slice(1),
+      searchParams: url.searchParams,
+      qs: Object.fromEntries(url.searchParams.entries()),
+    };
+    req.cookies = {};
+    if(req.headers.cookie) {
+      req.headers.cookie.split("; ").forEach((cookie) => {
+        const [key, value] = cookie.split("=");
+        req.cookies[key] = decodeURIComponent(value);
+      });
+    }
+  }
+
   /**
    * Reads and parses the body of an incoming HTTP request.
    * @param {IncomingMessage} req - The HTTP request object.
    * @returns {Promise<any>} - A promise that resolves to the parsed body data.
    */
-  readBody(req: IncomingMessage): Promise<Record<string, string>> {
+  private async readBody(req: IncomingMessage): Promise<Record<string, string>> {
     return new Promise((resolve, reject) => {
-      let data = "";
+      let body = "";
 
-      req.on("data", (chunk: string) => data += chunk);
+      req.on("data", (chunk) => {
+        body += chunk;
+      });
 
       req.on("end", () => {
-        if(req.headers["content-type"] === "application/json") {
-          try {
-            resolve(JSON.parse(data) as Record<string, string>);
-          }
-          catch(err: any) {
-            reject(err);
-          }
-        }
-        else {
-          const parsedData = querystring.parse(data);
-          const decodedData = Object.fromEntries(
-            Object.entries(parsedData).map(([key, value]) => {
-              try {
-                return [key, decodeURIComponent(value as string)];
-              }
-              catch(err: any) {
-                return [key, value];
-              }
-            })
+        try {
+          resolve(
+            req.headers["content-type"] === "application/json"
+              ? JSON.parse(body)
+              : querystring.parse(body)
           );
-          resolve(decodedData as Record<string, string>);
+        }
+        catch(err: any) {
+          reject(err);
         }
       });
 
-      req.on("error", (err: Error) => reject(err));
+      req.on("error", reject);
     });
   }
 
-  /**
-   * Enhances the HTTP response object with additional methods for convenience.
-   * @param {Request} req - The HTTP request object.
-   * @param {ServerResponse} res - The HTTP response object.
-   * @returns {ServerResponse & {
-  *  reply: Function, json: Function, html: Function, redirect: Function
-  * }} - The enhanced response object.
-  */
-  createResponse(req: Request, res: ServerResponse): ServerResponse & {
-    reply: (options: { code?: number; type?: string; body: string }) => ServerResponse;
-    json: (body: string, code?: number) => ServerResponse;
-    html: (body: string, code?: number) => ServerResponse;
-    redirect: (target: string, code?: number) => void;
-  } {
-    const enhancedRes = Object.assign(res, {
-      reply: ({ code = 200, type = "text/html", body }: { code?: number; type?: string; body: string }) => {
-        res.writeHead(code, {
-          "content-type": `${type}; charset=UTF-8`,
-          "content-length": Buffer.byteLength(body, "utf-8"),
-        });
-        if(req.method === "HEAD")
-          body = null as unknown as string;
-        res.end(body);
-        return res;
-      },
-      json: (body: string, code = 200) => {
-        if(typeof body === "object")
-          body = JSON.stringify(body);
-        return enhancedRes.reply({ code, body, type: "application/json" });
-      },
-      html: (body: string, code = 200) => enhancedRes.reply({ code, body, type: "text/html" }),
-      redirect: (target: string, code = 307) => {
-        res.writeHead(code, {
-          "Cache-Control": "no-cache, public",
-          "Location": target,
-        });
-        res.end();
-      }
-    });
+  private createResponse(
+    req: Request,
+    response: ServerResponse
+  ): Response {
+    const res = response as Response;
 
-    return enhancedRes;
-  }
+    res.reply = ({ code = 200, type = "text/html", body }) => {
+      res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` });
+      res.end(body);
+    };
 
-  /**
-   * Processes middleware functions for an incoming request.
-   * @param {Middleware} middleware - The middleware function to process.
-   * @param {IncomingMessage} req - The HTTP request object.
-   * @param {ServerResponse} res - The HTTP response object.
-   * @returns {Promise<boolean>} - Resolves to true if middleware processing is successful.
-   */
-  private async processMiddleware(
-    middleware: Middleware[],
-    req: IncomingMessage,
-    res: ServerResponse
-  ): Promise<boolean> {
-    if(!middleware || middleware.length === 0)
-      return true;
+    res.json = (body, code = 200) => {
+      res.reply({ code, type: "application/json", body: JSON.stringify(body) });
+    };
 
-    for(const fn of middleware) {
-      const proceed = await new Promise<boolean>(resolve => {
-        fn(req, res, () => resolve(true));
-      });
+    res.html = (body, code = 200) => {
+      res.reply({ code, type: "text/html", body });
+    };
 
-      if(!proceed)
-        return false;
-    }
-    return true;
+    res.redirect = (target, code = 302) => {
+      res.writeHead(code, { Location: target });
+      res.end();
+    };
+
+    return res;
   }
 }