enh(Routing) Multidomain support!

This commit is contained in:
gh0sTedBuddy
2024-04-01 22:46:52 +01:00
parent 8d34a9cb7c
commit 5e6e66dceb
2 changed files with 203 additions and 64 deletions

View File

@@ -4,42 +4,75 @@ import LayRequest from '../IO/LayRequest';
import LayResponse from '../IO/LayResponse'; import LayResponse from '../IO/LayResponse';
import Logger from '../Utils/Logger'; import Logger from '../Utils/Logger';
/**
* LayRouting class extends BaseRouter and manages routing for the server.
*/
export default class LayRouting extends BaseRouter { export default class LayRouting extends BaseRouter {
routers; #routers;
namespace = null; #namespace = null;
server; #server;
/**
* Creates an instance of LayRouting.
* @constructor
*/
constructor(server, namespace = null) { constructor(server, namespace = null) {
super(); super();
this.server = server; this.#server = server;
this.routers = new Map([ this.#routers = {
['$default', new Router(this.server)] $default: new Router(this.#server),
]); };
this.#namespace = namespace;
} }
/**
* Get the server instance.
* @returns {Server} The server instance.
*/
getServer() { getServer() {
return this.server; return this.#server;
} }
/**
* Set the namespace for the routes.
* @param {string|null} ns - The namespace to set.
*/
setNamespace(ns) { setNamespace(ns) {
if (!ns || typeof ns != 'string') this.namespace = null; if (!ns || typeof ns != 'string') this.namespace = null;
this.namespace = ns; this.namespace = ns;
} }
/**
* Get the routes from the default router.
* @returns {Map} The routes map.
*/
routes() { routes() {
return this.routers.get('$default').routes; return this.#routers.$default.routes;
} }
/**
* Add a new router instance.
* @param {Router} router - The router class to add.
*/
addRouter(router) { addRouter(router) {
let routerInstance; let routerInstance;
if(typeof router == 'undefined' || router === null) { if(typeof router == 'undefined' || router === null) {
routerInstance = new Router(this.server); routerInstance = new Router(this.#server);
} else { } else {
routerInstance = new router(this.server); routerInstance = new router(this.#server);
} }
this.routers.set(router.domain || '$default', routerInstance); this.#routers[router.domain || '$default'] = routerInstance;
} }
/**
* Add a new route configuration.
* @param {Object} config - The route configuration object.
* @param {string} config.path - The route path.
* @param {Function} config.handle - The route handler function.
* @param {string} [config.method='get'] - The HTTP method for the route.
* @returns {LayRouting} The LayRouting instance for chaining.
*/
route(config = {}) { route(config = {}) {
if (typeof config != 'object') if (typeof config != 'object')
throw new Error('route config is not an object'); throw new Error('route config is not an object');
@@ -53,51 +86,59 @@ export default class LayRouting extends BaseRouter {
return this; return this;
} }
/**
* Handle an incoming request.
* @param {IncomingMessage} req - The incoming request object.
* @returns {Promise<LayResponse>} A promise that resolves to the LayResponse instance.
*/
async handle(req) { async handle(req) {
const url = new URL(req.url); const { hostname, pathname } = new URL(req.url);
const reqContext = { const domain = this.#routers[hostname] ? hostname : '$default';
request: null,
response: null, const request = new LayRequest(req);
}; const response = new LayResponse(this);
try { try {
reqContext.request = new LayRequest(req); return await this.#routers[domain].handle({ request, response });
reqContext.response = new LayResponse(this); } catch (error) {
} catch (err) { Logger.error(error);
Logger.error(err); }
} }
let domain = url.hostname; /**
if([...this.routers.keys()].indexOf(domain) < 0) domain = '$default'; * Add a new route.
return await this.routers.get(domain).handle(reqContext); * @param {string} path - The route path.
} * @param {Function} cb - The route handler function.
* @param {Object} [options] - Additional options for the route.
add(path, cb) { * @param {string} [options.method='get'] - The HTTP method for the route.
let options = arguments[2] ?? { method: 'get', domain: '$default' }; * @param {string} [options.domain='$default'] - The domain for the route.
if(typeof options.domain != 'string') options.domain = '$default'; */
if (this.namespace) { add(path, cb, options = { method: 'get', domain: '$default' }) {
const { domain = '$default' } = options;
if (this.#namespace) {
if (!path.startsWith('/')) path = '/' + path; if (!path.startsWith('/')) path = '/' + path;
path = this.namespace + path; path = this.#namespace + path;
} }
Logger(path, options) if(domain === '$default') {;
for(let routerDomain of Object.keys(this.#routers)) {
if(options.domain === '$default') { this.#routers[routerDomain].add(path, cb, { ...options, domain: routerDomain });
let domains = this.routers.keys();
for(let domain of [...domains]) {
this.routers.get(domain).add(path, cb, options);
} }
} else { } else {
if(!this.routers.has(options.domain)) { if (!this.#routers[domain]) {
const domainRouter = Router; this.#routers[domain] = new Router(this.#server, { domain });
domainRouter.domain = options.domain; Logger('added new router for domain', domain);
this.addRouter(domainRouter);
Logger('added new router for domain', options.domain)
} }
this.routers.get(options.domain).add (path, cb, options); this.#routers[domain].add (path, cb, options);
} }
} }
/**
* Add a middleware function for all routes.
* @param {Function} cb - The middleware function.
* @param {Object} [options] - Additional options for the middleware.
*/
use (cb) { use (cb) {
this.add('*', cb, arguments[1]); this.add('*', cb, arguments[1]);
} }

View File

@@ -1,24 +1,52 @@
import RequestParser from '../Utils/RequestParser'; import RequestParser from '../Utils/RequestParser';
import BaseRouter from './BaseRouter'; import BaseRouter from './BaseRouter';
import Logger from '../Utils/Logger'; import Logger from '../Utils/Logger';
import LayResponse from '../IO/LayResponse';
/**
* Router class that extends BaseRouter.
* @extends BaseRouter
*/
export default class Router extends BaseRouter { export default class Router extends BaseRouter {
domain = undefined;
server = undefined;
routes = new Map();
static allMethodKey = '$all'; static allMethodKey = '$all';
static methods = ['all', 'get', 'post', 'put', 'patch', 'head', 'options']; static methods = ['all', 'get', 'post', 'put', 'patch', 'head', 'options'];
headerList = new Map();
constructor(server) { #domain = undefined;
#server = undefined;
#routes = new Map();
#headerList = new Map();
/**
* Constructor for the Router class.
* @param {Server} server - The server instance.
*/
constructor(server, options = {}) {
super(); super();
this.server = server; this.#server = server;
this.#domain = options.domain || undefined;
} }
/**
* Get the server instance.
* @returns {Server} The server instance.
*/
getServer() { getServer() {
return this.server; return this.#server;
} }
/**
* Get the domain of the router
* @returns {String} The base domain for the router.
*/
getDomain() {
return this.#domain;
}
/**
* Handle the incoming request.
* @param {RequestContext} reqContext - The request context.
* @returns {Promise<Response>} The response promise.
*/
async handle(reqContext) { async handle(reqContext) {
let method = (reqContext.request.method || '').toString().toLowerCase(); let method = (reqContext.request.method || '').toString().toLowerCase();
const methodKey = `$${method.toLowerCase()}`; const methodKey = `$${method.toLowerCase()}`;
@@ -32,7 +60,7 @@ export default class Router extends BaseRouter {
if (path.startsWith('/')) path = path.substring(1); if (path.startsWith('/')) path = path.substring(1);
if (path.endsWith('/')) path = path.substring(0, path.length - 1); if (path.endsWith('/')) path = path.substring(0, path.length - 1);
let currentNode = this.routes; let currentNode = this.#routes;
let fallbackNode = currentNode.has('*') ? currentNode.get('*') : undefined; let fallbackNode = currentNode.has('*') ? currentNode.get('*') : undefined;
let paramsList = []; let paramsList = [];
@@ -65,7 +93,7 @@ export default class Router extends BaseRouter {
(['get', 'head'].indexOf(method) < 0 && (['get', 'head'].indexOf(method) < 0 &&
!currentNode.has(methodKey) && !currentNode.has(methodKey) &&
!currentNode.has(Router.allMethodKey)) !currentNode.has(Router.allMethodKey))
) { ) {LayResponse
return new Response(`[${method}] Method Not Allowed`, { return new Response(`[${method}] Method Not Allowed`, {
status: 405, status: 405,
}); });
@@ -102,13 +130,26 @@ export default class Router extends BaseRouter {
try { try {
await reqContext.request.parseBody(); await reqContext.request.parseBody();
let retVal = await handle.callback( let retVal = await this.executeMiddleware(
handle.middleware,
reqContext.request,
reqContext.response,
async () => {
return await handle.callback(reqContext.request, reqContext.response);
},
5000 // Add a timeout of 5 seconds for middleware execution
);
await reqContext.request.parseBody();
retVal = await handle.callback(
reqContext.request, reqContext.request,
reqContext.response reqContext.response
); );
if (typeof retVal != 'undefined') { if (typeof retVal != 'undefined') {
if (retVal instanceof Promise) { if (retVal instanceof Promise) {
retVal = await retVal; retVal = await retVal;
} else if (retVal instanceof LayResponse) {
return await retVal.build();
} else if (retVal instanceof Response) { } else if (retVal instanceof Response) {
return retVal; return retVal;
} else if (typeof retVal == 'string') { } else if (typeof retVal == 'string') {
@@ -118,9 +159,9 @@ export default class Router extends BaseRouter {
} }
} }
} catch (err) { } catch (err) {
Logger(err); console.error(err)
console.log(err); Logger.error(err);
return new Response('Error', { status: 500 }); return new Response('Something went wrong!', { status: 500 });
} }
if (reqContext.response.redirect()) { if (reqContext.response.redirect()) {
@@ -133,18 +174,27 @@ export default class Router extends BaseRouter {
return await reqContext.response.build(); return await reqContext.response.build();
} }
/**
* Add a route to the router.
* @param {string} route - The route path.
* @param {function} callback - The route callback function.
* @param {Object} options - The route options.
* @param {function[]} [middleware] - The middleware functions.
*/
add(route, callback) { add(route, callback) {
let options = arguments[2]; let options = arguments[2];
let middleware = arguments[3]; let middleware = arguments[3];
if (typeof callback != 'function')
throw new Error('`callback` is not a function'); if (typeof callback !== 'function')
if (typeof route != 'string') throw new TypeError('`callback` must be a function');
throw new Error('`route` is not a string'); if (typeof route !== 'string')
throw new TypeError('`route` must be a string');
if (Router.methods.indexOf(options.method) < 0) if (Router.methods.indexOf(options.method) < 0)
throw new Error( throw new Error(
`invalid \`method\` (${options.method}) given`, `invalid \`method\` (${options.method}) given`,
Router.methods Router.methods
); );
if (!Array.isArray(middleware)) { if (!Array.isArray(middleware)) {
middleware = typeof middleware == 'function' ? [middleware] : null; middleware = typeof middleware == 'function' ? [middleware] : null;
} }
@@ -155,11 +205,11 @@ export default class Router extends BaseRouter {
let params = []; let params = [];
let segments = route.split('/'); let segments = route.split('/');
let currentNode = this.routes; let currentNode = this.#routes;
if (route == '') { if (route == '') {
if (currentNode.get(methodKey)) { if (currentNode.get(methodKey)) {
if (!this.server.option('allowRouteOverwrite')) { if (!this.#server.option('allowRouteOverwrite')) {
Logger( Logger(
`overwriting of routes is prohibited: [${options.method}] \`${route}\`` `overwriting of routes is prohibited: [${options.method}] \`${route}\``
); );
@@ -196,7 +246,7 @@ export default class Router extends BaseRouter {
} }
if (currentNode.has(methodKey)) { if (currentNode.has(methodKey)) {
if (!this.server.option('allowRouteOverwrite')) { if (!this.#server.option('allowRouteOverwrite')) {
Logger( Logger(
`overwriting of routes is prohibited: [${method}] \`${route}\`` `overwriting of routes is prohibited: [${method}] \`${route}\``
); );
@@ -209,12 +259,19 @@ export default class Router extends BaseRouter {
currentNode.set(methodKey, { callback, middleware, params }); currentNode.set(methodKey, { callback, middleware, params });
} }
/**
* Remove a route from the router.
* @param {string} route - The route path.
* @param {string} method - The HTTP method.
* @param {function} [cb=null] - The callback function to remove.
* @returns {boolean} True if the route was removed, false otherwise.
*/
remove(route, method, cb = null) { remove(route, method, cb = null) {
const methodKey = `$${method.toLowerCase()}`; const methodKey = `$${method.toLowerCase()}`;
if (route.startsWith('/')) route = route.substring(1); if (route.startsWith('/')) route = route.substring(1);
if (route.endsWith('/')) route = route.substring(0, route.length - 1); if (route.endsWith('/')) route = route.substring(0, route.length - 1);
let currentNode = this.routes; let currentNode = this.#routes;
if (route != '') { if (route != '') {
let segments = route.toString().split('/'); let segments = route.toString().split('/');
@@ -247,4 +304,45 @@ export default class Router extends BaseRouter {
return currentNode.delete(methodKey); return currentNode.delete(methodKey);
} }
/**
* Execute the middleware chain.
* @param {function[]} middleware - The middleware functions.
* @param {Request} req - The request object.
* @param {Response} res - The response object.
* @param {function} next - The next function to call.
* @returns {Promise<void>} A promise that resolves when the middleware chain is completed.
*/
async executeMiddleware(middleware, req, res, next) {
if (!Array.isArray(middleware) || middleware.length === 0) {
return await next();
}
const executeNext = async (index) => {
if (index >= middleware.length) {
return await next();
}
const currentMiddleware = middleware[index];
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Middleware execution timed out'));
}, timeout);
});
try {
await Promise.race([
currentMiddleware(req, res, async () => {
return await executeNext(index + 1);
}),
timeoutPromise,
]);
} catch (err) {
throw err;
}
};
return await executeNext(0);
}
} }