@mdf.js/core
Version:
MMS - API Core - Common types, classes and functions
281 lines • 11.4 kB
JavaScript
"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.
*
* 
*
* - 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