UNPKG

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
"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;