UNPKG

@microfleet/core

Version:
403 lines 14.5 kB
"use strict"; /** * Microservice Abstract Class * @module Microfleet */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Microfleet = exports.routerExtension = exports.PluginsPriority = exports.PluginTypes = exports.ConnectorsPriority = exports.ConnectorsTypes = exports.ActionTransport = exports.PluginHealthStatus = void 0; const assert_1 = require("assert"); const Bluebird = require("bluebird"); const EventEmitter = require("eventemitter3"); const is = require("is"); const constants = require("./constants"); const defaultOpts = 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 packageInfo_1 = require("./utils/packageInfo"); const defaults_deep_1 = require("./utils/defaults-deep"); const toArray = (x) => Array.isArray(x) ? x : [x]; __exportStar(require("./types"), exports); /** * Constants with possilble transport values * @memberof Microfleet */ exports.ActionTransport = constants.ActionTransport; /** * Constants with connect types to control order of service bootstrap * @memberof Microfleet */ exports.ConnectorsTypes = constants.ConnectorsTypes; /** * Default priority of connectors during bootstrap * @memberof Microfleet */ exports.ConnectorsPriority = constants.ConnectorsPriority; /** * Plugin Types * @memberof Microfleet */ exports.PluginTypes = constants.PluginTypes; /** * Plugin boot priority * @memberof Microfleet */ exports.PluginsPriority = constants.PluginsPriority; /** * Helper method to enable router extensions. * @param name - Pass extension name to require. * @returns Extension to router plugin. */ exports.routerExtension = (name) => { // eslint-disable-next-line @typescript-eslint/no-var-requires return require(require.resolve(`./plugins/router/extensions/${name}`)).default; }; /** * Healthcheck statuses */ var constants_1 = require("./constants"); Object.defineProperty(exports, "PLUGIN_STATUS_OK", { enumerable: true, get: function () { return constants_1.PLUGIN_STATUS_OK; } }); Object.defineProperty(exports, "PLUGIN_STATUS_FAIL", { enumerable: true, get: function () { return constants_1.PLUGIN_STATUS_FAIL; } }); 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.error(e); } return null; } } /** * @class Microfleet */ class Microfleet extends EventEmitter { /** * @param [opts={}] - Overrides for configuration. * @returns Instance of microservice. */ constructor(opts) { super(); /** * Notifies about errors when no other listeners are present * by throwing them. * @param err - Error that was emitted by the service members. */ this.onError = (err) => { if (this.listeners('error').length > 1) { return; } throw err; }; // init configuration this.config = defaults_deep_1.default(opts, defaultOpts); this.exit = this.exit.bind(this); // init migrations this.migrators = Object.create(null); this.connectorToPlugin = new Map(); // init health status checkers this[constants.HEALTH_CHECKS_PROPERTY] = []; // init plugins this.plugins = []; this[constants.CONNECTORS_PROPERTY] = Object.create(null); this[constants.DESTRUCTORS_PROPERTY] = Object.create(null); // setup error listener this.on('error', this.onError); 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); }); } } /** * 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]; assert_1.strict(is.fn(migrate), `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() { return this.processAndEmit(this.getConnectors(), 'ready', exports.ConnectorsPriority); } /** * Generic cleanup function. * @returns Walks over registered destructors and emits close event upon completion. */ async close() { return this.processAndEmit(this.getDestructors(), 'close', [...exports.ConnectorsPriority].reverse()); } // ****************************** 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 initPlugin(mod, conf) { const pluginName = mod.name; let expose; try { expose = mod.attach.call(this, conf || this.config[mod.name], __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 (!is.object(expose)) { return; } const { connect, status, close } = expose; const type = exports.ConnectorsTypes[mod.type]; 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[constants.CONNECTORS_PROPERTY]; } /** * Returns registered destructors. * @returns Destructors. */ getDestructors() { return this[constants.DESTRUCTORS_PROPERTY]; } /** * Returns registered health checks. * @returns Health checks. */ getHealthChecks() { return this[constants.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(constants.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(constants.DESTRUCTORS_PROPERTY, type, handler, plugin); } /** * Initializes plugin health check. * @param {Function} handler - Health check function. */ addHealthCheck(handler) { this[constants.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...'); try { await Promise.race([ this.close(), Bluebird.delay(10000).throw(new Bluebird.TimeoutError('failed to close after 10 seconds')), ]); } catch (e) { this.log.error({ error: e }, 'Unable to shutdown'); process.exit(128); } } /** * 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 = exports.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 === constants.DESTRUCTORS_PROPERTY) { // reverse this[property][type].unshift(handler); } else { this[property][type].push(handler); } if (plugin) { this.connectorToPlugin.set(handler, plugin); } } /** * Initializes service plugins. * @param {Object} config - Service plugins configuration. * @private */ initPlugins(config) { for (const pluginType of exports.PluginsPriority) { this[constants.CONNECTORS_PROPERTY][pluginType] = []; this[constants.DESTRUCTORS_PROPERTY][pluginType] = []; } // require all modules const plugins = []; for (const plugin of config.plugins) { const paths = [`./plugins/${plugin}`, `@microfleet/plugin-${plugin}`]; 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) { this.initPlugin(plugin); } this.emit('init'); } pluginComparator(a, b) { const ap = exports.PluginsPriority.indexOf(a.type); const bp = exports.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; } } exports.Microfleet = Microfleet; constants.CONNECTORS_PROPERTY, constants.DESTRUCTORS_PROPERTY, constants.HEALTH_CHECKS_PROPERTY; Microfleet.version = packageInfo_1.getVersion(); // if there is no parent module we assume it's called as a binary if (!module.parent) { const mservice = new Microfleet({ name: 'cli' }); mservice .connect() .catch((err) => { mservice.log.fatal('Failed to start service', err); setImmediate(() => { throw err; }); }); } //# sourceMappingURL=index.js.map