@iprokit/service
Version:
Powering distributed systems with simplicity and speed.
226 lines • 8.68 kB
JavaScript
;
/**
* @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