UNPKG

@iprokit/service

Version:

Powering distributed systems with simplicity and speed.

226 lines 8.68 kB
"use strict"; /** * @iProKit/Service * Copyright (c) 2019-2025 Rutvik Katuri / iProTechs * SPDX-License-Identifier: Apache-2.0 */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Router = void 0; // Import Libs. const http_1 = __importDefault(require("http")); const url_1 = __importDefault(require("url")); /** * `Server` binds to an IP address and port number, listening for incoming HTTP client connections. * Manages registered routes to handle various HTTP methods and dispatches requests to the appropriate route handlers. */ class Server extends http_1.default.Server { /** * Unique identifier of the server. */ identifier; /** * Routes registered on the server. */ routes; /** * Creates an instance of HTTP `Server`. * * @param identifier unique identifier of the server. */ constructor(identifier) { super(); // Initialize options. this.identifier = identifier; // Initialize variables. this.routes = new Array(); // Bind listeners. this.onRequest = this.onRequest.bind(this); // Add listeners. this.addListener('request', this.onRequest); // Apply `Router` properties. 👻 Router.applyProperties(this); } ////////////////////////////// //////// Event Listeners ////////////////////////////// /** * [Method?] is handled by `dispatch` function. */ onRequest(request, response) { // Set: Response. response.setHeader('X-Server-Identifier', this.identifier); // Set: Request. const { pathname, query } = url_1.default.parse(request.url, true); request.path = decodeURI(pathname); request.query = query; // Below line will blow your mind! 🤯 this.dispatch(0, 0, 0, this.routes, request, response, () => { }); } ////////////////////////////// //////// Dispatch ////////////////////////////// /** * Recursively loop through the routes to find and execute its handler. * * @param routeIndex index of the current route being processed. * @param stackIndex index of the current stack being processed. * @param handlerIndex index of the current handler being processed. * @param routes routes to be processed. * @param request incoming request. * @param response outgoing response. * @param unwind function called once the processed routes unwind. */ dispatch(routeIndex, stackIndex, handlerIndex, routes, request, response, unwind) { // Need I say more. if (routeIndex >= routes.length) return unwind(); const route = routes[routeIndex]; // Shits about to go down! 😎 if ('routes' in route) { // Treat as `Stack`. const pathMatches = request.path.match(route.regExp); const stackMatches = stackIndex < route.routes.length; if (pathMatches && stackMatches) { // Stack found, Save path and process the nested stacks. const unwindPath = request.path; const nestedPath = request.path.substring(route.path.length); request.path = nestedPath.startsWith('/') ? nestedPath : `/${nestedPath}`; // 🎢 const unwindFunction = () => { request.path = unwindPath; this.dispatch(routeIndex, stackIndex + 1, 0, routes, request, response, unwind); }; this.dispatch(0, 0, 0, route.routes[stackIndex], request, response, unwindFunction); return; } } else { // Treat as `Endpoint`. const methodMatches = request.method === route.method || 'ALL' === route.method; const pathMatches = request.path.match(route.regExp); const handlerMatches = handlerIndex < route.handlers.length; if (methodMatches && pathMatches && handlerMatches) { // Endpoint found, Extract params and execute the handler. request.params = route.paramKeys.reduce((params, param, index) => ((params[param] = pathMatches[index + 1] ?? undefined), params), {}); request.endpoint = route; // 🎉 const nextFunction = () => this.dispatch(routeIndex, stackIndex, handlerIndex + 1, routes, request, response, unwind); route.handlers[handlerIndex](request, response, nextFunction); return; } } // Route not found, lets keep going though the loop. this.dispatch(routeIndex + 1, 0, 0, routes, request, response, unwind); } } exports.default = Server; ////////////////////////////// //////// Router ////////////////////////////// /** * Registers routes that handle HTTP requests. * Once mounted, HTTP requests are dispatched to the appropriate registered routes. */ class Router { /** * Routes registered. */ routes; /** * Creates an instance of `Router`. */ constructor() { // Initialize Variables. this.routes = new Array(); // Apply `Router` properties. 👻 Router.applyProperties(this); } ////////////////////////////// //////// Apply ////////////////////////////// /** * Applies properties of `IRouter` interface to the provided instance, * enabling registration of routes. * * @param instance instance to which the `IRouter` properties are applied. */ static applyProperties(instance) { instance.get = this.registerEndpoint(instance, 'GET'); instance.post = this.registerEndpoint(instance, 'POST'); instance.put = this.registerEndpoint(instance, 'PUT'); instance.patch = this.registerEndpoint(instance, 'PATCH'); instance.delete = this.registerEndpoint(instance, 'DELETE'); instance.all = this.registerEndpoint(instance, 'ALL'); instance.mount = this.registerStack(instance); } ////////////////////////////// //////// Register ////////////////////////////// /** * Registers an individual HTTP endpoint for a specific HTTP method. * * @param instance router instance where the endpoint will be registered. * @param method HTTP method for the endpoint. */ static registerEndpoint(instance, method) { return (path, ...handlers) => { const regExp = new RegExp(`^${this.transformRequiredParams(this.transformOptionalParams(this.transformWildcard(this.transformTrailingSlash(path))))}$`); const paramKeys = (path.match(/:([^\s/]+)/g) || []).map((param) => param.slice(1).replace('?', '')); instance.routes.push({ method, path, regExp, paramKeys, handlers }); return instance; }; } /** * Registers a stack of routes. * * @param instance router instance where the stack will be registered. */ static registerStack(instance) { return (path, ...routers) => { const regExp = new RegExp(`^${this.transformTrailingSlash(path)}`); const routes = routers.map((router) => router.routes); instance.routes.push({ path, regExp, routes }); return instance; }; } ////////////////////////////// //////// Transform Path ////////////////////////////// /** * Transforms the path by removing the trailing slash, except for the root path. * * @param path path containing trailing slash. */ static transformTrailingSlash(path) { return path.replace(/\/$/, '') || '/'; } /** * Transforms wildcard characters (*) in the path to regex-compatible format. * * @param path path containing wildcards. */ static transformWildcard(path) { return path.replace(/\*/g, '.*'); } /** * Transforms optional parameters in the path to regex-compatible format. * * @param path path containing required parameters. */ static transformRequiredParams(path) { return path.replace(/:([^\s/]+)/g, '([^/]+)'); } /** * Transforms required parameters in the path to regex-compatible format. * * @param path path containing optional parameters. */ static transformOptionalParams(path) { return path.replace(/\/:([^\s/]+)\?/g, '(?:/([^/]*)?)?'); } } exports.Router = Router; //# sourceMappingURL=server.js.map