enh(Routing) Multidomain support!
This commit is contained in:
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user