UNPKG

@microfleet/core

Version:
444 lines 16.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Microfleet = exports.routerExtension = exports.ConnectorsPriority = exports.ConnectorsTypes = exports.PluginsPriority = exports.PluginTypes = exports.PLUGIN_STATUS_FAIL = exports.PLUGIN_STATUS_OK = exports.PluginHealthStatus = void 0; /** * Microservice Abstract Class * @module Microfleet */ const path_1 = require("path"); const assert_1 = require("assert"); const eventemitter3_1 = require("eventemitter3"); const defaultOpts = __importStar(require("./defaults")); const validation_1 = require("@microfleet/validation"); const pluginHealthStatus_1 = require("./utils/pluginHealthStatus"); Object.defineProperty(exports, "PluginHealthStatus", { enumerable: true, get: function () { return pluginHealthStatus_1.PluginHealthStatus; } }); const utils_1 = require("@microfleet/utils"); const debug_1 = require("debug"); const toArray = (x) => Array.isArray(x) ? x : [x]; const debug = (0, debug_1.debug)('@microfleet:core'); const kRegisterCalled = Symbol('@microfleet::registerCalled'); var utils_2 = require("@microfleet/utils"); Object.defineProperty(exports, "PLUGIN_STATUS_OK", { enumerable: true, get: function () { return utils_2.PLUGIN_STATUS_OK; } }); Object.defineProperty(exports, "PLUGIN_STATUS_FAIL", { enumerable: true, get: function () { return utils_2.PLUGIN_STATUS_FAIL; } }); Object.defineProperty(exports, "PluginTypes", { enumerable: true, get: function () { return utils_2.PluginTypes; } }); Object.defineProperty(exports, "PluginsPriority", { enumerable: true, get: function () { return utils_2.PluginsPriority; } }); Object.defineProperty(exports, "ConnectorsTypes", { enumerable: true, get: function () { return utils_2.ConnectorsTypes; } }); Object.defineProperty(exports, "ConnectorsPriority", { enumerable: true, get: function () { return utils_2.ConnectorsPriority; } }); /** * Helper method to enable router extensions. * @param name - Pass extension name to require. * @returns Extension to router plugin. */ const routerExtension = (name) => { // eslint-disable-next-line @typescript-eslint/no-var-requires return require(require.resolve(`./plugins/router/extensions/${name}`)).default; }; exports.routerExtension = routerExtension; function resolveModule(cur, path) { if (cur != null) { return cur; } try { return require(require.resolve(path)); } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') { // eslint-disable-next-line no-console console.warn(e); } return null; } } /** * @class Microfleet */ class Microfleet extends eventemitter3_1.EventEmitter { static version = (0, utils_1.getVersion)(); config; version; migrators; plugins; [utils_1.CONNECTORS_PROPERTY]; [utils_1.DESTRUCTORS_PROPERTY]; [utils_1.HEALTH_CHECKS_PROPERTY]; connectorToPlugin; [kRegisterCalled]; /** * @param [opts={}] - Overrides for configuration. * @returns Instance of microservice. */ constructor(opts) { super(); // init configuration this.config = (0, utils_1.defaultsDeep)(opts, defaultOpts); this.exit = this.exit.bind(this); this.version = Microfleet.version; // init migrations this.migrators = Object.create(null); this.connectorToPlugin = new Map(); // init health status checkers this[utils_1.HEALTH_CHECKS_PROPERTY] = []; // init plugins this.plugins = []; this[utils_1.CONNECTORS_PROPERTY] = Object.create(null); this[utils_1.DESTRUCTORS_PROPERTY] = Object.create(null); this[kRegisterCalled] = false; for (const pluginType of utils_1.PluginsPriority) { this[utils_1.CONNECTORS_PROPERTY][pluginType] = []; this[utils_1.DESTRUCTORS_PROPERTY][pluginType] = []; } // setup error listener this.on('error', this.onError); } /** * Initializes all plugins */ async register() { if (this[kRegisterCalled]) { throw new Error('register() has already been called'); } await this.initPlugins(this.config); // setup hooks for (const [eventName, hooks] of Object.entries(this.config.hooks)) { for (const hook of toArray(hooks)) { this.on(eventName, hook); } } if (this.config.sigterm) { this.on('ready', () => { process.once('SIGTERM', this.exit); process.once('SIGINT', this.exit); }); } this[kRegisterCalled] = true; } /** * Asyncronously calls event listeners * and waits for them to complete. * This is a bit odd compared to normal event listeners, * but works well for dynamically running async actions and waiting * for them to complete. * * @param event - Hook name to be called during execution. * @param args - Arbitrary args to pass to the hooks. * @returns Result of invoked hook. */ async hook(event, ...args) { const listeners = this.listeners(event); const work = []; for (const listener of listeners.values()) { work.push(listener.apply(this, args)); } return Promise.all(work); } /** * Adds migrators. * @param name - Migrator name. * @param fn - Migrator function to be invoked. * @param args - Arbitrary args to be passed to fn later on. */ addMigrator(name, fn, ...args) { this.migrators[name] = (...migratorArgs) => fn.call(this, ...args, ...migratorArgs); } /** * Performs migration for a given database or throws if migrator is not present. * @param name - Name of the migration to invoke. * @param args - Extra args to pass to the migrator. * @returns Result of the migration. */ migrate(name, ...args) { const migrate = this.migrators[name]; (0, assert_1.strict)(typeof migrate === 'function', `migrator ${name} not defined`); return migrate(...args); } /** * Generic connector for all of the plugins. * @returns Walks over registered connectors and emits ready event upon completion. */ async connect() { if (!this[kRegisterCalled]) { await this.register(); } return this.processAndEmit(this.getConnectors(), 'ready', utils_1.ConnectorsPriority); } /** * Generic cleanup function. * @returns Walks over registered destructors and emits close event upon completion. */ async close() { const r = await this.processAndEmit(this.getDestructors(), 'close', [...utils_1.ConnectorsPriority].reverse()); if (this.config.sigterm) { process.removeListener('SIGTERM', this.exit); process.removeListener('SIGINT', this.exit); } return r; } // ****************************** Plugin section: public ************************************ /** * Public function to init plugins. * * @param mod - Plugin module instance. * @param mod.name - Plugin name. * @param mod.attach - Plugin attach function. * @param [conf] - Configuration in case it's not present in the core configuration object. */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async initPlugin(mod, conf) { const pluginName = mod.name; debug('initializing', pluginName); let expose; try { const configuration = conf || this.config[mod.name] || Object.create(null); // Temporary workaround while we have bundled schemas if (pluginName === 'validator') { if (!configuration.schemas) { configuration.schemas = []; } configuration.schemas.push((0, path_1.resolve)(__dirname, '../schemas')); } expose = await mod.attach.call(this, configuration, __filename); } catch (e) { if (e.constructor === validation_1.HttpStatusError) { e.message = `[@microfleet/core] Could not attach ${mod.name}:\n${e.message}`; } throw e; } this.plugins.push(pluginName); if (typeof expose !== 'object' || expose == null) { return; } const { connect, status, close } = expose; const type = utils_1.ConnectorsTypes[mod.type]; (0, assert_1.strict)(type, 'Plugin type must be equal to one of connectors type'); if (typeof connect === 'function') { this.addConnector(type, connect, pluginName); } if (typeof close === 'function') { this.addDestructor(type, close, pluginName); } if (typeof status === 'function') { this.addHealthCheck(new pluginHealthStatus_1.PluginHealthCheck(mod.name, status)); } } /** * Returns registered connectors. * @returns Connectors. */ getConnectors() { return this[utils_1.CONNECTORS_PROPERTY]; } /** * Returns registered destructors. * @returns Destructors. */ getDestructors() { return this[utils_1.DESTRUCTORS_PROPERTY]; } /** * Returns registered health checks. * @returns Health checks. */ getHealthChecks() { return this[utils_1.HEALTH_CHECKS_PROPERTY]; } /** * Initializes connectors on the instance of Microfleet. * @param type - Connector type. * @param handler - Plugin connector. * @param plugin - name of the plugin, optional. */ addConnector(type, handler, plugin) { this.addHandler(utils_1.CONNECTORS_PROPERTY, type, handler, plugin); } /** * Initializes destructor on the instance of Microfleet. * @param type - Destructor type. * @param handler - Plugin destructor. * @param plugin - name of the plugin, optional. */ addDestructor(type, handler, plugin) { this.addHandler(utils_1.DESTRUCTORS_PROPERTY, type, handler, plugin); } /** * Initializes plugin health check. * @param {Function} handler - Health check function. */ addHealthCheck(handler) { this[utils_1.HEALTH_CHECKS_PROPERTY].push(handler); } /** * Asks for health status of registered plugins if it's possible, logs it and returns summary. */ getHealthStatus() { return pluginHealthStatus_1.getHealthStatus.call(this, this.getHealthChecks(), this.config.healthChecks); } hasPlugin(name) { return this.plugins.includes(name); } /** * Overrides SIG* events and exits cleanly. * @returns Resolves when exit sequence has completed. */ async exit() { this.log.info('received close signal... closing connections...'); let timeout = null; try { await Promise.race([ this.close(), new Promise((_, reject) => { timeout = setTimeout(reject, 30000, new Error('failed to close after 30 seconds')); timeout.unref(); }) ]); } catch (err) { this.log.error({ err }, 'Unable to shutdown'); process.exit(128); } if (timeout) clearTimeout(timeout); this.log.info('close finished'); } /** * Helper for calling funcs and emitting event after. * * @param collection - Object with namespaces for arbitrary handlers. * @param event - Type of handlers that must be called. * @param [priority=Microfleet.ConnectorsPriority] - Order to process collection. * @returns Result of the invocation. */ async processAndEmit(collection, event, priority = utils_1.ConnectorsPriority) { const responses = []; for (const connectorType of priority) { const connectors = collection[connectorType]; if (!connectors) { continue; } for (const handler of connectors) { const pluginName = this.connectorToPlugin.get(handler); if (this.log) { this.log.info({ pluginName, connectorType, event }, 'started'); } responses.push(await handler.call(this)); if (this.log) { this.log.info({ pluginName, connectorType, event }, 'completed'); } } } this.emit(event); return responses; } // ***************************** Plugin section: private ************************************** addHandler(property, type, handler, plugin) { if (this[property][type] === undefined) { this[property][type] = []; } if (property === utils_1.DESTRUCTORS_PROPERTY) { // reverse this[property][type].unshift(handler); } else { this[property][type].push(handler); } if (plugin) { this.connectorToPlugin.set(handler, plugin); } } /** * Initializes service plugi`ns. * @param {Object} config - Service plugins configuration. * @private */ async initPlugins(config) { // require all modules const plugins = []; for (const plugin of config.plugins) { const paths = (0, path_1.isAbsolute)(plugin) ? [plugin] : [`./plugins/${plugin}`, `@microfleet/plugin-${plugin}`]; // back-compatibility, should be removed when we redo initialization of plugins if (plugin === 'redisCluster') { paths.unshift('@microfleet/plugin-redis-cluster'); } else if (plugin === 'redisSentinel') { paths.unshift('@microfleet/plugin-redis-sentinel'); } const pluginModule = paths.reduce(resolveModule, null); if (pluginModule === null) { throw new Error(`failed to init ${plugin}`); } plugins.push(pluginModule); } // sort and ensure that they are attached based // on their priority plugins.sort(this.pluginComparator); // call the .attach function for (const plugin of plugins) { await this.initPlugin(plugin); } this.emit('init'); } pluginComparator(a, b) { const ap = utils_1.PluginsPriority.indexOf(a.type); const bp = utils_1.PluginsPriority.indexOf(b.type); // same plugin type, check priority if (ap === bp) { if (a.priority < b.priority) return -1; if (a.priority > b.priority) return 1; return 0; } // different plugin types, sort based on it if (ap < bp) return -1; return 1; } /** * Notifies about errors when no other listeners are present * by throwing them. * @param err - Error that was emitted by the service members. */ onError = (err) => { if (this.listeners('error').length > 1) { return; } throw err; }; } exports.Microfleet = Microfleet; //# sourceMappingURL=index.js.map