rexuws
Version:
An express-like framework built on top of uWebsocket.js aims at simple codebase and high performance
368 lines (367 loc) • 15.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const uWebSockets_js_1 = require("uWebSockets.js");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const Request_1 = __importDefault(require("./Request"));
const Response_1 = __importDefault(require("./Response"));
const middlewares_1 = require("./middlewares");
const types_1 = require("./utils/types");
const utils_1 = require("./utils/utils");
const symbol_1 = require("./utils/symbol");
const router_1 = require("./router");
class App extends router_1.AbstractRoutingParser {
constructor(options) {
super();
this.#globalMiddlewares = [];
this.#errorMiddlewares = [];
this.#renderMethod = null;
this.#compileMethod = null;
this.#compiledViewCaches = {};
this.#appOptions = options;
if (!this.#appOptions.useDefaultParser) {
this.#appOptions.useDefaultParser = true;
}
this.#logger = options.logger;
this.#checkHasAsync = utils_1.hasAsync(this.#logger);
}
#appOptions;
#logger;
#app;
#token;
#globalMiddlewares;
#errorMiddlewares;
#checkHasAsync;
#renderMethod;
#compileMethod;
#compiledViewCaches;
#viewDir;
#extName;
#nativeHandlers;
/**
* @override
* @param method
* @param path
* @param middlewares
* @param baseUrl
*/
add(method, args) {
const [path, ...middlewares] = args;
const { path: cleanedPath, parametersMap, basePath } = utils_1.extractParamsPath(path.startsWith('/') ? path : `/${path}`);
const exist = this.routeHandlers.get(method + basePath);
if (exist)
this.#logger.warn(`There's already a route handler for ${method.toUpperCase()} ${cleanedPath} (original path: ${exist.originPath}), the existed one will be overrided!`);
this.routeHandlers.set(method + basePath, {
method,
middlewares,
originPath: path,
parametersMap,
hasAsync: middlewares.some(this.#checkHasAsync),
path: cleanedPath,
});
this.#logger.info('Map', method.toUpperCase(), path, '=>', method.toUpperCase(), cleanedPath);
return this;
}
useNativeHandlers(fn) {
this.#nativeHandlers = fn;
}
publish(topic, message, isBinary, compress) {
this.#app?.publish(topic, message, isBinary, compress);
}
use(pathOrMiddleware, router) {
if (typeof pathOrMiddleware === 'string') {
if (!router)
throw new TypeError('app.use(path, router) missing Router');
if (!(router instanceof router_1.BaseRouter)) {
throw new TypeError('Router must be an instance of AbstractRoutingParser');
}
if (router instanceof router_1.DefaultRouter) {
// check for PrefixRouter ins BaseRouter
const prefixRouter = router.getPrefixRouter();
if (prefixRouter && prefixRouter instanceof router_1.PrefixRouter)
prefixRouter
.getRouteHandlers()
.forEach(({ method, middlewares, path }) => {
this.add(method, [
`${pathOrMiddleware}/${path}`,
middlewares,
pathOrMiddleware,
]);
});
}
const handlers = router.getRouteHandlers();
handlers.forEach(({ method, middlewares, path }) => {
this.add(method, [
`${pathOrMiddleware}/${path}`,
middlewares,
pathOrMiddleware,
]);
});
return this;
}
if (utils_1.getParamNames(pathOrMiddleware).length === 4) {
this.#errorMiddlewares.push(pathOrMiddleware);
}
else
this.#globalMiddlewares.push(pathOrMiddleware);
return this;
}
listen(...args) {
this.initUWS();
this.initRoutes();
const argsCt = [...args];
const givenLength = argsCt.length;
if (!this.#app) {
throw new Error("Couldn't find uWS instance");
}
if (givenLength > 3)
throw new TypeError('Invalid listen arguments ');
if (givenLength === 3 && typeof argsCt[2] !== 'function')
throw new TypeError('Callback must be a function');
if (typeof argsCt[givenLength - 1] === 'function') {
const customCb = argsCt[givenLength - 1];
// Pass user-defined callback into setToken callback to make sure the callback will be called after
// the server successfully started
argsCt[givenLength - 1] = this.setToken(customCb);
}
else {
// By default print out the success message
argsCt[givenLength] = this.setToken(() => {
this.#logger.print('Listening on', args[0], givenLength === 2 ? argsCt[1] : '', '...');
});
}
this.#app.listen(...argsCt);
}
/**
* Save server token to perform graceful shutdown by `app.close()`
*/
setToken(cb) {
return (token) => {
if (!token)
throw new Error('Something went wrong with the server');
this.#token = token;
if (cb)
cb();
};
}
initUWS() {
if (this.#appOptions?.uWSConfigurations) {
this.#app = uWebSockets_js_1.SSLApp(this.#appOptions?.uWSConfigurations);
}
else {
this.#app = uWebSockets_js_1.App();
}
}
initRoutes() {
if (!this.#app) {
throw new Error("Couldn't find uWS instance");
}
if (this.#nativeHandlers) {
this.#logger.warn('All uWS native handlers will be mounted first');
this.#nativeHandlers(this.#app);
}
const globalAsync = this.#appOptions.forceAsync ||
this.#globalMiddlewares.some(this.#checkHasAsync);
// Push default ErrorMiddleware
this.#errorMiddlewares.push(middlewares_1.errorMiddleware({
logMethod: 'trace',
logger: this.#logger,
preferJSON: !!this.#appOptions.preferJSON,
}));
let hasAnyMethodOnAll = false;
const { useDefaultParser } = this.#appOptions;
const useDefaultCookieParser = (typeof useDefaultParser === 'boolean' && useDefaultParser) ||
(typeof useDefaultParser === 'object' && useDefaultParser.cookieParser);
this.routeHandlers.forEach((v) => {
const { method, middlewares, hasAsync, parametersMap, path, baseUrl } = v;
if (!hasAnyMethodOnAll && method === types_1.HttpMethod.ANY && path === '/*') {
hasAnyMethodOnAll = true;
}
const mergedMiddlewares = [...this.#globalMiddlewares, ...middlewares];
if (!this.#app) {
throw new Error("Couldn't find uWS instance");
}
if (method === types_1.HttpMethod.PATCH ||
method === types_1.HttpMethod.PUT ||
method === types_1.HttpMethod.POST) {
if (useDefaultParser) {
if (typeof useDefaultParser === 'object') {
if (useDefaultParser.multipartParser)
mergedMiddlewares.unshift(middlewares_1.multipartParser);
if (useDefaultParser.bodyParser) {
mergedMiddlewares.unshift(middlewares_1.bodyParser);
}
}
else {
mergedMiddlewares.unshift(middlewares_1.multipartParser);
mergedMiddlewares.unshift(middlewares_1.bodyParser);
}
}
this.#app[method](path, (_res, _req) => {
const req = new Request_1.default(_req, {
paramsMap: parametersMap,
forceInit: true,
cookieParser: useDefaultCookieParser,
baseUrl,
});
const res = new Response_1.default(_res, { hasAsync: true }, this.#logger);
req[symbol_1.FROM_RES] = {
getProxiedRemoteAddressAsText: res[symbol_1.GET_PROXIED_ADDR],
getRemoteAddressAsText: res[symbol_1.GET_REMOTE_ADDR],
};
res[symbol_1.FROM_APP] = {
render: this.render.bind(this),
};
utils_1.readBody(res.originalRes, (raw) => {
req.raw = raw.byteLength === 0 ? undefined : raw;
mergedMiddlewares[0](req, res, this.nextHandler(1, req, res, mergedMiddlewares));
});
});
}
else {
this.#app[method](path, (_res, _req) => {
const res = new Response_1.default(_res, {
hasAsync: globalAsync || hasAsync,
}, this.#logger);
const req = new Request_1.default(_req, {
paramsMap: parametersMap,
cookieParser: useDefaultCookieParser,
baseUrl,
forceInit: globalAsync || hasAsync || undefined,
});
req[symbol_1.FROM_RES] = {
getProxiedRemoteAddressAsText: res[symbol_1.GET_PROXIED_ADDR],
getRemoteAddressAsText: res[symbol_1.GET_REMOTE_ADDR],
};
res[symbol_1.FROM_REQ] = {
get: req.get.bind(req),
};
res[symbol_1.FROM_APP] = {
render: this.render.bind(this),
};
mergedMiddlewares[0](req, res, this.nextHandler(1, req, res, mergedMiddlewares));
});
}
});
// Add not found handler
if (!hasAnyMethodOnAll) {
const notFoundMiddlewares = [
...this.#globalMiddlewares,
middlewares_1.notFoundMiddleware({
logMethod: 'info',
logger: this.#logger,
preferJSON: !!this.#appOptions.preferJSON,
}),
];
this.#app.any('/*', (_res, _req) => {
const res = new Response_1.default(_res, {
hasAsync: globalAsync,
}, this.#logger);
const req = new Request_1.default(_req, {
paramsMap: [],
});
// const req = _req as any;
req[symbol_1.FROM_RES] = {
getProxiedRemoteAddressAsText: res[symbol_1.GET_PROXIED_ADDR],
getRemoteAddressAsText: res[symbol_1.GET_REMOTE_ADDR],
};
notFoundMiddlewares[0](req, res, this.nextHandler(1, req, res, notFoundMiddlewares));
});
}
}
nextHandler(nextIdx, req, res, middlewares, errorIdx = 0) {
return (error) => {
if (error) {
if (errorIdx >= middlewares.length)
return;
this.#errorMiddlewares[errorIdx](error, req, res, this.nextHandler(-1, req, res, this.#errorMiddlewares, errorIdx + 1));
return;
}
if (nextIdx >= middlewares.length)
return;
middlewares[nextIdx](req, res, this.nextHandler(nextIdx + 1, req, res, middlewares));
};
}
render(name, options, callback) {
if (!this.#viewDir) {
this.#logger.warn('No default view directory or renderMethod or compileMethod was found. Please configurate via app.setView(path, engineOptions)');
return callback(new Error("Something went wrong with the server's configurations"));
}
const viewFile = path_1.default.join(this.#viewDir, name + this.#extName);
if (this.#compileMethod) {
const cacheView = this.#compiledViewCaches[name];
if (cacheView) {
try {
const html = cacheView(options);
return callback(null, html);
}
catch (err) {
return callback(err);
}
}
try {
// Read file from disk
const fileContent = fs_1.default.readFileSync(viewFile, 'utf-8');
const compiled = this.#compileMethod(fileContent, options);
this.#compiledViewCaches[name] = compiled;
return callback(null, compiled(options));
}
catch (err) {
return callback(err);
}
}
// Handle renderMethod()
if (this.#renderMethod) {
return this.#renderMethod(viewFile, options, callback);
}
this.#logger.warn('No default renderMethod or compileMethod was found. Please set up via app.setView(path, engineOptions)');
return callback(new Error("Something went wrong with the server's configurations"));
}
setView(viewPath, engine) {
if (typeof viewPath !== 'string')
throw new TypeError('path must be a string');
this.#viewDir = viewPath;
if (typeof engine !== 'object')
throw new TypeError('Missing engine');
const { renderMethod, compileMethod, extName } = engine;
if (!extName) {
throw new TypeError('extName must be a string');
}
this.#extName = extName.startsWith('.') ? extName : `.${extName}`;
if (renderMethod) {
if (renderMethod.constructor.name !== 'Function')
throw new TypeError('renderMethod must be a function');
this.#renderMethod = renderMethod;
return this;
}
if (compileMethod) {
if (compileMethod.constructor.name !== 'Function')
throw new TypeError('renderMethod must be a function');
this.#compileMethod = compileMethod;
return this;
}
throw new TypeError('Neither renderMethod nor compileMethod has been given');
}
config(options) {
if (!this.#appOptions) {
this.#appOptions = options;
return this;
}
this.#appOptions = { ...this.#appOptions, ...options };
return this;
}
close(cb) {
if (!this.#app) {
throw new Error("uWS App hasn't been instanciated");
}
uWebSockets_js_1.us_listen_socket_close(this.#token);
if (cb)
cb();
else
this.#logger.print('Thanks for using the app');
}
}
exports.default = App;