UNPKG

@xarc/fastify-server

Version:
376 lines 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.fastify = void 0; exports.electrodeServer = electrodeServer; const tslib_1 = require("tslib"); /* eslint-disable no-magic-numbers, prefer-template, max-len, @typescript-eslint/no-var-requires */ const assert_1 = tslib_1.__importDefault(require("assert")); const fastify_1 = tslib_1.__importDefault(require("fastify")); const lodash_1 = tslib_1.__importDefault(require("lodash")); const path_1 = tslib_1.__importDefault(require("path")); const check_node_env_1 = require("./check-node-env"); const start_failed_1 = require("./start-failed"); const electrode_confippet_1 = require("electrode-confippet"); const AsyncEventEmitter = require("async-eventemitter"); const requireAt = require("require-at"); const xaa = require("xaa"); const util = require("util"); const { fastifyPluginDecorate } = require("./fastify-plugin-decorate"); const DEFAULT_KEEPALIVE_TIMEOUT = 60000; async function emitEvent(context, event) { const timeout = lodash_1.default.get(context, "config.electrode.eventTimeout", 10000); if (!context.emitter._events[event]) { return; } let promise = new Promise((resolve, reject) => { context.emitter.emit(event, context, err => { // @ts-expect-error ts-migrate(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message return err ? reject(err) : resolve(); }); }); if (Number.isInteger(timeout) && timeout > 0) { promise = xaa.runTimeout(promise, timeout); } try { await promise; } catch (error) { const massageError = err => { err.timeout = timeout; err.event = event; if (err instanceof xaa.TimeoutError) { err.message = `timeout waiting for event '${event}' handler`; err.code = "XEVENT_TIMEOUT"; } else { err.message = `event '${event}' handler failed: ${err.message}`; err.code = "XEVENT_FAILED"; } return err; }; throw massageError(error); } } async function convertPluginsToArray(plugins) { // // The module could either be one in node_modules or a file in a path // relative to CWD // * module in node_modules: no leading "." // * file in a directory, relative path with leading "." under CWD, resolve // full path for require // const fullRequirePath = x => { return x.startsWith(".") ? path_1.default.resolve(x) : x; }; const topRequireFromPath = plugins.requireFromPath; (0, assert_1.default)(!topRequireFromPath || lodash_1.default.isString(topRequireFromPath), `config.plugins.requireFromPath must be a string`); const loadModule = async (p) => { if (p.register) { return fastifyPluginDecorate(p); } // if p.register is not defined then check p.module // if no p.module or p.module !== false, then use field name (p.__name) const getPluginModule = () => { const requireFromPath = p.requireFromPath; if (lodash_1.default.isString(p.module)) { return { name: p.module, requireFromPath }; } else if (lodash_1.default.isObject(p.module)) { (0, assert_1.default)(p.module.name, `plugin ${p.__name} 'module' must have 'name' field`); (0, assert_1.default)(!p.module.requireFromPath || lodash_1.default.isString(p.module.requireFromPath), `plugin ${p.__name} 'module.requireFromPath' must be a string`); return Object.assign({ requireFromPath }, p.module); } else if (p.module !== false) { return { name: p.__name, requireFromPath }; } throw new Error(`plugin ${p.__name} disable 'module' but has no 'register' field`); }; const doRequire = async () => { const pluginMod = getPluginModule(); let name; let mod = null; const requireMod = () => { // // if has a name for the module to load, then try to load it. // let fromPath = pluginMod.requireFromPath || topRequireFromPath; let xrequire; if (fromPath) { name = pluginMod.name; // use require-at to load the module from path xrequire = requireAt(fromPath); p.requireFromPath = fromPath; fromPath = ` from path: ${fromPath}`; } else { // use require to load the module xrequire = require; name = fullRequirePath(pluginMod.name); fromPath = p.requireFromPath = ""; } try { mod = xrequire(name); } catch (error) { error.message = `Failed loading module ${pluginMod.name}${fromPath}: ${error.message}`; throw error; } }; requireMod(); // order of fields to look for the hapi plugin from the module: // 1. mod.fastifyPlugin // 3. mod.default.fastifyPlugin (ES6) // 2. mod.plugin // 4. mod.default (ES6) // 5. mod const pluginField = ["fastifyPlugin", "default.fastifyPlugin", "plugin", "default"].find(x => lodash_1.default.get(mod, x)); // validate plugin const pluginExport = (pluginField && lodash_1.default.get(mod, pluginField)) || mod; p.register = pluginExport.register || pluginExport; const msg = `for plugin '${p.__name}' from exported field '${pluginField}' of its module from '${name}'`; const validatePlugin = pos => { (0, assert_1.default)(pos.register, `register of plugin is falsy value: ${pos.register} - ${msg}`); (0, assert_1.default)(typeof pos.register === "function", `register of plugin is not a function - ${msg}`); }; validatePlugin(p); return fastifyPluginDecorate(p); }; return doRequire(); }; const num = x => { return lodash_1.default.isString(x) ? parseInt(x, 10) : x; }; const checkNaN = x => { return isNaN(x) ? Infinity : x; }; const priority = p => checkNaN(num(p.priority)); const isEnable = p => p.__name !== "requireFromPath" && p.enable !== false; const transpose = (p, k) => Object.assign({ __name: k }, p); // // transpose each plugin, filter out disabled ones, and sort by priority // const pluginsArray = () => (0, lodash_1.default)(plugins).map(transpose).filter(isEnable).sortBy(priority).value(); // convert plugins object to array and check each one if it has a module to load. const arr = pluginsArray(); return xaa.map(arr, loadModule); } async function startElectrodeServer(context) { const server = context.server; const config = context.config; let started = false; const registerPlugins = async (plugins) => { const errorRegisterMessage = plg => { if (plg.module) { const fromPath = plg.requireFromPath ? ` from path: '${plg.requireFromPath}'` : ""; return `with module '${JSON.stringify(plg.module)}'${fromPath}`; } else { return `with register function`; } }; const regFail = (err, plugin) => { let err2 = err; if (!err || !err.hasOwnProperty("message")) { err2 = new Error(err); } else if (err.code === "ERR_AVVIO_PLUGIN_TIMEOUT") { err2.message = `plugin '${plugin.__name}' with register function timeout \ - did you return a resolved promise?`; } err2.code = "XPLUGIN_FAILED"; err2.plugin = plugin; err2.method = errorRegisterMessage(plugin); return err2; }; // // - all register must be called before calling server.ready // - but registration only executed after server.ready is called // - So 1st call must setup all register calls and promises // - 2nd call calls after(), but actual execution wait for server.ready // - 3rd call wait for all registration to complete // const regPromises = plugins.map(async (plugin) => { try { const x = server.register(plugin.register, plugin.options); return await util.promisify(x.after)(); } catch (err) { throw regFail(err, plugin); } }); await Promise.all(regPromises); return emitEvent(context, "plugins-registered"); }; const handleFail = async (err) => { if (started) { await server.close(); } return await (0, start_failed_1.startFailed)(err); }; const startServer = async () => { try { // must call ready to kick off the plugin registration await context.server.ready(); await context.registerPluginsPromise; await server.listen({ port: config.connection.port, host: config.connection.address }); started = true; await emitEvent(context, "server-started"); await emitEvent(context, "complete"); } catch (err) { await handleFail(err); } }; const setupServer = async () => { context.server.decorate("start", startServer); await emitEvent(context, "server-created"); const plugins = await convertPluginsToArray(config.plugins); context.plugins = plugins; await emitEvent(context, "plugins-sorted"); context.registerPluginsPromise = registerPlugins(context.plugins); }; try { await setupServer(); } catch (err) { return await handleFail(err); } if (!context.config.deferStart) { await startServer(); } return server; } /** * A configuration base helper to create and start a HTTP server using fastify * * @param appConfig - configuration * @param decors - extra decorators to add to the server * * @returns electrode fastify instance */ async function electrodeServer(appConfig = {}, decors) { const check = () => { (0, check_node_env_1.checkNodeEnv)(); if (lodash_1.default.isArray(decors)) { decors = decors.filter(lodash_1.default.identity).map(x => (lodash_1.default.isFunction(x) ? x() : x)); } else { decors = [].concat(decors).filter(lodash_1.default.identity); } }; check(); const makeFastifyServerConfig = context => { const fastifyServerConfig = { app: { electrodeServer: true }, keepAliveTimeout: lodash_1.default.get(context.config, "keepAliveTimeout", lodash_1.default.get(context.config, "electrode.keepAliveTimeout", DEFAULT_KEEPALIVE_TIMEOUT)) }; electrode_confippet_1.util.merge(fastifyServerConfig, context.config.server); lodash_1.default.assign(fastifyServerConfig, context.config.connection); // // This will allow Fastify to make config available through // server.settings.app.config // fastifyServerConfig.app.config = context.config; return fastifyServerConfig; }; const start = async (context) => { const settings = makeFastifyServerConfig(context); const server = (context.server = (0, fastify_1.default)(settings)); const SYM_PATH = Symbol("request.path"); const SYM_INFO = Symbol("request.info"); // add request.path and request.info as compatibility with Hapi server.decorateRequest("path", { getter() { return this[SYM_PATH] || (this[SYM_PATH] = this.raw.url.match("^[^?]*")[0]); } }); // request.info server.decorateRequest("info", { getter() { return this[SYM_INFO] || (this[SYM_INFO] = { remoteAddress: this.ip }); } }); const SERVER_SYM_INFO = Symbol("server.info"); // server.info micmic Hapi /* const sample = { created: 1593114093676, started: 1593114093685, host: "m-c02yw1s0lvdt", port: 3000, protocol: "http", id: "m-c02yw1s0lvdt:53774:kbv6zxws", uri: "http://m-c02yw1s0lvdt:3000", address: "0.0.0.0" }; */ server.decorate("info", { getter() { return (this[SERVER_SYM_INFO] || (this[SERVER_SYM_INFO] = { get port() { const address = server.server.address(); return address && address.port; }, get address() { const address = server.server.address(); return address && address.address; } })); } }); server.decorate("settings", settings); // server.app server.decorate("app", { config: context.config }); const SYM_APP = Symbol("request.app"); // request.app, should be different for each request server.decorateRequest("app", { getter() { return this[SYM_APP] || (this[SYM_APP] = { config: context.config }); } }); // Start server return startElectrodeServer(context); }; const applyDecorConfigs = context => { // load internal defaults const defaults = (0, electrode_confippet_1.loadConfig)({ dir: path_1.default.join(__dirname, "config"), warnMissing: false, failMissing: false, context: { deployment: process.env.NODE_ENV }, cache: false }); // apply decors decors.forEach(d => defaults._$.use(d)); // apply appConfig defaults._$.use(appConfig); context.config = defaults; delete defaults.listener; return context; }; const setListeners = context => { context.emitter = new AsyncEventEmitter(); decors.forEach(d => { return d.listener && d.listener(context.emitter); }); if (appConfig.listener) { appConfig.listener(context.emitter); } return context; }; let ctx = setListeners({}); ctx = applyDecorConfigs(ctx); await emitEvent(ctx, "config-composed"); return await start(ctx); } var fastify_2 = require("fastify"); Object.defineProperty(exports, "fastify", { enumerable: true, get: function () { return tslib_1.__importDefault(fastify_2).default; } }); //# sourceMappingURL=electrode-server.js.map