hyper-express
Version:
High performance Node.js webserver with a simple-to-use API powered by uWebsockets.js under the hood.
211 lines (184 loc) • 9.56 kB
JavaScript
'use strict';
const { parse_path_parameters } = require('../../shared/operators.js');
class Route {
id = null;
app = null;
path = '';
method = '';
pattern = '';
handler = null;
options = null;
streaming = null;
max_body_length = null;
path_parameters_key = null;
/**
* Constructs a new Route object.
* @param {Object} options
* @param {import('../Server.js')} options.app - The server instance.
* @param {String} options.method - The HTTP method.
* @param {String} options.pattern - The route pattern.
* @param {Function} options.handler - The route handler.
*/
constructor({ app, method, pattern, options, handler }) {
this.id = app._get_incremented_id();
this.app = app;
this.pattern = pattern;
this.handler = handler;
this.options = options;
this.method = method.toUpperCase();
this.streaming = options.streaming || app._options.streaming || {};
this.max_body_length = options.max_body_length || app._options.max_body_length;
this.path_parameters_key = parse_path_parameters(pattern);
// Translate to HTTP DELETE
if (this.method === 'DEL') this.method = 'DELETE';
// Cache the expected request path for this route if it is not a wildcard route
// This will be used to optimize performance for determining incoming request paths
const wildcard = pattern.includes('*') || this.path_parameters_key.length > 0;
if (!wildcard) this.path = pattern;
}
/**
* @typedef {Object} Middleware
* @property {Number} id - Unique identifier for this middleware based on it's registeration order.
* @property {String} pattern - The middleware pattern.
* @property {function} handler - The middleware handler function.
* @property {Boolean=} match - Whether to match the middleware pattern against the request path.
*/
/**
* Binds middleware to this route and sorts middlewares to ensure execution order.
*
* @param {Middleware} middleware
*/
use(middleware) {
// Store and sort middlewares to ensure proper execution order
this.options.middlewares.push(middleware);
}
/**
* Handles an incoming request through this route.
*
* @param {import('../http/Request.js')} request The HyperExpress request object.
* @param {import('../http/Response.js')} response The HyperExpress response object.
* @param {Number=} cursor The middleware cursor.
*/
handle(request, response, cursor = 0) {
// Do not handle the request if the response has been sent aka. the request is no longer active
if (response.completed) return;
// Retrieve the middleware for the current cursor, track the cursor if there is a valid middleware
let iterator;
const middleware = this.options.middlewares[cursor];
if (middleware) {
// Determine if this middleware requires path matching
if (middleware.match) {
// Check if the middleware pattern matches that starting of the request path
if (request.path.startsWith(middleware.pattern)) {
// Ensure that the character after the middleware pattern is either a trailing slash or out of bounds of string
const trailing = request.path[middleware.pattern.length];
if (trailing !== '/' && trailing !== undefined) {
// This handles cases where "/docs" middleware will incorrectly match "/docs-JSON" for example
return this.handle(request, response, cursor + 1);
}
} else {
// Since the middleware pattern does not match the start of the request path, skip this middleware
return this.handle(request, response, cursor + 1);
}
}
// Track the middleware cursor to prevent double execution
response._track_middleware_cursor(cursor);
// Initialize the iterator for this middleware
iterator = (error) => {
// If an error occured, pipe it to the error handler
if (error instanceof Error) return response.throw(error);
// Handle this request again with an incremented cursor to execute the next middleware or route handler
this.handle(request, response, cursor + 1);
};
}
// Determine if this is an async handler which can explicitly throw uncaught errors
const is_async_handler = (middleware ? middleware.handler : this.handler).constructor.name === 'AsyncFunction';
if (is_async_handler) {
// Execute the middleware or route handler within a promise to catch and pipe synchronous errors
new Promise(async (resolve) => {
try {
if (middleware) {
// Execute the middleware or route handler with the iterator
await middleware.handler(request, response, iterator);
// Call the iterator anyways in case the middleware never calls the next() iterator
iterator();
} else {
await this.handler(request, response);
}
} catch (error) {
// Catch and pipe any errors to the error handler
response.throw(error);
}
// Resolve promise to ensure it is properly cleaned up from memory
resolve();
});
} else {
// Execute the middleware or route handler within a protected try/catch to catch and pipe synchronous errors
try {
let output;
if (middleware) {
output = middleware.handler(request, response, iterator);
} else {
output = this.handler(request, response);
}
// Determine if a Promise instance was returned by the handler
if (typeof output?.then === 'function') {
// If this is a middleware, we must try to call iterator after returned promise resolves
if (middleware) output.then(iterator);
// Catch and pipe any errors to the global error handler
output.catch((error) => response.throw(error));
}
} catch (error) {
// Catch and pipe any errors to the error handler
response.throw(error);
}
}
}
/**
* Compiles the route's internal components and caches for incoming requests.
*/
compile() {
// Initialize a fresh array of middlewares
const middlewares = [];
const pattern = this.pattern;
// Determine wildcard properties about this route
const is_wildcard = pattern.endsWith('*');
const wildcard_path = pattern.substring(0, pattern.length - 1);
// Iterate through the global/local middlewares and connect them to this route if eligible
const app_middlewares = this.app.middlewares;
Object.keys(app_middlewares).forEach((pattern) =>
app_middlewares[pattern].forEach((middleware) => {
// A route can be a direct child when a route's pattern has more path depth than the middleware with a matching start
// A route can be an indirect child when it is a wildcard and the middleware's pattern is a direct parent of the route child
const direct_child = pattern.startsWith(middleware.pattern);
const indirect_child = middleware.pattern.startsWith(wildcard_path);
if (direct_child || (is_wildcard && indirect_child)) {
// Create shallow copy of the middleware
const record = Object.assign({}, middleware);
// Set the match property based on whether this is a direct child
record.match = direct_child;
// Push the middleware
middlewares.push(record);
}
})
);
// Find the largest ID from the current middlewares
const offset = middlewares.reduce((max, middleware) => (middleware.id > max ? middleware.id : max), 0);
// Push the route-specific middlewares to the array at the end
if (Array.isArray(this.options.middlewares))
this.options.middlewares.forEach((middleware) =>
middlewares.push({
id: this.id + offset,
pattern,
handler: middleware,
match: false, // Route-specific middlewares do not need to be matched
})
);
// Sort the middlewares by their id in ascending order
// This will ensure that middlewares are executed in the order they were registered throughout the application
middlewares.sort((a, b) => a.id - b.id);
// Write the middlewares property with the sorted array
this.options.middlewares = middlewares;
}
}
module.exports = Route;