diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9d3cdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Ignore tmp files and folders +/tmp/* +!/log/.keep +!/tmp/.keep +/.idea +/.idea/* + +package-lock.json + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/cli/shrinkwrap#caveats +node_modules + +# Dev Folder +.dev + +# Debug log from npm +npm-debug.log +.vscode + +.DS_Store + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..cc284a1 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "none", + "bracketSpacing": true, + "arrowParens": "always", + "parser": "flow", + "proseWrap": "preserve" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..90ff4b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## Base v1.0.0 +- Base repository is created and all the common functionality which different modules need are moved to it. Example - Logger, response helper, promise context, promise queue manager. +- Log level support is introduced and non-important logs are moved to debug log level. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md index 3c4984d..4c84a28 100644 --- a/README.md +++ b/README.md @@ -1 +1,237 @@ -# base \ No newline at end of file +# Base provides advanced Promise Queue Manager, Custom Console Logger and other utilities. +![npm version](https://img.shields.io/npm/v/@plgworks/base.svg?style=flat) + +# Install + +```bash +npm install @plgworks/base --save +``` + +# PromiseQueueManager Usage +```js +const Base = require('@plgworks/base'), + logger = new Base.Logger("my_module_name"); + +const queueManagerOptions = { + // Specify the name for easy identification in logs. + name: "my_module_name_promise_queue" + + // resolvePromiseOnTimeout :: set this flag to false if you need custom handling. + // By Default, the manager will neither resolve nor reject the Promise on time out. + , resolvePromiseOnTimeout: false + // The value to be passed to resolve when the Promise has timedout. + , resolvedValueOnTimeout: null + + // rejectPromiseOnTimeout :: set this flag to true if you need custom handling. + , rejectPromiseOnTimeout : false + + // Pass timeoutInMilliSecs in options to set the timeout. + // If less than or equal to zero, timeout will not be observed. + , timeoutInMilliSecs: 5000 + + // Pass maxZombieCount in options to set the max acceptable zombie count. + // When this zombie promise count reaches this limit, onMaxZombieCountReached will be triggered. + // If less than or equal to zero, onMaxZombieCountReached callback will not triggered. + , maxZombieCount: 0 + + // Pass logInfoTimeInterval in options to log queue healthcheck information. + // If less than or equal to zero, healthcheck will not be logged. + , logInfoTimeInterval : 0 + + + , onPromiseResolved: function ( resolvedValue, promiseContext ) { + //onPromiseResolved will be executed when the any promise is resolved. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + logger.log(oThis.name, " :: a promise has been resolved. resolvedValue:", resolvedValue); + } + + , onPromiseRejected: function ( rejectReason, promiseContext ) { + //onPromiseRejected will be executed when the any promise is timedout. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + logger.log(oThis.name, " :: a promise has been rejected. rejectReason: ", rejectReason); + } + + , onPromiseTimedout: function ( promiseContext ) { + //onPromiseTimedout will be executed when the any promise is timedout. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + logger.log(oThis.name, ":: a promise has timed out.", promiseContext.executorParams); + } + + , onMaxZombieCountReached: function () { + //onMaxZombieCountReached will be executed when maxZombieCount >= 0 && current zombie count (oThis.zombieCount) >= maxZombieCount. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + logger.log(oThis.name, ":: maxZombieCount reached."); + + } + + , onPromiseCompleted: function ( promiseContext ) { + //onPromiseCompleted will be executed when the any promise is removed from pendingPromise queue. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + logger.log(oThis.name, ":: a promise has been completed."); + } + , onAllPromisesCompleted: function () { + //onAllPromisesCompleted will be executed when the last promise in pendingPromise is resolved/rejected. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + //Ideally, you should set this inside SIGINT/SIGTERM handlers. + + logger.log("Examples.allResolve :: onAllPromisesCompleted triggered"); + manager.logInfo(); + } +}; + + +const promiseExecutor = function ( resolve, reject, params, promiseContext ) { + //promiseExecutor + setTimeout(function () { + resolve( params.cnt ); // Try different things here. + }, 1000); +}; + +const manager = new Base.CustomPromise.QueueManager( promiseExecutor, queueManagerOptions); + +for( let cnt = 0; cnt < 5; cnt++ ) { + manager.createPromise( {"cnt": (cnt + 1) } ); +} + +``` + + +# Logger Usage +```js +const Base = require('@plgworks/base'), + Logger = Base.Logger, + logger = new Logger("my_module_name", Logger.LOG_LEVELS.TRACE); + +//Log Level FATAL +logger.notify("notify called"); + +//Log Level ERROR +logger.error("error called"); + +//Log Level WARN +logger.warn("warn called"); + +//Log Level INFO +logger.info("info Invoked"); +logger.step("step Invoked"); +logger.win("win called"); + +//Log Level DEBUG +logger.log("log called"); +logger.debug("debug called"); +logger.dir({ l1: { l2 : { l3Val: "val3", l3: { l4Val: { val: "val" }}} }}); + +//Log Level TRACE +logger.trace("trace called"); + + +``` +All methods will be available for use irrespective of configured log level. +Log Level only controls what needs to be logged. + +### Method to Log Level Map +| Method | Enabling | +| | Log Level | +| :----- | :-------- | +| notify | FATAL | +| error | ERROR | +| warn | WARN | +| info | INFO | +| step | INFO | +| win | INFO | +| debug | DEBUG | +| log | DEBUG | +| dir | DEBUG | +| trace | TRACE | + + +# Response formatter usage + +```js + +const rootPrefix = '.', + paramErrorConfig = require(rootPrefix + '/tests/mocha/lib/formatter/paramErrorConfig'), + apiErrorConfig = require(rootPrefix + '/tests/mocha/lib/formatter/apiErrorConfig'); + +const Base = require('@plgworks/base'), + ResponseHelper = Base.responseHelper, + responseHelper = new ResponseHelper({ + moduleName: 'companyRestFulApi' + }); + +//using error function +responseHelper.error({ + internal_error_identifier: 's_vt_1', + api_error_identifier: 'test_1', + debug_options: {id: 1234}, + error_config: { + param_error_config: paramErrorConfig, + api_error_config: apiErrorConfig + } +}); + +//using paramValidationError function +responseHelper.paramValidationError({ + internal_error_identifier:"s_vt_2", + api_error_identifier: "test_1", + params_error_identifiers: ["user_name_inappropriate"], + debug_options: {id: 1234}, + error_config: { + param_error_config: paramErrorConfig, + api_error_config: apiErrorConfig + } +}); + +// Result object is returned from responseHelper method invocations above, we can chain several methods as shown below + +responseHelper.error({ + internal_error_identifier: 's_vt_1', + api_error_identifier: 'invalid_api_params', + debug_options: {id: 1234}, + error_config: { + param_error_config: paramErrorConfig, + api_error_config: apiErrorConfig + } +}).isSuccess(); + +responseHelper.error({ + internal_error_identifier: 's_vt_1', + api_error_identifier: 'invalid_api_params', + debug_options: {id: 1234}, + error_config: { + param_error_config: paramErrorConfig, + api_error_config: apiErrorConfig + } +}).isFailure(); + +responseHelper.error({ + internal_error_identifier: 's_vt_1', + api_error_identifier: 'invalid_api_params', + debug_options: {id: 1234}, + error_config: { + param_error_config: paramErrorConfig, + api_error_config: apiErrorConfig + } +}).toHash(); +``` + +# Running test cases +```shell script +./node_modules/.bin/mocha --recursive "./tests/**/*.js" +``` diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/index.js b/index.js new file mode 100644 index 0000000..9a2f579 --- /dev/null +++ b/index.js @@ -0,0 +1,44 @@ +/** + * Index File for @plgworks/base + */ + +const rootPrefix = '.', + Logger = require(rootPrefix + '/lib/logger/CustomConsoleLogger'), + PromiseContext = require(rootPrefix + '/lib/promiseContext/PromiseContext'), + PCQueueManager = require(rootPrefix + '/lib/promiseContext/PromiseQueueManager'), + InstanceComposer = require(rootPrefix + '/lib/InstanceComposer'), + ResponseHelper = require(rootPrefix + '/lib/formatter/ResponseHelper'); + +// Expose all libs here. +// All classes should begin with Capital letter. +// All instances/objects should begin with small letter. +module.exports = { + logger: new Logger(), + Logger: Logger, + CustomPromise: { + Context: PromiseContext, + QueueManager: PCQueueManager + }, + responseHelper: ResponseHelper, + InstanceComposer: InstanceComposer +}; + +/* + Base = require("./index"); + + //Test Logger + logger = new Base.Logger("Test"); + logger.testLogger() + + //Test PromiseQueueManager + PQM = Base.CustomPromise.QueueManager; + + //Run these one by one. + PQM.Examples.allReject(); + + PQM.Examples.allResolve(); + PQM.Examples.allReject(); + PQM.Examples.allTimeout(); + PQM.Examples.executorWithParams(); + PQM.Examples.maxZombieCount(); +*/ diff --git a/lib/InstanceComposer.js b/lib/InstanceComposer.js new file mode 100644 index 0000000..0dcfd9e --- /dev/null +++ b/lib/InstanceComposer.js @@ -0,0 +1,167 @@ +/** + * @fileoverview - Functionality to share configStrategy between classes and objects. + */ + +const instanceComposerMethodName = 'ic'; +const composerMap = {}; +const shadowMap = {}; + +/** + * Get full getter name + * + * @param getterNamespace {string} - getter namespace + * @param getterName {string} - getter name + * + * @returns {string} - returns the full getter name + */ +const fullGetterName = function(getterNamespace, getterName) { + return getterNamespace + '.' + getterName; +}; + +/** + * Check if full getter name is available + * + * @param fullGetterName {string} - full getter name + * + * Throws error if fullGetterName is not available, i.e. already taken. + */ +function checkAvailability(fullGetterName) { + if (composerMap.hasOwnProperty(fullGetterName) || shadowMap.hasOwnProperty(fullGetterName)) { + console.trace('Duplicate Getter Method name', fullGetterName); + throw 'Duplicate Getter Method Name '; + } +} + +/** + * @class Instance Composer Class + */ +class InstanceComposer { + /** + * Constructor for Instance Composer Class + * + * @param configStrategy {object} - config strategy + */ + constructor(configStrategy) { + this.configStrategy = configStrategy || {}; + this.instanceMap = {}; + this.shadowedClassMap = {}; + } + + /** + * Get Instance for getter name space and getter name + * + * @param getterNamespace {string} - getter name space + * @param getterName {string} - getter name + * + * @returns {object} + */ + getInstanceFor(getterNamespace, getterName) { + const oThis = this; //this refers to instance of InstanceComposer. + + let _getterName = fullGetterName(getterNamespace, getterName); + let registryInfo = composerMap[_getterName]; + let ClassConstructor = registryInfo.c; + let mustRetainInstance = registryInfo.mustRetain; + + let _instance; + + if (mustRetainInstance) { + _instance = oThis.instanceMap[_getterName]; + if (!_instance) { + _instance = new ClassConstructor(oThis.configStrategy, oThis); + _instance[instanceComposerMethodName] = function() { + return oThis; + }; + oThis.instanceMap[_getterName] = _instance; + } + } else { + _instance = new ClassConstructor(oThis.configStrategy, oThis); + _instance[instanceComposerMethodName] = function() { + return oThis; + }; + } + + return _instance; + } + + /** + * Get Shadowed Class for getter name space and getter name + * + * @param getterNamespace {string} - getter name space + * @param getterName {string} - getter name + * + * @returns {*} + */ + getShadowedClassFor(getterNamespace, getterName) { + const oThis = this; //this refers to instance of InstanceComposer. + + let _getterName = fullGetterName(getterNamespace, getterName); + + let registryInfo = shadowMap[_getterName]; + let ClassConstructor = registryInfo.c; + + let _shadowedClass; + _shadowedClass = oThis.shadowedClassMap[_getterName]; + + if (!_shadowedClass) { + oThis.shadowedClassMap[_getterName] = _shadowedClass = oThis._createShadowClass(ClassConstructor); + } + + return _shadowedClass; + } + + /** + * Register as object + * + * @param ClassConstructor {function} - class constructor to use for creation of the object + * @param getterNamespace {string} - getter name space + * @param getterName {string} - getter name + * @param mustRetainInstance {boolean} - should the instance of the object be retained. + */ + static registerAsObject(ClassConstructor, getterNamespace, getterName, mustRetainInstance) { + let _getterName = fullGetterName(getterNamespace, getterName); + + checkAvailability(_getterName); + + composerMap[_getterName] = { c: ClassConstructor, mustRetain: mustRetainInstance }; + } + + /** + * Register as shadowable class + * + * @param ClassConstructor {function} - class constructor to use for creation of the shadowed class + * @param getterNamespace {string} - getter name space + * @param getterName {string} - getter name + */ + static registerAsShadowableClass(ClassConstructor, getterNamespace, getterName) { + let _getterName = fullGetterName(getterNamespace, getterName); + + checkAvailability(_getterName); + + shadowMap[_getterName] = { c: ClassConstructor }; + } + + /** + * create shadowable class + * + * @param ClassConstructor {function} - class constructor to use for creation of the shadowed class + * @private + */ + _createShadowClass(ClassConstructor) { + const oThis = this; //this refers to instance of InstanceComposer. + + class DerivedClass extends ClassConstructor { + constructor(...args) { + super(...args); + } + + [instanceComposerMethodName]() { + return oThis; + } + } + + return DerivedClass; + } +} + +module.exports = InstanceComposer; diff --git a/lib/formatter/ResponseHelper.js b/lib/formatter/ResponseHelper.js new file mode 100644 index 0000000..099e37d --- /dev/null +++ b/lib/formatter/ResponseHelper.js @@ -0,0 +1,412 @@ +/* + * Standard Response Formatter + */ + +const shortId = require('shortid'); + +const rootPrefix = '../..', + Logger = require(rootPrefix + '/lib/logger/CustomConsoleLogger'); + +const logger = new Logger('Base'); + +/** + * Result Class constructor + * + * @param {object} params - parameters object + * @param {string} params.success_data - external error code + * @param {object} params.error_config - optional debug params + * @param {object} params.error_config.api_error_config - api error info + * @param {object} params.error_config.param_error_config - param error info + * + * @return {result} + */ +const Result = function(params) { + const oThis = this; + + oThis.data = params.success_data || {}; + oThis.apiErrorIdentifier = params.api_error_identifier; + oThis.paramsErrorIdentifiers = params.params_error_identifiers; + oThis.internalErrorCode = params.internal_error_identifier || 'base_default'; + + params.error_config = params.error_config || {}; + oThis.paramErrorConfig = params.error_config.param_error_config || {}; + oThis.apiErrorConfig = params.error_config.api_error_config || {}; + oThis.debugOptions = params.debugOptions || {}; + + oThis.goTo = params.go_to || {}; + + oThis.success = typeof oThis.apiErrorIdentifier === 'undefined' || oThis.apiErrorIdentifier == null; +}; + +Result.prototype = { + /** + * method to check if the result is a success + * + * @returns {boolean} + */ + isSuccess: function() { + const oThis = this; + + return oThis.success; + }, + + /** + * method to check if the result is a failure + * + * @returns {boolean} + */ + isFailure: function() { + const oThis = this; + + return !oThis.isSuccess(); + }, + + /** + * Overriding inspect of console + * @param data + */ + + inspect: function(data) { + const oThis = this; + + let result = null; + + if (oThis.isSuccess()) { + logger.log('Formatted Result Object [SUCCESS]\n', oThis.toHash()); + result = oThis.toHash(); + } else { + logger.log('Formatted Result Object [FAILURE]\n', oThis.getDebugData()); + result = oThis.getDebugData(); + } + + return result; + }, + + /** + * converts the Result object to Hash object, + * which can be passed to outside code not having knowledge of Result + * + * @param errorConfig - this is config of eror - contains keys + * @param errorConfig.api_error_config - api error info + * @param errorConfig.param_error_config - param error info + * + * @returns {object} + */ + toHash: function(errorConfig) { + const oThis = this; + + errorConfig = errorConfig || {}; + + Object.assign(oThis.paramErrorConfig, errorConfig.param_error_config || {}); + Object.assign(oThis.apiErrorConfig, errorConfig.api_error_config || {}); + + const apiErrInfo = oThis._fetchApiErrorInfo(), + formattedData = {}; + + if (!oThis.success) { + formattedData.success = false; + formattedData.err = { + code: apiErrInfo.code, + msg: apiErrInfo.message, + error_data: oThis._fetchDetailedParamErrors(), + internal_id: oThis.internalErrorCode || 'base_error' + }; + + if (oThis.goTo && Object.keys(oThis.goTo).length) { + formattedData.go_to = oThis.goTo; + } + } else { + formattedData.success = true; + formattedData.data = oThis.data; + + if (oThis.goTo && Object.keys(oThis.goTo).length) { + formattedData.go_to = oThis.goTo; + } + } + + return formattedData; + }, + + /** + * converts the Result object to Hash object, + * which can be passed to outside code not having knowledge of Result + * + * @param errorConfig - this is config of eror - contains keys + * @param errorConfig.api_error_config - api error info + * @param errorConfig.param_error_config - param error info + * + * @returns {object} + */ + getDebugData: function(errorConfig) { + const oThis = this; + + errorConfig = errorConfig || {}; + + Object.assign(oThis.paramErrorConfig, errorConfig.param_error_config || {}); + Object.assign(oThis.apiErrorConfig, errorConfig.api_error_config || {}); + + const apiErrInfo = oThis._fetchApiErrorInfo(), + formattedData = {}; + + if (!oThis.success) { + formattedData.success = false; + formattedData.err = { + code: apiErrInfo.code, + msg: apiErrInfo.message, + error_data: oThis._fetchDetailedParamErrors(), + internal_id: oThis.internalErrorCode || 'base_error', + debugOptions: oThis.debugOptions + }; + + if (oThis.goTo && Object.keys(oThis.goTo).length) { + formattedData.go_to = oThis.goTo; + } + } else { + formattedData.success = true; + formattedData.data = oThis.data; + + if (oThis.goTo && Object.keys(oThis.goTo).length) { + formattedData.go_to = oThis.goTo; + } + } + + return formattedData; + }, + + /** + * renderResponse - return the final response along with status + * + * @param responseObj - this is the response object for request + * @param errorConfig - this is config of eror - contains keys + * @param errorConfig.api_error_config - api error info + * @param errorConfig.param_error_config - param error info + */ + renderResponse: function(responseObj, errorConfig) { + const oThis = this; + + errorConfig = errorConfig || {}; + + const formattedResponse = oThis.toHash(errorConfig), + status = oThis.success ? 200 : oThis._fetchHttpCode(errorConfig.api_error_config || {}); + + return responseObj.status(status).json(formattedResponse); + }, + + /** + * @private + * + * @returns {Array} - returns array of parameter level error messages. + */ + _fetchDetailedParamErrors: function() { + const oThis = this, + detailedParamErrors = []; + + for (var i = 0; i < oThis.paramsErrorIdentifiers.length; i++) { + let paramErrorIdentifier = oThis.paramsErrorIdentifiers[i]; + let errInfo = oThis._fetchParamErrorDetails(paramErrorIdentifier); + detailedParamErrors.push({ + parameter: errInfo.parameter, + msg: errInfo.message + }); + } + + return detailedParamErrors; + }, + + /** + * fetch API error details from Config + * + * @private + * + * @return {object} + */ + _fetchApiErrorInfo: function() { + const oThis = this; + + return ( + oThis.apiErrorConfig[oThis.apiErrorIdentifier] || { + code: '', + message: 'Something went wrong' + } + ); + }, + + /** + * fetch Param error details from Config + * + * @private + * + * @param {String} paramErrorCode - code using which details are to be fetched + * + * @return {object} + */ + _fetchParamErrorDetails: function(paramErrorCode) { + const oThis = this; + + return ( + oThis.paramErrorConfig[paramErrorCode] || { + //TODO: Add defaults + name: 'Something went wrong', + message: 'Something went wrong' + } + ); + }, + + /** + * fetch response http code + * + * @private + * + * @param {object} apiErrorConfig - api error config + * + * @return {Number} - returns the http code + */ + _fetchHttpCode: function(apiErrorConfig) { + const oThis = this; + + Object.assign(oThis.apiErrorConfig, apiErrorConfig || {}); + + return (oThis.apiErrorConfig[oThis.apiErrorIdentifier] || {}).http_code || '200'; + } +}; + +/** + * ResponseHelper constructor + * + * @param {object} params - parameters object + * @param {string} params.module_name - module name + * @param {object} params.error_config - optional debug params + * @param {object} params.error_config.api_error_config - api error info + * @param {object} params.error_config.param_error_config - param error info + * + * @return {result} + */ +const ResponseHelper = function(params) { + const oThis = this; + + oThis.moduleName = params.module_name || 'base'; +}; + +/** + * ResponseHelper - Helper Class for constructing the Result object and formatting it. + */ +ResponseHelper.prototype = { + /** + * Success with data + * + * @param {object} data - data to be sent with success result + * @param {object} goTo - go to + * + * @return {result} + */ + successWithData: function(data, goTo) { + goTo = goTo || {}; + + const resultParams = { + success_data: data, + go_to: goTo + }; + + return new Result(resultParams); + }, + + /** + * Error result + * @param {object} params - parameters object + * @param {string} params.internal_error_identifier - internal error code + * @param {string} params.api_error_identifier - external error code + * @param {object} params.error_config - error_config + * @param {object} params.debug_options - optional debug_options + * @param {object} params.go_to - optional go_to + * + * @return {result} + */ + error: function(params) { + const oThis = this, + internalErrorId = params.internal_error_identifier, + apiErrorId = params.api_error_identifier, + errorConfig = params.error_config || {}, + debugOptions = params.debug_options || {}, + err_code_for_log = oThis.moduleName + '(' + internalErrorId + ':' + shortId.generate() + ')', + goTo = params.go_to || {}; + + logger.error( + 'err_code_for_log: ', + err_code_for_log, + 'internalErrorId: ', + internalErrorId, + 'apiErrorId: ', + apiErrorId, + 'debugOptions: ', + debugOptions + ); + + const resultParams = { + success_data: {}, + internal_error_identifier: internalErrorId, + api_error_identifier: apiErrorId, + params_error_identifiers: [], + error_config: errorConfig, + debugOptions: debugOptions, + go_to: goTo + }; + + return new Result(resultParams); + }, + + /** + * Param validation error result + * @param {object} params - parameters object + * @param {string} params.internal_error_identifier - internal error code + * @param {string} params.api_error_identifier - external error code + * @param {Array} params.params_error_identifiers - array having error identifier for params + * @param {object} params.error_config - error_config + * @param {object} params.debug_options - optional debug_options + * @param {object} params.go_to - optional go_to + * + * @return {result} + */ + paramValidationError: function(params) { + const internalErrorId = params.internal_error_identifier, + apiErrorId = params.api_error_identifier, + paramsErrorIdentifiers = params.params_error_identifiers, + errorConfig = params.error_config || {}, + debugOptions = params.debug_options || {}, + goTo = params.go_to || {}; + + logger.error( + 'internalErrorId: ', + internalErrorId, + '\napiErrorId: ', + apiErrorId, + '\nparamsErrorIdentifiers: ', + paramsErrorIdentifiers, + '\ndebugOptions: ', + debugOptions + ); + + const resultParams = { + success_data: {}, + internal_error_identifier: internalErrorId, + api_error_identifier: apiErrorId, + params_error_identifiers: paramsErrorIdentifiers, + error_config: errorConfig, + go_to: goTo + }; + + return new Result(resultParams); + }, + + /** + * return true if the object passed is of Result class + * + * @param {object} obj - object to check instanceof + * + * @return {boolean} + */ + isCustomResult: function(obj) { + return obj instanceof Result; + } +}; + +module.exports = ResponseHelper; diff --git a/lib/logger/CustomConsoleLogger.js b/lib/logger/CustomConsoleLogger.js new file mode 100644 index 0000000..4be6b5e --- /dev/null +++ b/lib/logger/CustomConsoleLogger.js @@ -0,0 +1,456 @@ +/** + * Custom console logger + * + * @module helpers/custom_console_logger + */ + +const myProcess = require('process'); + +const pid = String(myProcess.pid), + pIdPrexfix = '[' + pid + ']', + DEBUG = false; + +/** + Notes for developer trying to change the behaviour of CustomConsoleLogger.prototype.notify or trying to set requestNamespace. + 1. Please derive the CustomConsoleLogger. + 2. Please override the getRequestNameSpace & notify methods. +**/ + +const LOG_LEVELS = { + /* STANDARD LOG LEVELS */ + OFF: 0 /* 00000000 */, + FATAL: 100, + ERROR: 200, + WARN: 300, + INFO: 400, + DEBUG: 500, + TRACE: 600, + ALL: Number.MAX_SAFE_INTEGER +}; + +const ENV_VAR_NAME = 'LOG_LEVEL'; + +/* Indexes below are from right to left. */ +const LoggerMethodToLevelMap = { + notify: LOG_LEVELS.FATAL, + error: LOG_LEVELS.ERROR, + warn: LOG_LEVELS.WARN, + info: LOG_LEVELS.INFO, + step: LOG_LEVELS.INFO, + win: LOG_LEVELS.INFO, + debug: LOG_LEVELS.DEBUG, + log: LOG_LEVELS.DEBUG, + dir: LOG_LEVELS.DEBUG, + trace: LOG_LEVELS.TRACE +}; + +const CONSOLE_RESET = '\x1b[0m', + ERR_PRE = '\x1b[31m', //Error. (RED) + NOTE_PRE = '\x1b[91m', //Notify Error. (Purple) + INFO_PRE = '\x1b[35m', //Info (Magenta) + WIN_PRE = '\x1b[32m', //Success (GREEN) + LOG_PRE = CONSOLE_RESET, //Log (Default Console Color) + DEBUG_PRE = '\x1b[36m', //Debug log (Cyan) + WARN_PRE = '\x1b[43m\x1b[30m', + STEP_PRE = '\x1b[34m', + DIR_PRE = '\x1b[36m'; + +//Other Known Colors +//"\x1b[33m" // (YELLOW) + +let DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO; +// Process the Environment Varibale config. +(function() { + let envVal = process.env[ENV_VAR_NAME]; + + // Case 1: Variable not defined. + if (typeof envVal === 'undefined') { + return; + } + + // Case 2: Its might be key defined in LOG_LEVELS. + let strEnvVal = String(envVal).toUpperCase(); + if (LOG_LEVELS.hasOwnProperty(strEnvVal)) { + DEFAULT_LOG_LEVEL = LOG_LEVELS[strEnvVal]; + return; + } + + // Case 3: It might be a number. + let numEnvVal = Number(envVal); + if (!isNaN(numEnvVal)) { + DEFAULT_LOG_LEVEL = numEnvVal; + } +})(); + +/** + * Custom Console Logger + * + * @constructor + */ +const CustomConsoleLogger = function(moduleName, logLevel) { + var oThis = this; + + if (moduleName) { + oThis.moduleNamePrefix = '[' + moduleName + ']'; + } + + oThis.setLogLevel(logLevel); +}; + +CustomConsoleLogger.LOG_LEVELS = LOG_LEVELS; +CustomConsoleLogger.LoggerMethodToLevelMap = LoggerMethodToLevelMap; + +CustomConsoleLogger.prototype = { + constructor: CustomConsoleLogger, + + logLevel: DEFAULT_LOG_LEVEL, + setLogLevel: function(logLevel) { + const oThis = this; + + const debugLog = CustomConsoleLogger.prototype.log; + + if (isNaN(logLevel)) { + logLevel = String(logLevel).toUpperCase(); + DEBUG && debugLog.call(oThis, 'logLevel set to ', logLevel); + if (LOG_LEVELS.hasOwnProperty(logLevel)) { + logLevel = LOG_LEVELS[logLevel]; + } else { + DEBUG && debugLog.call(oThis, 'logLevel', logLevel, ' NOT FOUND! ', LOG_LEVELS); + logLevel = DEFAULT_LOG_LEVEL; + } + } + + logLevel = oThis.logLevel = Number(logLevel); + + //set the logging methods on self. + oThis.setLoggingMethods(); + + return logLevel; + }, + + setLoggingMethods: function() { + const debugLog = CustomConsoleLogger.prototype.log; + + const oThis = this; + + const logLevel = oThis.logLevel, + doNothing = oThis.doNothing; + + var fnName, fnLogLevel; + + for (fnName in LoggerMethodToLevelMap) { + if (!LoggerMethodToLevelMap.hasOwnProperty(fnName)) { + DEBUG && debugLog.call(oThis, 'fnName', fnName, 'ignored'); + continue; + } + + fnLogLevel = LoggerMethodToLevelMap[fnName]; + + if (fnLogLevel > logLevel) { + //Set it to doNothing + oThis[fnName] = doNothing; + DEBUG && debugLog.call(oThis, 'fnName', fnName, 'set to doNothing'); + } else { + //Set it to original method. + oThis[fnName] = CustomConsoleLogger.prototype[fnName]; + DEBUG && debugLog.call(oThis, 'fnName', fnName, 'set to prototype'); + } + } + }, + + /** + * @ignore + * + * @constant {string} + */ + STEP_PRE: STEP_PRE, + + /** + * @ignore + * + * @constant {string} + */ + WARN_PRE: WARN_PRE, + + /** + * @ignore + * + * @constant {string} + */ + LOG_PRE: LOG_PRE, + + /** + * @ignore + * + * @constant {string} + */ + DEBUG_PRE: DEBUG_PRE, + + /** + * @ignore + * + * @constant {string} + */ + DIR_PRE: DIR_PRE, + + /** + * @ignore + * + * @constant {string} + */ + WIN_PRE: WIN_PRE, + + /** + * @ignore + * + * @constant {string} + */ + INFO_PRE: INFO_PRE, + + /** + * @ignore + * + * @constant {string} + */ + ERR_PRE: ERR_PRE, + + /** + * @ignore + * + * @constant {string} + */ + NOTE_PRE: NOTE_PRE, + + /** + * @ignore + * + * @constant {string} + */ + CONSOLE_RESET: CONSOLE_RESET, + + getRequestNameSpace: function() { + //To-be overridden by derived class. + return null; + }, + + //Method to Log Request Started. + requestStartLog: function(requestUrl, requestType) { + const oThis = this, + d = new Date(), + dateTime = + d.getFullYear() + + '-' + + (d.getMonth() + 1) + + '-' + + d.getDate() + + ' ' + + d.getHours() + + ':' + + d.getMinutes() + + ':' + + d.getSeconds() + + '.' + + d.getMilliseconds(), + message = "Started '" + requestType + "' '" + requestUrl + "' at " + dateTime; + + oThis.info(message); + }, + + /** + * Method to append Request in each log line. + * + * @param {string} message + */ + moduleNamePrefix: '', + getPrefix: function(prefix) { + const oThis = this; + var newMessage = pIdPrexfix; + + const requestNamespace = oThis.getRequestNameSpace(); + if (requestNamespace) { + if (requestNamespace.get('reqId')) { + newMessage += '[' + requestNamespace.get('reqId') + ']'; + } + } + + newMessage += '[' + Date.now() + ']' + oThis.moduleNamePrefix + prefix; + return newMessage; + }, + + /** + * Log level fatal/off methods + */ + doNothing: function() { + //Do Nothing. + }, + + /** + * Log level error methods + */ + error: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.ERR_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + /** + * Notify for FATAL errors + */ + notify: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.NOTE_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + /** + * Log level warn methods + */ + warn: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.WARN_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + /** + * Log level info methods + */ + info: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.INFO_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + /** + * Log step + */ + step: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.STEP_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + /** + * Log win - on done + */ + win: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.WIN_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + /** + * Log level debug methods + */ + log: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.LOG_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + debug: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.DEBUG_PRE)]; + args = args.concat(argsPassed); + args.push(this.CONSOLE_RESET); + console.log.apply(console, args); + }, + + /** + * Log level trace methods + */ + trace: function() { + var oThis = this; + + var argsPassed = oThis._filterArgs(arguments); + + var args = [oThis.getPrefix(this.ERR_PRE)]; + args = args.concat(Array.prototype.slice.call(argsPassed)); + console.trace.apply(console, args); + console.log(this.CONSOLE_RESET); + }, + + dir: function(obj) { + var oThis = this; + var args = [oThis.getPrefix(this.DIR_PRE)]; + console.log.apply(console, args); + console.dir(obj); + console.log(this.CONSOLE_RESET); + }, + + testLogger: function() { + const oThis = this, + logMeObj = { l1: { l2: { l3Val: 'val3', l3: { l4Val: { val: 'val' } } } } }; + + console.log('Testing Basic Methods'); + try { + oThis.step('step Invoked', logMeObj); + oThis.info('info Invoked', logMeObj); + oThis.error('error called', logMeObj); + oThis.warn('warn called', logMeObj); + oThis.win('win called', logMeObj); + oThis.log('log called', logMeObj); + oThis.debug('debug called::', logMeObj); + oThis.trace('trace called', logMeObj); + oThis.dir({ l1: { l2: { l3Val: 'val3', l3: { l4Val: { val: 'val' } } } } }); + } catch (e) { + console.error('Basic Test Failed. Error:\n', e); + return; + } + console.log('All Basic Test Passed!'); + }, + + _filterArgs: function(args) { + var argsPassed = [], + currArg, + i; + for (i = 0; i < args.length; i++) { + currArg = args[i]; + if (!(currArg instanceof Error) && currArg instanceof Object) { + currArg = JSON.stringify(currArg); + } + argsPassed.push(currArg); + } + + return argsPassed; + } +}; + +module.exports = CustomConsoleLogger; diff --git a/lib/promiseContext/PromiseContext.js b/lib/promiseContext/PromiseContext.js new file mode 100644 index 0000000..b0abdaf --- /dev/null +++ b/lib/promiseContext/PromiseContext.js @@ -0,0 +1,457 @@ +const rootPrefix = '../..', + Logger = require(rootPrefix + '/lib/logger/CustomConsoleLogger'); + +const logMe = true, + verboseLog = logMe && true, + defaultTimeout = 30000, + logger = new Logger('PromiseContext'); + +const PromiseContext = (module.exports = function(executor, options, executorParams) { + const oThis = this; + + // Take Care of Options + options = options || {}; + Object.assign(oThis, options); + + oThis.executorParams = executorParams || oThis.executorParams; + + const wrappedExecutor = oThis.createWrappedExecutor(executor); + oThis.createTimeout(); + oThis.creationTs = Date.now(); + oThis.promise = new Promise(wrappedExecutor); +}); + +PromiseContext.prototype = { + constructor: PromiseContext, + + // Pass timeoutInMilliSecs in options to set the timeout. + // If less than or equal to zero, timeout will not be observed. + timeoutInMilliSecs: defaultTimeout, + + // To Auto-Resolve a promise on timeout, set resolvePromiseOnTimeout to true. + // It can be set using options parameter in constructor. + resolvePromiseOnTimeout: false, + rejectPromiseOnTimeout: false, + + // The value to be passed to resolve when the Promise has timedout. + // It can be set using options parameter in constructor. + resolvedValueOnTimeout: null, + rejectedReasonOnTimeout: 'Promise has timed out', + + onResolved: function(resolvedValue, promiseContext) { + // Triggered when promise is fulfilled. + // This callback method should be set by instance creator. + // It can be set using options parameter in constructor. + verboseLog && logger.log('Promise has been resolved with value', resolvedValue); + verboseLog && promiseContext.logInfo(); + }, + onRejected: function(rejectReason, promiseContext) { + // Triggered when callback is rejected. + // This callback method should be set by instance creator. + // It can be set using options parameter in constructor. + verboseLog && logger.log('Promise has been rejected with reason', rejectReason); + verboseLog && promiseContext.logInfo(); + }, + onTimedout: function(promiseContext) { + // Triggered when Promise failed to resolve/reject with-in time limit. + // Time-Limit can be set using + // This callback method should be set by instance creator. + // It can be set using options parameter in constructor. + verboseLog && logger.log('Promise has timedout.'); + verboseLog && promiseContext.logInfo(); + }, + + onCompletedAfterTimeout: function(promiseContext) { + // Triggered when the Promise is fulfilled/rejected after time limit. + // This callback can only trigger when both resolvePromiseOnTimeout & rejectPromiseOnTimeout are set to false. + // This callback method should be set by instance creator. + // It can be set using options parameter in constructor. + + verboseLog && logger.log('Promise has completed after timeout.'); + verboseLog && promiseContext.logInfo(); + }, + + // Use promise property to get promise instance. + promise: null, + getPromise: function() { + const oThis = this; + + return oThis.promise; + }, + executorParams: null, + getExecutorParams: function() { + const oThis = this; + + return oThis.executorParams; + }, + + wrappedExecutor: null, + createWrappedExecutor: function(executor) { + const oThis = this; + + const executorParams = oThis.getExecutorParams(); + + // The actual executor function that is passed on to the subscribers. + oThis.wrappedExecutor = function(resolve, reject) { + const wrappedResolve = oThis.createWrappedResolve(resolve); + const wrappedReject = oThis.createWrappedReject(reject); + executor && executor(wrappedResolve, wrappedReject, executorParams, oThis); + oThis.executionTs = Date.now(); + }; + + return oThis.wrappedExecutor; + }, + + resolve: null, + createWrappedResolve: function(resolve) { + const oThis = this; + + // The actual resolve function that is passed on to the subscribers. + oThis.resolve = function(resolvedValue) { + if (oThis.isResolved || oThis.isRejected) { + logger.trace( + 'PromiseContext :: resolve invoked when promise has already been resolved/rejected.' + + '\n\t Ignoring resolve.' + ); + oThis.logInfo(); + return; + } + + // Invoke resolve method. Don't bother about arguments, pass it on as is. + if (resolve instanceof Promise) { + resolve.apply(null, arguments).catch(function(reason) { + logger.trace('PromiseContext :: resolve threw an error :: ', reason); + }); + } else { + try { + resolve.apply(null, arguments); + } catch (e) { + logger.trace('PromiseContext :: resolve threw an error :: ', e); + } + } + + // Update the flags. + oThis.isResolved = true; + + // Trigger Callback if available. + oThis.onResolved && oThis.onResolved(resolvedValue, oThis); + + if (oThis.hasTimedout) { + oThis.completedAfterTimeout(); + } + + // Clean Up + oThis.cleanup(); + }; + + return oThis.resolve; + }, + + reject: null, + createWrappedReject: function(reject) { + const oThis = this; + + // The actual reject function that is passed on to the subscribers. + oThis.reject = function(reason) { + if (oThis.isResolved || oThis.isRejected) { + logger.trace( + 'IMPORTANT :: PromiseContext :: reject invoked when promise has already been resolved/rejected.' + + '\n\t Ignoring reject' + ); + oThis.logInfo(); + return; + } + + // Invoke reject method. Don't bother about arguments, pass it on as is. + if (reject instanceof Promise) { + reject.apply(null, arguments).catch(function(reason) { + logger.trace('PromiseContext :: reject threw an error :: ', reason); + setTimeout(function() { + return Promise.reject(reason); + }, 100); + }); + } else { + try { + reject.apply(null, arguments); + } catch (e) { + logger.trace('PromiseContext :: reject threw an error :: ', e); + } + } + + // Update the flags. + oThis.isRejected = true; + + // Trigger Callback if available. + oThis.onRejected && oThis.onRejected(reason, oThis); + + if (oThis.hasTimedout) { + oThis.completedAfterTimeout(); + } + + // Clean Up + oThis.cleanup(); + }; + + return oThis.reject; + }, + + timeout: null, + createTimeout: function() { + const oThis = this; + + const timeoutInMilliSecs = (oThis.timeoutInMilliSecs = Number(oThis.timeoutInMilliSecs)); + oThis.timeout = function() { + if (oThis.isResolved || oThis.isRejected || oThis.hasTimedout) { + // The Promise has been taken care off. + return; + } + + // Update the flags. + oThis.hasTimedout = true; + + // Trigger Callback if available. Do it first so that something can be done about it from outside. + if (oThis.onTimedout) { + oThis.onTimedout(oThis); + } + + logger.error('PromiseContext :: timeout :: a promise has timedout. executorParams: ', oThis.getExecutorParams()); + + if (oThis.resolvePromiseOnTimeout) { + logger.warn('PromiseContext :: timeout :: Forcefully Resolving it.'); + oThis.resolve(oThis.resolvedValueOnTimeout); + } else if (oThis.rejectPromiseOnTimeout) { + logger.warn('PromiseContext :: timeout :: Forcefully Rejecting it.'); + oThis.reject(oThis.rejectedReasonOnTimeout); + } else { + logger.error('PromiseContext :: timeout :: Zombie process has been detected.'); + oThis.logInfo(); + oThis.cleanup(); + } + + // IMPORTANT: DO NOT CLEAN UP HERE. + // The code using this class may want to retry. + }; + + // Set the timeout if needed. + if (!isNaN(timeoutInMilliSecs) && timeoutInMilliSecs > 0) { + // Observe self. + setTimeout(oThis.timeout, timeoutInMilliSecs); + } + }, + + completedAfterTimeout: function() { + const oThis = this; + + if (!oThis.hasTimedout) { + logger.trace('completedAfterTimeout invoked unexpectedly. hasTimedout is false'); + return; + } + + if (!(oThis.isResolved || oThis.isRejected)) { + logger.trace( + 'completedAfterTimeout invoked unexpectedly. The promise is neither resolved nor rejected. isResolved =', + oThis.isResolved, + 'isRejected =', + oThis.isRejected + ); + return; + } + + const currTimestamp = Date.now(), + executionTime = currTimestamp - oThis.creationTs; + + logger.warn( + 'PromiseContext :: completedAfterTimeout ::', + 'A promise completed (resolved/rejected) after timeout!', + '\n\tExecutionTime (miliseconds) :: ', + executionTime, + 'Configured Timeout :: ', + oThis.timeoutInMilliSecs + ); + + oThis.onCompletedAfterTimeout && oThis.onCompletedAfterTimeout(oThis); + }, + + isResolved: false, + isRejected: false, + hasTimedout: false, + executionTs: 0, + creationTs: 0, + clenupTs: 0, + + cleanup: function() { + const oThis = this; + + oThis.wrappedExecutor = null; + oThis.promise = null; + oThis.resolve = null; + oThis.reject = null; + oThis.timeout = null; + + // Note when the clean up was done. + oThis.clenupTs = Date.now(); + }, + + logInfo: function() { + const oThis = this; + + logger.info( + ' PromiseContext Info :: ', + 'isResolved:', + oThis.isResolved, + 'isRejected:', + oThis.isRejected, + 'hasTimedout:', + oThis.hasTimedout, + 'creationTs:', + oThis.creationTs, + 'executionTs:', + oThis.executionTs, + 'clenupTs:', + oThis.clenupTs + ); + } +}; + +PromiseContext.Examples = { + simpleResolve: function() { + const _timeout = 3000; + var p1 = new PromiseContext( + function(resolve, reject) { + // Lets call resolve in 2 secs + setTimeout(function() { + resolve('Examples.simpleResolve :: resolving p1'); + }, _timeout * 0.9); + }, + { timeoutInMilliSecs: _timeout } + ); + }, + simpleReject: function() { + const _timeout = 3000; + var p2 = new PromiseContext( + function(resolve, reject) { + // Lets call reject in 2 secs + setTimeout(function() { + reject('Examples.simpleReject :: rejecting p2'); + }, _timeout * 0.9); + }, + { timeoutInMilliSecs: _timeout } + ); + }, + simpleTimeout: function() { + const _timeout = 3000; + var p3 = new PromiseContext( + function(resolve, reject) { + //Do Nothing. + }, + { + timeoutInMilliSecs: _timeout, + onTimedout: function(promiseContext) { + logger.log('Examples.simpleTimeout :: p3 timedout.'); + } + } + ); + }, + resolvePromiseOnTimeout: function() { + const _timeout = 3000; + var p4 = new PromiseContext( + function(resolve, reject) { + // Lets call resolve in 6 secs + setTimeout(function() { + resolve('p4 resolved'); + }, _timeout * 2); + }, + { + timeoutInMilliSecs: _timeout, + resolvePromiseOnTimeout: true, + onTimedout: function(promiseContext) { + logger.log('Examples.simpleTimeout :: p4 timedout.'); + } + } + ); + }, + rejectPromiseOnTimeout: function() { + const _timeout = 3000; + var p5 = new PromiseContext( + function(resolve, reject) { + // Lets call resolve in 6 secs + setTimeout(function() { + reject('p5 resolved'); + }, _timeout * 2); + }, + { + timeoutInMilliSecs: 3000, + rejectPromiseOnTimeout: true, + onTimedout: function(promiseContext) { + logger.log('Examples.simpleTimeout :: p5 timedout.'); + } + } + ); + }, + zomibePromiseTimedOut: function() { + const _timeout = 3000; + var p6 = new PromiseContext( + function(resolve, reject) { + // Do nothing + }, + { + timeoutInMilliSecs: _timeout, + resolvePromiseOnTimeout: false, + rejectPromiseOnTimeout: false, + onTimedout: function(promiseContext) { + logger.log('Examples.simpleTimeout :: p6 timedout.'); + } + } + ); + }, + + resolvePromiseAfterTimeout: function() { + const _timeout = 3000; + var undead = new PromiseContext( + function(resolve, reject) { + // Lets call resolve in 6 secs + setTimeout(function() { + resolve('undead resolved'); + }, _timeout * 2); + }, + { + timeoutInMilliSecs: _timeout, + resolvePromiseOnTimeout: false, + onTimedout: function(promiseContext) { + logger.log('Examples.simpleTimeout :: undead timedout.'); + } + } + ); + }, + + rejectPromiseAfterTimeout: function() { + const _timeout = 3000; + var undead = new PromiseContext( + function(resolve, reject) { + // Lets call resolve in 6 secs + setTimeout(function() { + reject('undead rejected'); + }, _timeout * 2); + }, + { + timeoutInMilliSecs: _timeout, + resolvePromiseOnTimeout: false, + onTimedout: function(promiseContext) { + logger.log('Examples.simpleTimeout :: undead timedout.'); + } + } + ); + }, + + testAll: function() { + var oThis = this; + oThis.simpleResolve(); + oThis.simpleReject(); + oThis.simpleTimeout(); + oThis.resolvePromiseOnTimeout(); + oThis.rejectPromiseOnTimeout(); + oThis.zomibePromiseTimedOut(); + oThis.resolvePromiseAfterTimeout(); + oThis.rejectPromiseAfterTimeout(); + } +}; diff --git a/lib/promiseContext/PromiseQueueManager.js b/lib/promiseContext/PromiseQueueManager.js new file mode 100644 index 0000000..1e20ce0 --- /dev/null +++ b/lib/promiseContext/PromiseQueueManager.js @@ -0,0 +1,500 @@ +const rootPrefix = '../..', + PromiseContext = require(rootPrefix + '/lib/promiseContext/PromiseContext'), + Logger = require(rootPrefix + '/lib/logger/CustomConsoleLogger'); + +const logMe = false, + verboseLog = logMe && true, + defaultTimeout = 30000, + logger = new Logger('PromiseQueueManager'); + +const Manager = (module.exports = function(promiseExecutor, options) { + const oThis = this; + + //Take Care of Options + options = options || {}; + Object.assign(oThis, options); + + oThis.promiseExecutor = promiseExecutor || oThis.promiseExecutor; + oThis.pendingPromises = oThis.pendingPromises || []; + oThis.completedPromises = oThis.completedPromises || []; + + if (!oThis.name) { + oThis.name = 'PQM_' + Date.now(); + } + oThis.logHealthCheckIfNeeded(); +}); + +Manager.prototype = { + constructor: Manager, + + // Specify the name for easy identification in logs. + name: '', + + //Executor method to be passed on to Promise Constructor + promiseExecutor: null, + + // resolvePromiseOnTimeout :: set this flag to false if you need custom handling. + // By Default, the manager will neither resolve nor reject the Promise on time out. + resolvePromiseOnTimeout: false, + // The value to be passed to resolve when the Promise has timedout. + resolvedValueOnTimeout: null, + + // rejectPromiseOnTimeout :: set this flag to true if you need custom handling. + rejectPromiseOnTimeout: false, + // The reason with which Promise has to be rejected when the Promise has timedout. + rejectedReasonOnTimeout: 'Promise has timed out', + + // Pass timeoutInMilliSecs in options to set the timeout. + // If less than or equal to zero, timeout will not be observed. + timeoutInMilliSecs: defaultTimeout, + + // Pass maxZombieCount in options to set the max acceptable zombie count. + // When this zombie promise count reaches this limit, onMaxZombieCountReached will be triggered. + // If less than or equal to zero, onMaxZombieCountReached callback will not triggered. + maxZombieCount: 0, + + // Pass logInfoTimeInterval in options to log queue healthcheck information. + // If less than or equal to zero, healthcheck will not be logged. + logInfoTimeInterval: 3 * 1000, + + onPromiseResolved: function(resolvedValue, promiseContext) { + //onPromiseResolved will be executed when the any promise is resolved. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + verboseLog && logger.log(oThis.name, ' :: a promise has been resolved. resolvedValue:', resolvedValue); + }, + + onPromiseRejected: function(rejectReason, promiseContext) { + //onPromiseRejected will be executed when the any promise is timedout. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + verboseLog && logger.log(oThis.name, ' :: a promise has been rejected. rejectReason: ', rejectReason); + }, + + onPromiseTimedout: function(promiseContext) { + //onPromiseTimedout will be executed when the any promise is timedout. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + verboseLog && logger.log(oThis.name, ':: a promise has timed out.', promiseContext.executorParams); + }, + + onMaxZombieCountReached: function() { + //onMaxZombieCountReached will be executed when maxZombieCount >= 0 && current zombie count (oThis.zombieCount) >= maxZombieCount. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + verboseLog && logger.log(oThis.name, ':: maxZombieCount reached.'); + }, + + onPromiseCompleted: function(promiseContext) { + //onPromiseCompleted will be executed when the any promise is removed from pendingPromise queue. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + const oThis = this; + + verboseLog && logger.log(oThis.name, ':: a promise has been completed.'); + }, + + //onAllPromisesCompleted will be executed when the last promise in pendingPromise is resolved/rejected. + //This callback method should be set by instance creator. + //It can be set using options parameter in constructor. + //Ideally, you should set this inside SIGINT/SIGTERM handlers. + + onAllPromisesCompleted: null, + + createPromise: function(executorParams) { + //Call this method to create a new promise. + + const oThis = this; + + const executor = oThis.promiseExecutor, + pcOptions = oThis.getPromiseContextOptions(), + newPC = new PromiseContext(executor, pcOptions, executorParams); + + oThis.createdCount++; + oThis.pendingPromises.push(newPC); + + return newPC.promise; + }, + + getPendingCount: function() { + const oThis = this; + + return oThis.pendingPromises.length; + }, + + getCompletedCount: function() { + const oThis = this; + + return oThis.completedCount; + }, + + // Arrays/Queues to hold Promise Context. + pendingPromises: null, + completedPromises: null, + + // Some Stats. + createdCount: 0, + resolvedCount: 0, + rejectedCount: 0, + zombieCount: 0, + timedOutCount: 0, + completedCount: 0, + + // Some flags + hasTriggeredMaxZombieCallback: false, + + pcOptions: null, + getPromiseContextOptions: function() { + const oThis = this; + + oThis.pcOptions = oThis.pcOptions || { + resolvePromiseOnTimeout: oThis.resolvePromiseOnTimeout, + rejectPromiseOnTimeout: oThis.rejectPromiseOnTimeout, + resolvedValueOnTimeout: oThis.resolvedValueOnTimeout, + rejectedReasonOnTimeout: oThis.rejectedReasonOnTimeout, + timeoutInMilliSecs: oThis.timeoutInMilliSecs, + onResolved: function() { + oThis._onResolved.apply(oThis, arguments); + }, + onRejected: function() { + oThis._onRejected.apply(oThis, arguments); + }, + onTimedout: function() { + oThis._onTimedout.apply(oThis, arguments); + }, + onCompletedAfterTimeout: function() { + oThis._onCompletedAfterTimeout.apply(oThis, arguments); + } + }; + + return oThis.pcOptions; + }, + + _onResolved: function(resolvedValue, promiseContext) { + const oThis = this; + + //Give a callback. + //Dev-Note: Can this be inside settimeout ? + if (oThis.onPromiseResolved) { + oThis.onPromiseResolved.apply(oThis, arguments); + } + + //Update the stats. + oThis.resolvedCount++; + + //Mark is Completed. + oThis.markAsCompleted(promiseContext); + }, + _onRejected: function(rejectReason, promiseContext) { + const oThis = this; + + //Give a callback. + //Dev-Note: Can this be inside settimeout ? + if (oThis.onPromiseRejected) { + oThis.onPromiseRejected.apply(oThis, arguments); + } + + //Update the stats. + oThis.rejectedCount++; + + //Mark is Completed. + oThis.markAsCompleted(promiseContext); + }, + _onTimedout: function(promiseContext) { + const oThis = this; + + const maxZombieCount = oThis.maxZombieCount; + + //Update the stats. + oThis.timedOutCount++; + + logMe && logger.log(oThis.name, ':: _onTimedout :: promise has timedout'); + + //Give a callback. + //Dev-Note: This callback should not be triggered inside setTimeout. + //Give the instance creator a chance to do something with promiseContext. + if (oThis.onPromiseTimedout) { + oThis.onPromiseTimedout.apply(oThis, arguments); + } + + //Update the zombie count only if resolve and reject on timeout are false. + if (!oThis.resolvePromiseOnTimeout && !oThis.rejectPromiseOnTimeout) { + oThis.zombieCount++; + } + + //Mark is Completed. + oThis.markAsCompleted(promiseContext); + + // Check if we have reached max zombie count. + if ( + oThis.onMaxZombieCountReached && + !oThis.hasTriggeredMaxZombieCallback && + !isNaN(maxZombieCount) && + maxZombieCount > 0 && + oThis.zombieCount >= maxZombieCount + ) { + oThis.hasTriggeredMaxZombieCallback = true; + oThis.onMaxZombieCountReached(); + } + }, + + _onCompletedAfterTimeout: function(promiseContext) { + const oThis = this; + logMe && + logger.log( + oThis.name, + ':: _onCompletedAfterTimeout :: promise has completed after the assumed timeout. Updating timedOutCount & zombieCount (if applicable) ' + ); + + // Update the stats. + oThis.timedOutCount--; + + // Update the zombie count only if resolve and reject on timeout are false. + if (!oThis.resolvePromiseOnTimeout && !oThis.rejectPromiseOnTimeout) { + oThis.zombieCount--; + } + oThis.logInfo(); + }, + + markAsCompleted: function(promiseContext) { + const oThis = this; + + const pendingPromises = oThis.pendingPromises, + completedPromises = oThis.completedPromises, + pcIndx = pendingPromises.indexOf(promiseContext); + if (pcIndx < 0) { + logger.trace( + oThis.name + + ' :: markAsCompleted :: Could not find a promiseContext. _onCompletedAfterTimeout should trigger soon. ' + ); + verboseLog && logger.log(promiseContext); + return; + } + + //Remove it from queue. + pendingPromises.splice(pcIndx, 1); + oThis.completedCount++; + + if (oThis.onPromiseCompleted) { + oThis.onPromiseCompleted.apply(oThis, arguments); + } + + if (!pendingPromises.length && oThis.onAllPromisesCompleted) { + oThis.onAllPromisesCompleted(); + } + }, + isValid: function() { + const oThis = this; + + const pendingCount = oThis.getPendingCount(), + createdCount = oThis.createdCount, + completedCount = oThis.completedCount, + resolvedCount = oThis.resolvedCount, + rejectedCount = oThis.rejectedCount, + zombieCount = oThis.zombieCount; + + var isValid = completedCount === resolvedCount + rejectedCount + zombieCount; + isValid = isValid && createdCount === pendingCount + completedCount; + + if (isValid) { + logMe && logger.log(oThis.name, ':: isValid :: Queue is valid.'); + } else { + logger.error('IMPORTANT ::', oThis.name, ':: validation failed!'); + } + + return isValid; + }, + logInfo: function() { + const oThis = this; + + const pendingCount = oThis.getPendingCount(), + createdCount = oThis.createdCount, + completedCount = oThis.completedCount, + resolvedCount = oThis.resolvedCount, + rejectedCount = oThis.rejectedCount, + timedOutCount = oThis.timedOutCount, + zombieCount = oThis.zombieCount, + isValid = oThis.isValid(); + + logger.log( + oThis.name, + ':: logInfo ::', + 'createdCount:', + createdCount, + 'pendingCount:', + pendingCount, + 'completedCount:', + completedCount, + 'resolvedCount:', + resolvedCount, + 'rejectedCount:', + rejectedCount, + 'zombieCount:', + zombieCount, + 'timedOutCount:', + timedOutCount, + 'isValid:', + isValid + ); + }, + + logHealthCheckIfNeeded: function() { + var oThis = this, + timeOut = oThis.logInfoTimeInterval; + if (timeOut < 1) { + return; + } + setTimeout(function() { + oThis.logInfo(); + oThis.logHealthCheckIfNeeded(); + }, timeOut); + }, + + someMethod: function() {} +}; + +Manager.Examples = { + allResolve: function(len) { + len = len || 50; + + const manager = new Manager( + function(resolve, reject) { + //promiseExecutor + setTimeout(function() { + resolve(len--); + }, 1000); + }, + { + onAllPromisesCompleted: function() { + logger.log('Examples.allResolve :: onAllPromisesCompleted triggered'); + manager.logInfo(); + }, + timeoutInMilliSecs: 5000, + logInfoTimeInterval: 0 + } + ); + + for (var cnt = 0; cnt < len; cnt++) { + manager.createPromise({ cnt: cnt + 1 }); + } + }, + + allReject: function(len) { + len = len || 50; + + const manager = new Manager( + function(resolve, reject) { + //promiseExecutor + setTimeout(function() { + reject(len--); + }, 1000); + }, + { + onAllPromisesCompleted: function() { + logger.log('Examples.allReject :: onAllPromisesCompleted triggered'); + manager.logInfo(); + }, + timeoutInMilliSecs: 5000, + logInfoTimeInterval: 0 + } + ); + + for (var cnt = 0; cnt < len; cnt++) { + manager.createPromise({ cnt: cnt + 1 }).catch(function(reason) { + logger.log('Examples.allReject :: promise catch triggered.'); + }); + } + }, + + allTimeout: function(len) { + len = len || 50; + + const manager = new Manager( + function(resolve, reject) { + //promiseExecutor + setTimeout(function() { + resolve(len--); + }, 10000); + }, + { + onAllPromisesCompleted: function() { + logger.log('Examples.allResolve :: onAllPromisesCompleted triggered'); + manager.logInfo(); + }, + timeoutInMilliSecs: 5000, + logInfoTimeInterval: 0 + } + ); + + for (var cnt = 0; cnt < len; cnt++) { + manager.createPromise({ cnt: cnt + 1 }).catch(); + } + }, + + executorWithParams: function(len) { + len = len || 50; + + const manager = new Manager( + function(resolve, reject, params) { + //promiseExecutor + setTimeout(function() { + resolve(params); + }, 1000); + }, + { + onAllPromisesCompleted: function() { + logger.log('Examples.executorWithParams :: onAllPromisesCompleted triggered'); + manager.logInfo(); + }, + timeoutInMilliSecs: 5000, + logInfoTimeInterval: 0 + } + ); + + for (var cnt = 0; cnt < len; cnt++) { + manager.createPromise({ cnt: cnt + 1 }); + } + }, + + maxZombieCount: function(len) { + len = len || 50; + + const maxZombieCount = Math.round(len * 0.1), + _timeout = 3000; + + const manager = new Manager( + function(resolve, reject, params) { + //Do Nothing. + }, + { + onAllPromisesCompleted: function() { + logger.log('Examples.executorWithParams :: onAllPromisesCompleted triggered'); + manager.logInfo(); + }, + timeoutInMilliSecs: 5000, + logInfoTimeInterval: 0, + maxZombieCount: maxZombieCount, + onMaxZombieCountReached: function() { + logger.win( + 'Examples.maxZombieCount :: onMaxZombieCountReached triggered. current zombieCount', + manager.zombieCount, + 'maxZombieCount', + manager.maxZombieCount + ); + } + } + ); + + for (var cnt = 0; cnt < len; cnt++) { + manager.createPromise({ cnt: cnt + 1 }); + } + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b308c61 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "@plgworks/base", + "version": "1.0.0", + "description": "Base provides advanced Promise Queue Manager and other utilities.", + "main": "index.js", + "scripts": { + "pre-commit": "lint-staged" + }, + "repository": { + "type": "git", + "url": "https://github.com/PLG-Works/base.git" + }, + "keywords": [ + "Base" + ], + "author": "PLG Works", + "license": "LGPL-3.0", + "bugs": { + "url": "https://github.com/PLG-Works/base/issues" + }, + "homepage": "https://github.com/PLG-Works/base", + "dependencies": { + "shortid": "2.2.16" + }, + "devDependencies": { + "chai": "4.3.0", + "lint-staged": "10.5.4", + "mocha": "10.0.0", + "pre-commit": "1.2.2", + "prettier": "1.14.3" + }, + "pre-commit": [ + "pre-commit" + ], + "lint-staged": { + "*.js": [ + "prettier --write --config .prettierrc.json", + "git add" + ] + } +} diff --git a/tests/mocha/lib/formatter/apiErrorConfig.json b/tests/mocha/lib/formatter/apiErrorConfig.json new file mode 100644 index 0000000..4606cb1 --- /dev/null +++ b/tests/mocha/lib/formatter/apiErrorConfig.json @@ -0,0 +1,7 @@ +{ + "test_1": { + "http_code": "400", + "code": "invalid_request", + "message": "At least one parameter is invalid or missing. See err.error_data for more details." + } +} \ No newline at end of file diff --git a/tests/mocha/lib/formatter/paramErrorConfig.json b/tests/mocha/lib/formatter/paramErrorConfig.json new file mode 100644 index 0000000..22f661b --- /dev/null +++ b/tests/mocha/lib/formatter/paramErrorConfig.json @@ -0,0 +1,6 @@ +{ + "test_1": { + "parameter": "Username", + "message": "Invalid" + } +} \ No newline at end of file diff --git a/tests/mocha/lib/formatter/responseHelper.js b/tests/mocha/lib/formatter/responseHelper.js new file mode 100644 index 0000000..bcac7c8 --- /dev/null +++ b/tests/mocha/lib/formatter/responseHelper.js @@ -0,0 +1,138 @@ +// Load external packages +const Chai = require('chai'), + assert = Chai.assert; + +const rootPrefix = '../../../..', + Base = require(rootPrefix + '/index'), + paramErrorConfig = require('./paramErrorConfig'), + apiErrorConfig = require('./apiErrorConfig'); + +const api_error_key = Object.keys(apiErrorConfig)[0], + param_error_key = Object.keys(paramErrorConfig)[0]; + +const responseHelper = new Base.responseHelper({ + moduleName: 'Base' +}); + +const errorConfig = { + param_error_config: paramErrorConfig, + api_error_config: apiErrorConfig +}; + +const commonErrorParams = { + internal_error_identifier: 'test_1', + api_error_identifier: api_error_key, + params_error_identifiers: [], + error_config: errorConfig +}; + +const commonParamErrorParams = { + internal_error_identifier: 'test_1', + api_error_identifier: api_error_key, + params_error_identifiers: [param_error_key], + error_config: errorConfig +}; + +const testHash = { test_key: 'test_value' }; + +describe('lib/formatter/ResponseHelper', function() { + it('Should create responseHelper object', function() { + assert.instanceOf(responseHelper, Base.responseHelper); + }); + + it('Should return failure status for error call', function() { + assert.equal(true, responseHelper.error(commonErrorParams).isFailure()); + }); + + it('Should return false for isSuccess call for error', function() { + assert.equal(false, responseHelper.error(commonErrorParams).isSuccess()); + }); + + it('Should return failure status for paramValidationError call', function() { + assert.equal(true, responseHelper.paramValidationError(commonParamErrorParams).isFailure()); + }); + + it('Should return false for isSuccess call for paramValidationError', function() { + assert.equal(false, responseHelper.paramValidationError(commonParamErrorParams).isSuccess()); + }); + + it('Should return true for isSuccess when called successWithData', function() { + assert.equal(true, responseHelper.successWithData(testHash).isSuccess()); + }); + + it('Should return false for isFailure when called successWithData', function() { + assert.equal(false, responseHelper.successWithData(testHash).isFailure()); + }); + + it('Should have all the expected keys in response hash', function() { + let responseData = responseHelper.error(commonErrorParams).toHash(errorConfig); + + assert.equal(true, responseData.hasOwnProperty('success')); + assert.equal(true, responseData.hasOwnProperty('err')); + assert.equal(true, responseData.err.hasOwnProperty('code')); + assert.equal(true, responseData.err.hasOwnProperty('msg')); + assert.equal(true, responseData.err.hasOwnProperty('error_data')); + assert.equal(true, responseData.err.hasOwnProperty('internal_id')); + }); + + it('Should have all the expected keys and values when called successWithData', function() { + let responseData = responseHelper.successWithData(testHash).toHash(errorConfig); + + assert.equal(true, responseData.hasOwnProperty('success')); + assert.equal(true, responseData.hasOwnProperty('data')); + assert.equal(true, responseData.data.hasOwnProperty('test_key')); + assert.equal('test_value', responseData.data.test_key); + }); + + it('Should return an array for error_data in err object', function() { + let responseData = responseHelper.paramValidationError(commonParamErrorParams).toHash(errorConfig); + + assert.equal(true, responseData.err.error_data instanceof Array); + }); + + it('Should return code in err as per input error code', function() { + assert.equal( + apiErrorConfig[api_error_key].code, + responseHelper.error(commonErrorParams).toHash(errorConfig).err.code + ); + }); + + it('Should return msg in err as per input error code', function() { + assert.equal( + apiErrorConfig[api_error_key].message, + responseHelper.error(commonErrorParams).toHash(errorConfig).err.msg + ); + }); + + it('Should return internal_id in err as per input error code', function() { + assert.equal('test_1', responseHelper.error(commonErrorParams).toHash(errorConfig).err.internal_id); + }); + + it('Should have key count of 3 in response hash', function() { + assert.equal(2, Object.keys(responseHelper.error(commonErrorParams).toHash(errorConfig)).length); + }); + + it('Should have key count of 4 in err object of response hash', function() { + assert.equal(4, Object.keys(responseHelper.error(commonErrorParams).toHash(errorConfig).err).length); + }); + + it('Should return name in error_data object as per code in input', function() { + assert.equal( + paramErrorConfig.test_1.parameter, + responseHelper.paramValidationError(commonParamErrorParams).toHash(errorConfig).err.error_data[0].parameter + ); + }); + + it('Should return message in error_data object as per code in input', function() { + assert.equal( + paramErrorConfig.test_1.message, + responseHelper.paramValidationError(commonParamErrorParams).toHash(errorConfig).err.error_data[0].msg + ); + }); + + it('Should return a Result object', function() { + var obj = responseHelper.paramValidationError(commonParamErrorParams); + + assert.equal(true, responseHelper.isCustomResult(obj)); + }); +}); diff --git a/tests/mocha/lib/logger/customConsoleLogger.js b/tests/mocha/lib/logger/customConsoleLogger.js new file mode 100644 index 0000000..70f7e56 --- /dev/null +++ b/tests/mocha/lib/logger/customConsoleLogger.js @@ -0,0 +1,72 @@ +// Load external packages +const Chai = require('chai'), + assert = Chai.assert; + +const rootPrefix = '../../../..', + Base = require(rootPrefix + '/index'), + Logger = Base.Logger; + +const SUPPORTED_LOG_LEVEL_KEYS = ['OFF', 'FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'ALL']; +const SUPPORTED_METHODS = ['notify', 'error', 'warn', 'info', 'step', 'win', 'debug', 'log', 'dir', 'trace']; + +const createLoggerInstanceValidator = function(moduleName, logLevel) { + return function() { + let len = SUPPORTED_METHODS.length, + logger = new Logger(moduleName, logLevel), + loggerMethodName, + loggerMethod; + + assert.instanceOf(logger, Logger); + + while (len--) { + loggerMethodName = SUPPORTED_METHODS[len]; + loggerMethod = logger[loggerMethodName]; + assert.isFunction(loggerMethod); + if (loggerMethodName === 'dir') { + loggerMethod.call(logger, { + message: 'logger.dir called' + }); + } else { + loggerMethod.call(logger, 'Calling', loggerMethodName); + } + } + }; +}; + +describe('lib/logger/CustomConsoleLogger', function() { + it('Logger should be a function', function() { + assert.isFunction(Logger); + }); + + it('should be support all log levels', function() { + let supportedLogLevels = Logger.LOG_LEVELS; + assert.typeOf(supportedLogLevels, 'object'); + + let len = SUPPORTED_LOG_LEVEL_KEYS.length, + logLevelKey; + while (len--) { + logLevelKey = SUPPORTED_LOG_LEVEL_KEYS[len]; + assert.typeOf(supportedLogLevels[logLevelKey], 'number'); + } + }); + + let len = SUPPORTED_LOG_LEVEL_KEYS.length, + supportedLogLevels = Logger.LOG_LEVELS, + logLevelKey, + logLevelNum, + strValidator, + numValidator, + moduleName; + while (len--) { + logLevelKey = SUPPORTED_LOG_LEVEL_KEYS[len]; + logLevelNum = supportedLogLevels[logLevelKey]; + + moduleName = 'STR_' + logLevelKey; + strValidator = createLoggerInstanceValidator(moduleName, logLevelKey); + it('should support all methods for log-level: ' + logLevelKey + ' and module-name: ' + moduleName, strValidator); + + moduleName = 'NUM_' + logLevelKey + '_' + logLevelNum; + numValidator = createLoggerInstanceValidator(moduleName, logLevelNum); + it('should support all methods for log-level: ' + logLevelKey + ' and module-name: ' + moduleName, numValidator); + } +}); diff --git a/tests/mocha/lib/promiseContext/promiseContext.js b/tests/mocha/lib/promiseContext/promiseContext.js new file mode 100644 index 0000000..aeed5fc --- /dev/null +++ b/tests/mocha/lib/promiseContext/promiseContext.js @@ -0,0 +1,166 @@ +//import { resolve } from 'path'; + +// Load external packages +const Chai = require('chai'), + assert = Chai.assert; + +const rootPrefix = '../../../..', + Base = require(rootPrefix + '/index'), + PromiseContext = Base.CustomPromise.Context; + +// For the purpose of test, set default timeout to 3 seconds. +PromiseContext.prototype.timeoutInMilliSecs = 3000; + +let netExecutionTime = PromiseContext.prototype.timeoutInMilliSecs * 2; + +// This executor will always resolve with true as result. +const executorThatResolves = function(resolve, reject) { + //I will resolve after 1 second + setTimeout(function() { + resolve(true); + }, 1000); +}; + +// This executor will always reject with error object as reason. +const executorThatRejects = function(resolve, reject) { + //I will reject after 1 sec. + setTimeout(function() { + reject(new Error('This promise is supposed to be rejected')); + }, 1000); +}; + +// This executor will not do anything. +const executorThatTimesout = function(resolve, reject) { + //I am doing nothing. + //...still nothing. + //...still nothing. + //...still nothing. +}; + +const configTestCases = [ + { name: 'resolved', executor: executorThatResolves }, + { name: 'rejected', executor: executorThatRejects }, + { name: 'timedout', executor: executorThatTimesout } +]; + +const createTestCasesForOptions = function(optionsDesc, options) { + optionsDesc = optionsDesc || ''; + options = options || {}; + let defaultExecutorParams = { + resolved: false, + timedout: false, + rejected: false, + completedAfterTimeout: false + }, + executorParams, + testCaseName; + configTestCases.forEach(function(testCase) { + executorParams = Object.create(defaultExecutorParams); + testCaseName = testCase.name; + executorParams[testCaseName] = true; + if (testCaseName == 'timedout') { + if (options.resolvePromiseOnTimeout) { + executorParams.resolved = true; + executorParams.completedAfterTimeout = true; + if (options.resolvedValueOnTimeout) { + executorParams.resolvedValue = options.resolvedValueOnTimeout; + } + } else if (options.rejectPromiseOnTimeout) { + executorParams.rejected = true; + executorParams.completedAfterTimeout = true; + if (options.rejectedReasonOnTimeout) { + executorParams.rejectedReason = options.rejectedReasonOnTimeout; + } + } + } + let promise = new PromiseContext(testCase.executor, options, executorParams); + let outParamsOfPromise = bindPromiseContextCallbacks(promise); + let Validator = function(done) { + validatePromiseContext(promise, options, done, outParamsOfPromise); + }; + it(optionsDesc + ' :: Promise should be ' + testCaseName, Validator); + }); +}; + +const bindPromiseContextCallbacks = function(promiseContext) { + let outParams = { + resolved: false, + timedout: false, + rejected: false, + completedAfterTimeout: false, + rejectedReason: null, + resolvedValue: null + }; + + promiseContext.onResolved = function() { + outParams.resolved = true; + }; + + promiseContext.onRejected = function(reason) { + outParams.rejected = true; + }; + + promiseContext.onTimedout = function() { + outParams.timedout = true; + }; + + promiseContext.onCompletedAfterTimeout = function() { + outParams.completedAfterTimeout = true; + }; + + let currPromise = promiseContext.getPromise(); + + currPromise + .then(function(resolvedValue) { + outParams.resolvedValue = resolvedValue; + }) + .catch(function(rejectedReason) { + //Do nothing. Validator should get the required reason. + outParams.rejectedReason = rejectedReason; + }); + + return outParams; +}; + +const validatePromiseContext = function(promiseContext, options, done, outParams) { + let expectedFlagSet = promiseContext.getExecutorParams(); + let buffer = 2000; + + let validateAfter = promiseContext.timeoutInMilliSecs + buffer; + validateAfter = validateAfter - (Date.now() - promiseContext.executionTs); + if (validateAfter < 0) { + validateAfter = 0; + } + + setTimeout(function() { + let flag = null; + for (flag in expectedFlagSet) { + if (!expectedFlagSet.hasOwnProperty(flag)) { + continue; + } + let outVal = outParams[flag]; + let expectedVal = expectedFlagSet[flag]; + // console.log("expectedVal",expectedVal,"outVal", outVal, "flag", flag); + assert.equal(expectedVal, outVal, 'Expected flag set not matched with output one.'); + } + done(); + }, validateAfter); +}; + +describe('lib/promiseContext/PromiseContext', function() { + createTestCasesForOptions('PromiseContext with default options'); + createTestCasesForOptions('PromiseContext with resolvePromiseOnTimeout = true', { + resolvePromiseOnTimeout: true + }); + createTestCasesForOptions('PromiseContext with rejectPromiseOnTimeout = true', { + rejectPromiseOnTimeout: true + }); + createTestCasesForOptions("PromiseContext with resolvedValueOnTimeout = 'auto-resolved'", { + resolvePromiseOnTimeout: true, + resolvedValueOnTimeout: 'auto-resolved' + }); + createTestCasesForOptions("PromiseContext with rejectedReasonOnTimeout = Error with message 'Auto-Rejected' ", { + rejectPromiseOnTimeout: true, + rejectedReasonOnTimeout: new Error('Auto-Rejected') + }); +}); diff --git a/tests/mocha/lib/promiseContext/promiseQueueManager.js b/tests/mocha/lib/promiseContext/promiseQueueManager.js new file mode 100644 index 0000000..e218243 --- /dev/null +++ b/tests/mocha/lib/promiseContext/promiseQueueManager.js @@ -0,0 +1,328 @@ +// Load external packages +const Chai = require('chai'), + assert = Chai.assert; + +const rootPrefix = '../../../..', + Base = require(rootPrefix + '/index'), + Logger = Base.Logger, + PromiseContext = Base.CustomPromise.Context, + PCQueueManager = Base.CustomPromise.QueueManager, + logger = new Logger('PQMTestCases'); + +// For the purpose of test, set default timeout to 3 seconds. +const _default_timeout_val = (PromiseContext.prototype.timeoutInMilliSecs = PCQueueManager.prototype.timeoutInMilliSecs = 3000); +const default_auto_resolve_val = (PCQueueManager.prototype.resolvedValueOnTimeout = PromiseContext.prototype.resolvedValueOnTimeout = + 'default_auto_resolved'); +const default_auto_reject_reason = (PCQueueManager.prototype.rejectedReasonOnTimeout = PromiseContext.prototype.rejectedReasonOnTimeout = new Error( + 'default_auto_rejected' +)); + +//Do not log info. +PCQueueManager.prototype.logInfoTimeInterval = 0; + +const _promises_per_queue = 3 * 10; + +// This executor will always resolve with true as result. +const _resolved_in_milisecs = 1000; +const _resolved_value = true; +const _rejected_in_milisecs = 1000; +const _rejected_error = new Error('This promise is supposed to be rejected'); +const _promise_indx_key = 'promise_indx_key'; +const _expected_value_keys = 'expected'; +const _actual_value_keys = 'actual'; + +const defaultExecutor = function(resolve, reject, executorParams, promiseContext) { + // Define Expected Values. + let expectedVals = (executorParams[_expected_value_keys] = executorParams[_expected_value_keys] || {}); + expectedVals['resolved'] = false; + expectedVals['resolvedValue'] = null; + expectedVals['rejected'] = false; + expectedVals['rejectedReason'] = null; + expectedVals['timedout'] = false; + + let promiseIndx = executorParams[_promise_indx_key]; + switch (promiseIndx % 3) { + case 0: + expectedVals['resolved'] = true; + expectedVals['resolvedValue'] = _resolved_value; + //I will resolve after 1 second + setTimeout(function() { + resolve(_resolved_value); + }, _resolved_in_milisecs); + break; + case 1: + expectedVals['rejected'] = true; + expectedVals['rejectedReason'] = _rejected_error; + //I will reject after 1 sec. + setTimeout(function() { + reject(_rejected_error); + }, _rejected_in_milisecs); + break; + default: + expectedVals['timedout'] = true; + //I am doing nothing. + //...still nothing. + //...still nothing. + //...still nothing. + break; + } + + //Define Actual Value. + let actualVals = (executorParams[_actual_value_keys] = executorParams[_actual_value_keys] || {}); + actualVals['resolved'] = false; + actualVals['resolvedValue'] = null; + actualVals['rejected'] = false; + actualVals['rejectedReason'] = null; + + setTimeout(function() { + let promiseObj = promiseContext.getPromise(); + promiseObj + .then(function(resolvedValue) { + actualVals['resolved'] = true; + actualVals['resolvedValue'] = resolvedValue; + return resolvedValue; + }) + .catch(function(rejectedReason) { + //Do nothing. Validator should get the required reason. + actualVals['rejected'] = true; + actualVals['rejectedReason'] = rejectedReason; + }); + + if (typeof promiseContext === 'undefined') { + reject(new Error('promiseContext not passed into excutor')); + return; + } + + if (typeof promiseObj === 'undefined') { + reject(new Error('promiseContext.getPromise() returned null')); + } + + if (!(promiseObj instanceof Promise)) { + reject(new Error('promiseContext.getPromise() is not an instanceof Promise.')); + } + + if (typeof executorParams === 'undefined') { + reject(new Error('executorParams not passed into excutor')); + return; + } + }, 10); +}; + +const bindCallbacks = function(promiseQueue) { + let outParams = { + maxZombieCountReachedCount: 0, + allPromisesCompletedCount: 0, + promiseContexts: {}, + promiseOutParams: {} + }; + + const getContextOutParams = function(promiseContext) { + let executorParams = promiseContext.getExecutorParams(), + promiseIndx, + promiseKey = (promiseIndx = executorParams[_promise_indx_key]); + + if (!outParams.promiseContexts[promiseKey]) { + outParams.promiseContexts[promiseKey] = promiseContext; + } + + let pcOutParams = outParams.promiseOutParams[promiseKey]; + if (!pcOutParams) { + pcOutParams = outParams.promiseOutParams[promiseKey] = { + resolved: false, + timedout: false, + rejected: false, + completed: false, + rejectedReason: null, + resolvedValue: null + }; + } + return pcOutParams; + }; + + promiseQueue.onPromiseResolved = function(resolvedValue, promiseContext) { + let pcOutParams = getContextOutParams(promiseContext); + pcOutParams.resolved = true; + pcOutParams.resolvedValue = resolvedValue; + }; + + promiseQueue.onPromiseRejected = function(rejectReason, promiseContext) { + let pcOutParams = getContextOutParams(promiseContext); + pcOutParams.rejected = true; + pcOutParams.rejectedReason = rejectReason; + }; + + promiseQueue.onPromiseTimedout = function(promiseContext) { + let pcOutParams = getContextOutParams(promiseContext); + pcOutParams.timedout = true; + }; + + promiseQueue.onPromiseCompleted = function(promiseContext) { + let pcOutParams = getContextOutParams(promiseContext); + pcOutParams.completed = true; + }; + + promiseQueue.onMaxZombieCountReached = function() { + outParams.maxZombieCountReachedCount++; + }; + + promiseQueue.onAllPromisesCompleted = function() { + outParams.allPromisesCompletedCount++; + }; + + return outParams; +}; + +const validatePromiseQueue = function(promiseQueue, options, expectedParamsOfQueue, outParamsOfQueue, done) { + let executorParams, pcExpected, pcActual, queueExpected, queueActual, promiseContext, promiseContextIndx; + + //Check if promiseQueue is valid. + assert.strictEqual(promiseQueue.isValid(), true, 'PromiseQueue is not valid.'); + + //Check if allPromisesCompleted has been triggered at-least once. + assert.isAtLeast( + outParamsOfQueue.allPromisesCompletedCount, + 1, + 'onAllPromisesCompleted should have been called atleast once' + ); + + for (let expectedKey in expectedParamsOfQueue) { + if (expectedParamsOfQueue.hasOwnProperty(expectedKey)) { + assert.strictEqual( + expectedParamsOfQueue[expectedKey], + outParamsOfQueue[expectedKey], + 'expected value of (PromiseQueue) ' + + expectedKey + + '(' + + expectedParamsOfQueue[expectedKey] + + ' did not match actual value ' + + outParamsOfQueue[expectedKey] + ); + } + } + + for (let promiseKey in outParamsOfQueue.promiseContexts) { + promiseContext = outParamsOfQueue.promiseContexts[promiseKey]; + queueActual = outParamsOfQueue.promiseOutParams[promiseKey]; + executorParams = promiseContext.getExecutorParams(); + pcExpected = executorParams[_expected_value_keys]; + pcActual = executorParams[_actual_value_keys]; + + for (let valKey in pcExpected) { + if (pcExpected.hasOwnProperty(valKey) && pcActual.hasOwnProperty(valKey)) { + assert.strictEqual( + pcActual[valKey], + pcExpected[valKey], + 'actual value of (PromiseContext) ' + + valKey + + '(' + + pcActual[valKey] + + ') did not match expected value ' + + pcExpected[valKey] + ); + } + + if (queueActual[valKey] !== pcExpected[valKey]) { + logger.log('---valKey', valKey, '--- queueActual'); + logger.log(queueActual); + logger.log('--- pcExpected'); + logger.log(pcExpected); + } + + assert.strictEqual( + queueActual[valKey], + pcExpected[valKey], + 'actual value of (PromiseContext)' + + valKey + + '(' + + queueActual[valKey] + + ') did not match expected value ' + + pcExpected[valKey] + ); + } + } + + done(); +}; + +const run_test_cases_after = 2 * _default_timeout_val; +let test_start_time = 0; + +const createTestCasesForOptions = function(optionsDesc, options) { + test_start_time = test_start_time || Date.now(); + options = options || {}; + let promiseQueue = new PCQueueManager(defaultExecutor, options); + let len = _promises_per_queue, + outParamsOfQueue = bindCallbacks(promiseQueue), + expectedParamsOfQueue = { + maxZombieCountReachedCount: 0, + allPromisesCompletedCount: 1 + }, + executorParams, + expectedVals, + timeoutCnt; + + timeoutCnt = 0; + while (len--) { + executorParams = {}; + executorParams[_promise_indx_key] = len; + + promiseQueue.createPromise(executorParams); + expectedVals = executorParams[_expected_value_keys]; + if (expectedVals.timedout) { + timeoutCnt++; + if (options.resolvePromiseOnTimeout) { + expectedVals.resolved = true; + expectedVals.resolvedValue = options.resolvedValueOnTimeout || default_auto_resolve_val; + } else if (options.rejectPromiseOnTimeout) { + expectedVals.rejected = true; + expectedVals.rejectedReason = options.rejectedReasonOnTimeout || default_auto_reject_reason; + } + } + + if (options.maxZombieCount && !options.resolvePromiseOnTimeout && !options.rejectPromiseOnTimeout) { + // onMaxZombieCountReached should be triggered only once. + expectedParamsOfQueue.maxZombieCountReachedCount = 1; + } + } + + let Validator = function(done) { + let validateAfter = run_test_cases_after; + validateAfter = validateAfter - (Date.now() - test_start_time); + if (validateAfter < 0) { + validateAfter = 0; + } + setTimeout(function() { + validatePromiseQueue(promiseQueue, options, expectedParamsOfQueue, outParamsOfQueue, done); + }, validateAfter); + }; + + it(optionsDesc, Validator); +}; + +describe('lib/promiseContext/PromiseQueueManager', function() { + createTestCasesForOptions('CustomPromise.QueueManager with default options'); + createTestCasesForOptions('CustomPromise.QueueManager with resolvePromiseOnTimeout = true', { + resolvePromiseOnTimeout: true + }); + + createTestCasesForOptions("CustomPromise.QueueManager with resolvedValueOnTimeout = 'auto_resolved_from_options'", { + resolvePromiseOnTimeout: true, + resolvedValueOnTimeout: 'auto_resolved_from_options' + }); + + createTestCasesForOptions('CustomPromise.QueueManager with rejectPromiseOnTimeout = true', { + rejectPromiseOnTimeout: true + }); + + createTestCasesForOptions( + "CustomPromise.QueueManager with rejectedReasonOnTimeout = Error with message 'auto_rejected_from_options' ", + { + rejectPromiseOnTimeout: true, + rejectedReasonOnTimeout: new Error('auto_rejected_from_options') + } + ); + + createTestCasesForOptions('CustomPromise.QueueManager with maxZombieCount = 2', { + maxZombieCount: 2 + }); +});