@microfleet/core
Version:
Abstract microservice core
444 lines • 16.8 kB
JavaScript
"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 = `[ /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}`, ` /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