enh(Routing) Multidomain support!
This commit is contained in:
@@ -4,42 +4,75 @@ import LayRequest from '../IO/LayRequest';
|
||||
import LayResponse from '../IO/LayResponse';
|
||||
import Logger from '../Utils/Logger';
|
||||
|
||||
/**
|
||||
* LayRouting class extends BaseRouter and manages routing for the server.
|
||||
*/
|
||||
export default class LayRouting extends BaseRouter {
|
||||
routers;
|
||||
namespace = null;
|
||||
server;
|
||||
#routers;
|
||||
#namespace = null;
|
||||
#server;
|
||||
|
||||
/**
|
||||
* Creates an instance of LayRouting.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(server, namespace = null) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.routers = new Map([
|
||||
['$default', new Router(this.server)]
|
||||
]);
|
||||
this.#server = server;
|
||||
this.#routers = {
|
||||
$default: new Router(this.#server),
|
||||
};
|
||||
this.#namespace = namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server instance.
|
||||
* @returns {Server} The server instance.
|
||||
*/
|
||||
getServer() {
|
||||
return this.server;
|
||||
return this.#server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the namespace for the routes.
|
||||
* @param {string|null} ns - The namespace to set.
|
||||
*/
|
||||
setNamespace(ns) {
|
||||
if (!ns || typeof ns != 'string') this.namespace = null;
|
||||
this.namespace = ns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the routes from the default router.
|
||||
* @returns {Map} The routes map.
|
||||
*/
|
||||
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) {
|
||||
let routerInstance;
|
||||
if(typeof router == 'undefined' || router === null) {
|
||||
routerInstance = new Router(this.server);
|
||||
routerInstance = new Router(this.#server);
|
||||
} 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 = {}) {
|
||||
if (typeof config != 'object')
|
||||
throw new Error('route config is not an object');
|
||||
@@ -53,51 +86,59 @@ export default class LayRouting extends BaseRouter {
|
||||
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) {
|
||||
const url = new URL(req.url);
|
||||
const reqContext = {
|
||||
request: null,
|
||||
response: null,
|
||||
};
|
||||
const { hostname, pathname } = new URL(req.url);
|
||||
const domain = this.#routers[hostname] ? hostname : '$default';
|
||||
|
||||
const request = new LayRequest(req);
|
||||
const response = new LayResponse(this);
|
||||
|
||||
try {
|
||||
reqContext.request = new LayRequest(req);
|
||||
reqContext.response = new LayResponse(this);
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
return await this.#routers[domain].handle({ request, response });
|
||||
} catch (error) {
|
||||
Logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
let domain = url.hostname;
|
||||
if([...this.routers.keys()].indexOf(domain) < 0) domain = '$default';
|
||||
return await this.routers.get(domain).handle(reqContext);
|
||||
}
|
||||
|
||||
add(path, cb) {
|
||||
let options = arguments[2] ?? { method: 'get', domain: '$default' };
|
||||
if(typeof options.domain != 'string') options.domain = '$default';
|
||||
if (this.namespace) {
|
||||
/**
|
||||
* Add a new route.
|
||||
* @param {string} path - The route path.
|
||||
* @param {Function} cb - The route handler function.
|
||||
* @param {Object} [options] - Additional options for the route.
|
||||
* @param {string} [options.method='get'] - The HTTP method for the route.
|
||||
* @param {string} [options.domain='$default'] - The domain for the route.
|
||||
*/
|
||||
add(path, cb, options = { method: 'get', domain: '$default' }) {
|
||||
const { domain = '$default' } = options;
|
||||
if (this.#namespace) {
|
||||
if (!path.startsWith('/')) path = '/' + path;
|
||||
path = this.namespace + path;
|
||||
path = this.#namespace + path;
|
||||
}
|
||||
|
||||
Logger(path, options)
|
||||
|
||||
if(options.domain === '$default') {
|
||||
let domains = this.routers.keys();
|
||||
for(let domain of [...domains]) {
|
||||
this.routers.get(domain).add(path, cb, options);
|
||||
if(domain === '$default') {;
|
||||
for(let routerDomain of Object.keys(this.#routers)) {
|
||||
this.#routers[routerDomain].add(path, cb, { ...options, domain: routerDomain });
|
||||
}
|
||||
} else {
|
||||
if(!this.routers.has(options.domain)) {
|
||||
const domainRouter = Router;
|
||||
domainRouter.domain = options.domain;
|
||||
this.addRouter(domainRouter);
|
||||
Logger('added new router for domain', options.domain)
|
||||
if (!this.#routers[domain]) {
|
||||
this.#routers[domain] = new Router(this.#server, { domain });
|
||||
Logger('added new router for domain', 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) {
|
||||
this.add('*', cb, arguments[1]);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,52 @@
|
||||
import RequestParser from '../Utils/RequestParser';
|
||||
import BaseRouter from './BaseRouter';
|
||||
import Logger from '../Utils/Logger';
|
||||
import LayResponse from '../IO/LayResponse';
|
||||
|
||||
/**
|
||||
* Router class that extends BaseRouter.
|
||||
* @extends BaseRouter
|
||||
*/
|
||||
export default class Router extends BaseRouter {
|
||||
domain = undefined;
|
||||
server = undefined;
|
||||
routes = new Map();
|
||||
static allMethodKey = '$all';
|
||||
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();
|
||||
this.server = server;
|
||||
this.#server = server;
|
||||
this.#domain = options.domain || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server instance.
|
||||
* @returns {Server} The server instance.
|
||||
*/
|
||||
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) {
|
||||
let method = (reqContext.request.method || '').toString().toLowerCase();
|
||||
const methodKey = `$${method.toLowerCase()}`;
|
||||
@@ -32,7 +60,7 @@ export default class Router extends BaseRouter {
|
||||
if (path.startsWith('/')) path = path.substring(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 paramsList = [];
|
||||
|
||||
@@ -65,7 +93,7 @@ export default class Router extends BaseRouter {
|
||||
(['get', 'head'].indexOf(method) < 0 &&
|
||||
!currentNode.has(methodKey) &&
|
||||
!currentNode.has(Router.allMethodKey))
|
||||
) {
|
||||
) {LayResponse
|
||||
return new Response(`[${method}] Method Not Allowed`, {
|
||||
status: 405,
|
||||
});
|
||||
@@ -102,13 +130,26 @@ export default class Router extends BaseRouter {
|
||||
|
||||
try {
|
||||
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.response
|
||||
);
|
||||
if (typeof retVal != 'undefined') {
|
||||
if (retVal instanceof Promise) {
|
||||
retVal = await retVal;
|
||||
} else if (retVal instanceof LayResponse) {
|
||||
return await retVal.build();
|
||||
} else if (retVal instanceof Response) {
|
||||
return retVal;
|
||||
} else if (typeof retVal == 'string') {
|
||||
@@ -118,9 +159,9 @@ export default class Router extends BaseRouter {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logger(err);
|
||||
console.log(err);
|
||||
return new Response('Error', { status: 500 });
|
||||
console.error(err)
|
||||
Logger.error(err);
|
||||
return new Response('Something went wrong!', { status: 500 });
|
||||
}
|
||||
|
||||
if (reqContext.response.redirect()) {
|
||||
@@ -133,18 +174,27 @@ export default class Router extends BaseRouter {
|
||||
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) {
|
||||
let options = arguments[2];
|
||||
let middleware = arguments[3];
|
||||
if (typeof callback != 'function')
|
||||
throw new Error('`callback` is not a function');
|
||||
if (typeof route != 'string')
|
||||
throw new Error('`route` is not a string');
|
||||
|
||||
if (typeof callback !== 'function')
|
||||
throw new TypeError('`callback` must be a function');
|
||||
if (typeof route !== 'string')
|
||||
throw new TypeError('`route` must be a string');
|
||||
if (Router.methods.indexOf(options.method) < 0)
|
||||
throw new Error(
|
||||
`invalid \`method\` (${options.method}) given`,
|
||||
Router.methods
|
||||
);
|
||||
|
||||
if (!Array.isArray(middleware)) {
|
||||
middleware = typeof middleware == 'function' ? [middleware] : null;
|
||||
}
|
||||
@@ -155,11 +205,11 @@ export default class Router extends BaseRouter {
|
||||
|
||||
let params = [];
|
||||
let segments = route.split('/');
|
||||
let currentNode = this.routes;
|
||||
let currentNode = this.#routes;
|
||||
|
||||
if (route == '') {
|
||||
if (currentNode.get(methodKey)) {
|
||||
if (!this.server.option('allowRouteOverwrite')) {
|
||||
if (!this.#server.option('allowRouteOverwrite')) {
|
||||
Logger(
|
||||
`overwriting of routes is prohibited: [${options.method}] \`${route}\``
|
||||
);
|
||||
@@ -196,7 +246,7 @@ export default class Router extends BaseRouter {
|
||||
}
|
||||
|
||||
if (currentNode.has(methodKey)) {
|
||||
if (!this.server.option('allowRouteOverwrite')) {
|
||||
if (!this.#server.option('allowRouteOverwrite')) {
|
||||
Logger(
|
||||
`overwriting of routes is prohibited: [${method}] \`${route}\``
|
||||
);
|
||||
@@ -209,12 +259,19 @@ export default class Router extends BaseRouter {
|
||||
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) {
|
||||
const methodKey = `$${method.toLowerCase()}`;
|
||||
if (route.startsWith('/')) route = route.substring(1);
|
||||
if (route.endsWith('/')) route = route.substring(0, route.length - 1);
|
||||
|
||||
let currentNode = this.routes;
|
||||
let currentNode = this.#routes;
|
||||
|
||||
if (route != '') {
|
||||
let segments = route.toString().split('/');
|
||||
@@ -247,4 +304,45 @@ export default class Router extends BaseRouter {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user