diff --git a/src/Routing/LayRouting.js b/src/Routing/LayRouting.js index bb32d91..9539131 100644 --- a/src/Routing/LayRouting.js +++ b/src/Routing/LayRouting.js @@ -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} A promise that resolves to the LayResponse instance. + */ async handle(req) { - const url = new URL(req.url); - const reqContext = { - request: null, - response: null, - }; - try { - reqContext.request = new LayRequest(req); - reqContext.response = new LayResponse(this); - } catch (err) { - Logger.error(err); - } + const { hostname, pathname } = new URL(req.url); + const domain = this.#routers[hostname] ? hostname : '$default'; - let domain = url.hostname; - if([...this.routers.keys()].indexOf(domain) < 0) domain = '$default'; - return await this.routers.get(domain).handle(reqContext); + const request = new LayRequest(req); + const response = new LayResponse(this); + + try { + return await this.#routers[domain].handle({ request, response }); + } catch (error) { + Logger.error(error); + } } - 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]); } diff --git a/src/Routing/Router.js b/src/Routing/Router.js index c2828c4..937fd7b 100644 --- a/src/Routing/Router.js +++ b/src/Routing/Router.js @@ -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} 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} 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); + } }