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 { static allMethodKey = '$all'; static methods = ['all', 'get', 'post', 'put', 'patch', 'head', 'options']; #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.#domain = options.domain || undefined; } /** * Get the server instance. * @returns {Server} The server instance. */ getServer() { 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()}`; let path; let url = reqContext.request.url; if (!(url instanceof URL) && typeof url == 'string') { url = new URL(url); path = url.pathname; } if (path.startsWith('/')) path = path.substring(1); if (path.endsWith('/')) path = path.substring(0, path.length - 1); let currentNode = this.#routes; let fallbackNode = currentNode.has('*') ? currentNode.get('*') : undefined; let paramsList = []; if (path != '') { let segments = path.split('/'); for (let index = 0; index < segments.length; index++) { const segment = segments[index]; if (currentNode.has('*')) { fallbackNode = currentNode.get('*'); } if (currentNode.has(segment)) { currentNode = currentNode.get(segment); continue; } if (currentNode.has(':') || currentNode.has('?')) { paramsList.push(segment); currentNode = currentNode.get(':') ?? currentNode.get('?'); continue; } currentNode = fallbackNode ?? null; break; } } if ( !(currentNode instanceof Map) || (['get', 'head'].indexOf(method) < 0 && !currentNode.has(methodKey) && !currentNode.has(Router.allMethodKey)) ) {LayResponse return new Response(`[${method}] Method Not Allowed`, { status: 405, }); } if ( !currentNode.has(methodKey) && !currentNode.get(Router.allMethodKey) ) { return new Response(`Not Implemented: [${method}, ${req.url}]`, { status: 501, }); } let handle = currentNode.get(methodKey) || currentNode.get(Router.allMethodKey); if (typeof handle == 'object' && typeof handle.callback == 'function') { if (Array.isArray(handle.params)) { for (const name of handle.params) { let value = paramsList[reqContext.request.params.size]; if (reqContext.request.params.has(name)) { let yet = reqContext.request.params.get(name); if (!Array.isArray(yet)) { yet = [yet]; } yet.push(value); value = yet; } reqContext.request.params.set(name, value); } } } try { await reqContext.request.parseBody(); 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') { await reqContext.response.text(retVal); } else { reqContext.response.json(retVal); } } } catch (err) { console.error(err) Logger.error(err); return new Response('Something went wrong!', { status: 500 }); } if (reqContext.response.redirect()) { return Response.redirect( reqContext.response.redirect, reqContext.response.status || 302 ); } 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 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; } const methodKey = `$${options.method.toLowerCase()}`; if (route.startsWith('/')) route = route.substring(1); if (route.endsWith('/')) route = route.substring(0, route.length - 1); let params = []; let segments = route.split('/'); let currentNode = this.#routes; if (route == '') { if (currentNode.get(methodKey)) { if (!this.#server.option('allowRouteOverwrite')) { Logger( `overwriting of routes is prohibited: [${options.method}] \`${route}\`` ); return; } Logger(`overwriting route for \`${route}\``); } currentNode.set(methodKey, { callback, middleware, params: [] }); return; } // add method to the segments to store the callback with // and to allow different methods for the same route for (let index = 0; index < segments.length; index++) { let segment = segments[index]; let varName = null; if (segment.startsWith(':')) { let newSegment = ':'; varName = segment.substring(1); if (segment.endsWith('?')) { // optional param newSegment = '?'; varName = varName.substring(0, -1); } segment = newSegment; params.push(varName); } if (!currentNode.has(segment)) currentNode.set(segment, new Map()); currentNode = currentNode.get(segment); } if (currentNode.has(methodKey)) { if (!this.#server.option('allowRouteOverwrite')) { Logger( `overwriting of routes is prohibited: [${method}] \`${route}\`` ); return; } Logger(`overwriting route for \`${route}\``); } 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; if (route != '') { let segments = route.toString().split('/'); for (let index = 0; index < segments.length; index++) { const segment = segments[index]; if (currentNode.has(segment)) { currentNode = currentNode.get(segment); continue; } if (currentNode.has(':') || currentNode.has('?')) { currentNode = currentNode.get(':') ?? currentNode.get('?'); continue; } currentNode = null; break; } } if ( !(currentNode instanceof Map) || !currentNode.has(methodKey) || (typeof cb == 'function' && currentNode.get(methodKey) != cb) ) { return false; } 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); } }