UNPKG

@mdf.js/core

Version:

MMS - API Core - Common types, classes and functions

281 lines 11.4 kB
"use strict"; /** * Copyright 2024 Mytra Control S.L. All rights reserved. * * Use of this source code is governed by an MIT-style license that can be found in the LICENSE file * or at https://opensource.org/licenses/MIT. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Manager = void 0; const tslib_1 = require("tslib"); const crash_1 = require("@mdf.js/crash"); const logger_1 = require("@mdf.js/logger"); const utils_1 = require("@mdf.js/utils"); const events_1 = require("events"); const joi_1 = tslib_1.__importStar(require("joi")); const lodash_1 = require("lodash"); const uuid_1 = require("uuid"); const Health_1 = require("../../Health"); const states_1 = require("./states"); const types_1 = require("./types"); /** * Provider Manager wraps a specific port created by the extension of the {@link Port} abstract * class, instrumenting it with the necessary logic to manage: * * - The state of the provider, represented by the {@link Manager.state} property, and managed by * the {@link Manager.start}, {@link Manager.stop} and {@link Manager.fail} methods. * * ![class diagram](../../../media/Provider-States-Methods.png) * * - Merge and validate the configuration of the provider represented by the generic type * _**PortConfig**_. The manager configuration object {@link ProviderOptions} has a _**validation**_ * property that represent a structure of type {@link PortConfigValidationStruct} where default * values, environment based and a [Joi validation object](https://joi.dev/api/?v=17.7.0) are * defined. During the initialization process, the manager will merge all the sources of * configuration (default, environment and specific) and validate the result against the Joi schema. * So, the order of priority of the configuration sources is: specific, environment and default. * If the validation fails, the manager will use the default values and emit an error that will be * managed by the observability layer. * * @category Provider * * @param PortClient - Underlying client type, this is, the real client of the wrapped provider * @param PortConfig - Port configuration object, could be an extended version of the client config * @param T - Port class, this is, the class that extends the {@link Port} abstract class * @public */ class Manager extends events_1.EventEmitter { /** * Implementation of base functionalities of a provider manager * @param port - Port wrapper class * @param config - Port configuration options * @param options - Manager configuration options */ constructor(port, options, config) { super(); this.options = options; /** * Change the provider state * @param newState - state to which it transitions */ this.changeState = (newState) => { // Stryker disable next-line all this.logger.debug(`Changing state to ${newState.state}`); this._state = newState; this._date = new Date().toISOString(); if (this.listenerCount('status') > 0) { // Stryker disable next-line all this.logger.debug(`Emitting state change event to ${this.listenerCount('status')} listeners`); this.emit('status', types_1.ProviderStatus[this.state]); } return newState; }; /** * Manage the errors in the provider (logging, emitting, last error ...) * @param error - Error from wrapper instance */ this.manageError = (error) => { this._error = this.formatError(error); // Stryker disable all this.logger.error(`New error event from provider: ${this._error.message}`, this.componentId, this.options.name); this.logger.crash(this._error, this.options.name); // Stryker enable all if (this.listenerCount('error') > 0) { // Stryker disable all this.logger.debug(`Emitting error event to ${this.listenerCount('error')} listeners`, this.componentId, this.options.name); // Stryker enable all this.emit('error', this._error); } }; this.logger = this.options.logger || new logger_1.DebugLogger(this.options.name); this.config = this.validateConfig(config); try { this.port = new port(this.config, this.logger, this.options.name); } catch (error) { // Stryker disable next-line all this.logger.warn(`Error trying to create an instance of the port`, (0, uuid_1.v4)(), this.options.name); this.manageError(error); throw this._error; } this.componentId = this.port.uuid; this.logger = (0, logger_1.SetContext)(this.logger, this.options.name, this.componentId); this._date = new Date().toISOString(); this.port.on('error', error => { this.logger.error(`New error event from port: ${error.message}`, this.componentId); this.manageError(error); }); if (this._error) { this._state = this.changeState(new states_1.ErrorState(this.port, this.changeState, this.manageError)); } else { this._state = this.changeState(new states_1.StoppedState(this.port, this.changeState, this.manageError)); } } /** Return the errors in the provider */ get error() { return this._error; } /** Provider state */ get state() { return this._state.state; } /** * Return the status of the connection in a standard format * @returns _check object_ as defined in the draft standard * https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-05 */ get checks() { const checks = {}; for (const [measure, check] of Object.entries(this.port.checks)) { checks[`${this.options.name}:${measure}`] = check; } checks[`${this.options.name}:status`] = [ { status: types_1.ProviderStatus[this.state], componentId: this.componentId, componentType: this.options.type, observedValue: this.state, time: this.date, output: this.detailedOutput(), }, ]; return checks; } /** Provider status */ get status() { return (0, Health_1.overallStatus)(this.checks); } /** Port client */ get client() { return this.port.client; } /** Provider name */ get name() { return this.options.name; } /** Timestamp of actual state in ISO format, when the current state was reached */ get date() { return this._date; } /** Initialize the process: internal jobs, external dependencies connections ... */ async start() { return this._state.start(); } /** Stop the process: internal jobs, external dependencies connections ... */ async stop() { return this._state.stop(); } /** Close the provider: release resources, connections ... */ async close() { return this.port.close(); } /** * Error state: wait for new state of to fix the actual degraded stated * @param error - Cause ot this fail transition * @returns */ async fail(error) { return this._state.fail(error); } /** * Format the error to a manageable error format * @param error - error to be formatted * @returns */ formatError(error) { let formattedError; if (error instanceof joi_1.ValidationError) { if (this._error && this._error instanceof crash_1.Multi && this._error.findCauseByName('ValidationError')) { formattedError = this._error; } else { formattedError = new crash_1.Multi(`Error in the provider configuration process`, this.componentId); } formattedError.Multify(error); } else if (error instanceof crash_1.Crash || error instanceof crash_1.Multi) { formattedError = error; } else if (error instanceof Error) { formattedError = new crash_1.Crash(error.message, this.componentId); } else if (typeof error === 'string') { formattedError = new crash_1.Crash(error, this.componentId); } else if (error && typeof error === 'object' && typeof error['message'] === 'string') { formattedError = new crash_1.Crash(error['message']); } else { formattedError = new crash_1.Crash(`Unknown error in port ${this.options.name}`, this.componentId); } return formattedError; } /** * Manage the actual stored error (last error), to create a human readable output used in the * observability (SubcomponentDetail) */ detailedOutput() { if (this.state === 'error' && this._error) { return this._error.trace(); } else { return undefined; } } /** * Merge the configuration with the default values and environment values and perform the * validation of the new configuration against the schema * @param options - validation configuration options */ validateConfig(config) { let baseConfig; const actualConfig = this.mergeConfigSources(config); try { baseConfig = joi_1.default.attempt(actualConfig, this.options.validation.schema); // Stryker disable next-line all this.logger.info(`Configuration has been validated properly`); } catch (error) { // Stryker disable next-line all this.logger.warn(`Incorrect configuration, default configuration will be used`); this.manageError(error); try { baseConfig = joi_1.default.attempt(this.options.validation.defaultConfig, this.options.validation.schema); } catch (defaultError) { // Stryker disable next-line all this.logger.warn(`Default configuration is not valid too, nevertheless will be used ...`); this.manageError(defaultError); baseConfig = this.options.validation.defaultConfig; } } return baseConfig; } /** * Merge the environment configuration and the default configuration with the specific * configuration * @param config - specific configuration for the provider instances * @returns */ mergeConfigSources(config) { const defaultConfig = (0, lodash_1.cloneDeep)(this.options.validation.defaultConfig); let envConfig; if (typeof this.options.useEnvironment === 'boolean' && this.options.useEnvironment) { envConfig = this.options.validation.envBasedConfig; } else if (typeof this.options.useEnvironment === 'string') { envConfig = (0, utils_1.formatEnv)(this.options.useEnvironment); } else { envConfig = {}; } return (0, lodash_1.merge)(defaultConfig, envConfig, config); } } exports.Manager = Manager; //# sourceMappingURL=Manager.js.map