'use strict';
/* eslint-disable security/detect-object-injection */
/**
* Module dependencies, required for ALL Twyr' modules
* @ignore
*/
/**
* Module dependencies, required for this module
* @ignore
*/
const TwyrBaseClass = require('./twyr-base-class').TwyrBaseClass,
TwyrBaseError = require('./twyr-base-error').TwyrBaseError;
/**
* @class TwyrBaseModule
* @extends {TwyrBaseClass}
* @classdesc The Twyr Server Base Class for all Modules.
*
* @param {TwyrBaseModule} [parent] - The parent module, if any.
* @param {TwyrModuleLoader} [loader] - The loader to be used for managing modules' lifecycle, if any.
*
* @description
* Serves as the "base class" for all other classes in the Twyr Web Application Server, including {@link TwyrApplication}.
* 1. Defines the "lifecycle" hooks - {@link TwyrBaseModule#load}, {@link TwyrBaseModule#initialize}, {@link TwyrBaseModule#start}, {@link TwyrBaseModule#stop}, {@link TwyrBaseModule#uninitialize}, and {@link TwyrBaseModule#unload}
* + Defines the basic property - {@link TwyrBaseModule#dependencies}.
*
*/
class TwyrBaseModule extends TwyrBaseClass {
// #region Constructor
constructor(parent, loader) {
super();
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::constructor`);
let actualLoader = loader;
if(!loader) {
const TwyrModuleLoader = require('./twyr-module-loader').TwyrModuleLoader;
actualLoader = new TwyrModuleLoader(this);
}
Object.defineProperties(this, {
'$parent': {
'get': () => {
return parent;
}
},
'$loader': {
'value': actualLoader,
'writable': !loader
},
'$locale': {
'value': parent ? parent.$locale : 'en',
'configurable': !parent
}
});
}
// #endregion
// #region Lifecycle hooks
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name load
*
* @param {ConfigurationService} configSrvc - Instance of the {@link ConfigurationService} that supplies configuration.
*
* @returns {Object} - The aggregated status returned by sub-modules (if any) once they complete their loading.
*
* @summary Loads sub-modules, if any.
*
* @description
* 1. Use the supplied {@link ConfigurationService} instance (if any), to get / store both configuration and state.
* + Call the loader (typically, {@link TwyrModuleLoader#load}) to load sub-modules, if any.
*/
async load(configSrvc) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::load`);
try {
let config = this.$config || { 'state': true };
if(configSrvc) config = await configSrvc.loadConfiguration(this);
config = config || this.$config || { 'state': true };
this.$config = config.configuration;
this.$enabled = (config.state === true);
const subModuleStatus = await this.$loader.load(configSrvc);
return subModuleStatus;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::load error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name initialize
*
* @returns {Object} - The aggregated status returned by sub-modules (if any) once they complete their initialization.
*
* @summary Initializes sub-modules, if any.
*
* @description
* Call the loader (typically, {@link TwyrModuleLoader#initialize}) to initialize sub-modules, if any.
*/
async initialize() {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::initialize`);
try {
const subModuleStatus = await this.$loader.initialize();
return subModuleStatus;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::initialize error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name start
*
* @param {Object} dependencies - Interfaces to {@link TwyrBaseService} instances that this module depends on.
*
* @returns {Object} - The aggregated status returned by sub-modules (if any) once they complete their startup sequences.
*
* @summary Starts sub-modules, if any.
*
* @description
* Call the loader (typically, {@link TwyrModuleLoader#start}) to start sub-modules, if any.
*/
async start(dependencies) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::start`);
try {
this.$dependencies = dependencies;
// First, set state to true so the module sets itself up
const actualState = this.$enabled;
this.$enabled = true;
// Do the same for all of the sub-modules
const subModuleStatus = await this.$loader.start();
// Now, set the actual state, if required
if(!actualState) await this._changeState(actualState);
await this._setup();
return subModuleStatus;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::start error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name stop
*
* @returns {Object} - The aggregated status returned by sub-modules (if any) once they complete their shutdown sequences.
*
* @summary Stops sub-modules, if any.
*
* @description
* Call the loader (typically, {@link TwyrModuleLoader#stop}) to shutdown sub-modules, if any.
*/
async stop() {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::stop`);
try {
await this._teardown();
const subModuleStatus = await this.$loader.stop();
return subModuleStatus;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::stop error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name uninitialize
*
* @returns {Object} - The aggregated status returned by sub-modules (if any) once they uninitialize themselves.
*
* @summary Uninitializes sub-modules, if any.
*
* @description
* Call the loader (typically, {@link TwyrModuleLoader#uninitialize}) to uninitialize sub-modules, if any.
*/
async uninitialize() {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::uninitialize`);
try {
const subModuleStatus = await this.$loader.uninitialize();
return subModuleStatus;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::uninitialize error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name unload
*
* @returns {Object} - The aggregated status returned by sub-modules (if any) once they unload themselves.
*
* @summary Unloads sub-modules, if any.
*
* @description
* Call the loader (typically, {@link TwyrModuleLoader#unload}) to unload sub-modules, if any.
*/
async unload() {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::unload`);
try {
const subModuleStatus = await this.$loader.unload();
return subModuleStatus;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::unload error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _setup
*
* @returns {undefined} Nothing.
*
* @summary To be implemented by derived classes for setting themselves up.
*/
async _setup() {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_setup`);
return null;
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _teardown
*
* @returns {undefined} Nothing.
*
* @summary To be implemented by derived classes for un-setting themselves down.
*/
async _teardown() {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_teardown`);
return null;
}
// #endregion
// #region Configuration Change Handlers
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _reconfigure
*
* @param {Object} newConfig - The changed confoguration.
*
* @returns {undefined} Nothing.
*
* @summary Changes the configuration of this module, and informs everyone interested.
*/
async _reconfigure(newConfig) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_reconfigure`);
try {
// Step 1: If the config has not changed, do nothing
const deepEqual = require('deep-equal');
if(deepEqual(newConfig, this.$config))
return null;
// Step 2: If the module is currently disabled, store the config
// and return
if(!this.$enabled) {
this.$config = JSON.parse(JSON.stringify(newConfig || {}));
return null;
}
// Step 3: Config has changed, and the module is active
// So recycle the module - teardown, copy config, and setup
await this._teardown();
this.$config = JSON.parse(JSON.stringify(newConfig || {}));
await this._setup();
// Step 4: Go up the hierarchy and let the parent modules react
if(this.$parent) await this.$parent._subModuleReconfigure(this);
// Step 5: Let the sub-modules know about the change in configuration
for(const subModules of [this.$components, this.$features, this.$middlewares, this.$services, this.$templates]) {
if(!subModules) continue;
const subModuleNames = Object.keys(subModules);
for(const subModuleName of subModuleNames) {
const subModule = subModules[subModuleName];
await subModule._parentReconfigure();
}
}
// Step 6: Now that the entire hierarchy has been informed, let the modules
// that depend on this one know about the state change
if(!this.$dependants) return null;
const dependantNames = Object.keys(this.$dependants);
for(const dependantName of dependantNames) {
const dependant = this.$dependants[dependantName];
await dependant._dependencyReconfigure(this);
}
return null;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::_reconfigure error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _subModuleReconfigure
*
* @param {TwyrBaseModule} subModule - The sub-module that changed configuration.
*
* @returns {undefined} Nothing.
*
* @summary Lets the module know that one of its subModules changed configuration.
*/
async _subModuleReconfigure(subModule) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_subModuleReconfigure: ${subModule.name}`);
return null;
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _parentReconfigure
*
* @returns {undefined} Nothing.
*
* @summary Lets the module know that its parent changed configuration.
*/
async _parentReconfigure() {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_parentReconfigure: ${this.$parent.name}`);
return null;
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _dependencyReconfigure
*
* @param {TwyrBaseModule} dependency - The dependency that changed configuration.
*
* @returns {undefined} Nothing.
*
* @summary Lets the module know that one of its dependencies changed configuration.
*/
async _dependencyReconfigure(dependency) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_dependencyReconfigure: ${dependency.name}`);
return null;
}
// #endregion
// #region State Change Handlers
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _changeState
*
* @param {Object} newState - The next state of this module.
*
* @returns {undefined} Nothing.
*
* @summary Enables / disables this module, and all its sub-modules (if any).
*/
async _changeState(newState) {
if(this.$enabled === newState)
return null;
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_changeState`);
try {
// Step 1: Go up the hierarcy and let the parent modules reset themselves
if(this.$parent) await this.$parent._subModuleStateChange(this, newState);
// Step 2: Let the sub-modules know about the change in state
for(const subModules of [this.$components, this.$features, this.$middlewares, this.$services, this.$templates]) {
if(!subModules) continue;
const subModuleNames = Object.keys(subModules);
for(const subModuleName of subModuleNames) {
const subModule = subModules[subModuleName];
await subModule._parentStateChange(newState);
}
}
// Step 3: Now that the entire hierarchy has changed state, let the modules
// that depend on this one know about the state change
if(!this.$dependants) return null;
const dependantNames = Object.keys(this.$dependants);
for(const dependantName of dependantNames) {
const dependant = this.$dependants[dependantName];
await dependant._dependencyStateChange(this, newState);
}
return null;
}
catch(err) {
throw new TwyrBaseError(`${this.name}::_changeState error`, err);
}
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _subModuleStateChange
*
* @param {TwyrBaseModule} subModule - The sub-module that changed state.
* @param {Object} newState - The next state of the sub-module.
*
* @returns {undefined} Nothing.
*
* @summary Lets the module know that one of its subModules changed state.
*/
async _subModuleStateChange(subModule, newState) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_subModuleStateChange::${subModule.name}: ${newState}`);
return null;
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _parentStateChange
*
* @param {Object} newState - The next state of the parent module.
*
* @returns {undefined} Nothing.
*
* @summary Lets the module know that its parent changed state.
*/
async _parentStateChange(newState) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_parentStateChange::${this.$parent.name}: ${newState}`);
await this._changeState(newState);
return null;
}
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _dependencyStateChange
*
* @param {TwyrBaseModule} dependency - The dependency that changed state.
* @param {Object} newState - The next state of the dependency.
*
* @returns {undefined} Nothing.
*
* @summary Lets the module know that one of its dependencies changed state.
*/
async _dependencyStateChange(dependency, newState) {
if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::_dependencyChangeState::${dependency.name}: ${newState}`);
if(!this.$dependencies) return null;
// Since dependencies return thir interface only if they are enabled, this is a good way
// to switch this module to enabled/disabled state - irrespective of the actual newState.
let allDependenciesEnabled = true;
Object.keys(this.$dependencies).forEach((dependencyName) => {
allDependenciesEnabled = allDependenciesEnabled && this.$dependencies[dependencyName];
});
await this._changeState(allDependenciesEnabled);
return null;
}
// #endregion
// #region Utility methods
/**
* @async
* @function
* @instance
* @memberof TwyrBaseModule
* @name _exists
*
* @param {string} filepath - Path of the filesystem entity.
* @param {number} mode - Permission to be checked for.
*
* @returns {boolean} True / False.
*
* @summary Checks to see if the path can be accessed by this process using the mode specified.
*/
async _exists(filepath, mode) {
const Promise = require('bluebird'),
filesystem = require('fs-extra');
return new Promise((resolve, reject) => {
try {
filesystem.access(filepath, (mode || filesystem.constants.F_OK), (exists) => {
resolve(!exists);
});
}
catch(err) {
const error = new TwyrBaseError(`${this.$twyrModule.name}::loader::_findFiles error`, err);
reject(error);
}
});
}
_dummyAsync() {
const Promise = require('bluebird');
return new Promise((resolve) => {
resolve();
});
}
// #endregion
// #region Properties
/**
* @member {Object} dependencies
* @instance
* @memberof TwyrBaseModule
*
* @readonly
*/
get dependencies() {
return [].concat(super.dependencies || []);
}
/**
* @override
*/
get basePath() {
return __dirname;
}
// #endregion
}
exports.TwyrBaseModule = TwyrBaseModule;