From 636cfdb355f84f1f8e0e33c326abab96b53bd46a Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 10:53:27 +0200 Subject: [PATCH 01/35] Update controller/action to typescript --- .../src/controller/{action.js => action.ts} | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) rename framework/src/controller/{action.js => action.ts} (61%) diff --git a/framework/src/controller/action.js b/framework/src/controller/action.ts similarity index 61% rename from framework/src/controller/action.js rename to framework/src/controller/action.ts index 006b0073fc6..d6ca226ee8f 100644 --- a/framework/src/controller/action.js +++ b/framework/src/controller/action.ts @@ -12,15 +12,29 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const assert = require('assert'); +import { strict as assert } from 'assert'; const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; -class Action { - constructor(name, params = null, source = null) { +export interface ActionObject { + readonly module: string; + readonly name: string; + readonly source?: string; + readonly params?: object; +} + +export class Action { + public module: string; + public name: string; + public source?: string; + public params?: object; + + public constructor( + name: string, + params: object | undefined, + source: string | undefined, + ) { assert( actionWithModuleNameReg.test(name), `Action name "${name}" must be a valid name with module name.`, @@ -37,17 +51,11 @@ class Action { } } - serialize() { - return { - name: this.name, - module: this.module, - source: this.source, - params: this.params, - }; - } + public static deserialize(data: ActionObject | string): Action { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsedAction: ActionObject = + typeof data === 'string' ? JSON.parse(data) : data; - static deserialize(data) { - const parsedAction = typeof data === 'string' ? JSON.parse(data) : data; return new Action( `${parsedAction.module}:${parsedAction.name}`, parsedAction.params, @@ -55,13 +63,20 @@ class Action { ); } - toString() { - return `${this.source} -> ${this.module}:${this.name}`; + public serialize(): ActionObject { + return { + name: this.name, + module: this.module, + source: this.source, + params: this.params, + }; + } + + public toString(): string { + return `${this.source ?? 'undefined'} -> ${this.module}:${this.name}`; } - key() { + public key(): string { return `${this.module}:${this.name}`; } } - -module.exports = Action; From 88008791ad203db75fcd6f74158ea18ad9a0097a Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 10:56:33 +0200 Subject: [PATCH 02/35] Update controller/channels/constants to typescript --- .../channels/base/{constants.js => constants.ts} | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) rename framework/src/controller/channels/base/{constants.js => constants.ts} (74%) diff --git a/framework/src/controller/channels/base/constants.js b/framework/src/controller/channels/base/constants.ts similarity index 74% rename from framework/src/controller/channels/base/constants.js rename to framework/src/controller/channels/base/constants.ts index 9ced1e52887..78d957ef137 100644 --- a/framework/src/controller/channels/base/constants.js +++ b/framework/src/controller/channels/base/constants.ts @@ -12,17 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const INTERNAL_EVENTS = Object.freeze([ +export const INTERNAL_EVENTS = Object.freeze([ 'registeredToBus', 'loading:started', 'loading:finished', ]); -const eventWithModuleNameReg = /^([^\d][\w]+)((?::[^\d][\w]+)+)$/; - -module.exports = { - eventWithModuleNameReg, - INTERNAL_EVENTS, -}; +export const eventWithModuleNameReg = /^([^\d][\w]+)((?::[^\d][\w]+)+)$/; From f00f15ba71cf12329863d10caedb926c126f631b Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 11:12:45 +0200 Subject: [PATCH 03/35] Update controller/event to typescript --- .../src/controller/{event.js => event.ts} | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) rename framework/src/controller/{event.js => event.ts} (52%) diff --git a/framework/src/controller/event.js b/framework/src/controller/event.ts similarity index 52% rename from framework/src/controller/event.js rename to framework/src/controller/event.ts index abd5085d19d..4557d9e3a3f 100644 --- a/framework/src/controller/event.js +++ b/framework/src/controller/event.ts @@ -12,25 +12,45 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; +import { strict as assert } from 'assert'; -const assert = require('assert'); +import { eventWithModuleNameReg } from './channels/base/constants'; -const { eventWithModuleNameReg } = require('./channels/base/constants'); +export interface EventObject { + readonly module: string; + readonly name: string; + readonly data?: object | string; +} + +export class Event { + public module: string; + public name: string; + public data?: object | string; -class Event { - constructor(name, data = null) { + public constructor(name: string, data?: object | string) { assert( eventWithModuleNameReg.test(name), `Event name "${name}" must be a valid name with module name.`, ); + + const [moduleName, ...eventName] = name.split(':'); + this.module = moduleName; + this.name = eventName.join(':'); this.data = data; - [, this.module, this.name] = eventWithModuleNameReg.exec(name); - // Remove the first prefixed ':' symbol - this.name = this.name.substring(1); } - serialize() { + public static deserialize(data: EventObject | string): Event { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsedEvent: EventObject = + typeof data === 'string' ? JSON.parse(data) : data; + + return new Event( + `${parsedEvent.module}:${parsedEvent.name}`, + parsedEvent.data, + ); + } + + public serialize(): EventObject { return { name: this.name, module: this.module, @@ -38,27 +58,11 @@ class Event { }; } - toString() { + public toString(): string { return `${this.module}:${this.name}`; } - key() { + public key(): string { return `${this.module}:${this.name}`; } - - static deserialize(data) { - let parsedEvent = null; - if (typeof data === 'string') { - parsedEvent = JSON.parse(data); - } else { - parsedEvent = data; - } - - return new Event( - `${parsedEvent.module}:${parsedEvent.name}`, - parsedEvent.data, - ); - } } - -module.exports = Event; From b4a95d1094a761ff7d31e691e41b1bbdf1ed29df Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 12:14:10 +0200 Subject: [PATCH 04/35] Update controller/channels/base_channel to typescript --- framework/src/controller/action.ts | 9 +- .../src/controller/channels/base_channel.js | 133 ------------------ .../src/controller/channels/base_channel.ts | 109 ++++++++++++++ framework/src/controller/event.ts | 2 + 4 files changed, 115 insertions(+), 138 deletions(-) delete mode 100644 framework/src/controller/channels/base_channel.js create mode 100644 framework/src/controller/channels/base_channel.ts diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index d6ca226ee8f..b5ce3786476 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -24,17 +24,16 @@ export interface ActionObject { readonly params?: object; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ActionCallback = (action: ActionObject) => any; + export class Action { public module: string; public name: string; public source?: string; public params?: object; - public constructor( - name: string, - params: object | undefined, - source: string | undefined, - ) { + public constructor(name: string, params?: object, source?: string) { assert( actionWithModuleNameReg.test(name), `Action name "${name}" must be a valid name with module name.`, diff --git a/framework/src/controller/channels/base_channel.js b/framework/src/controller/channels/base_channel.js deleted file mode 100644 index 54582c9cf4f..00000000000 --- a/framework/src/controller/channels/base_channel.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const Event = require('../event'); -const Action = require('../action'); -const { INTERNAL_EVENTS, eventWithModuleNameReg } = require('./base/constants'); - -const _eventsList = new WeakMap(); -const _actionsList = new WeakMap(); -const _actions = new WeakMap(); - -class BaseChannel { - constructor(moduleAlias, events, actions, options = {}) { - this.moduleAlias = moduleAlias; - this.options = options; - - const eventList = options.skipInternalEvents - ? events - : [...events, ...INTERNAL_EVENTS]; - - _eventsList.set( - this, - eventList.map(eventName => new Event(`${this.moduleAlias}:${eventName}`)), - ); - - _actionsList.set( - this, - Object.keys(actions).map( - actionName => new Action(`${this.moduleAlias}:${actionName}`), - ), - ); - - _actions.set(this, actions); - } - - get actionsList() { - return _actionsList.get(this); - } - - get eventsList() { - return _eventsList.get(this); - } - - get actions() { - return _actions.get(this); - } - - // eslint-disable-next-line @typescript-eslint/require-await,class-methods-use-this - async registerToBus() { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Listen to any event happening in the application - // Specified as moduleName:eventName - // If its related to your own moduleAlias specify as :eventName - // eslint-disable-next-line no-unused-vars, class-methods-use-this - subscribe(eventName, cb) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Publish the event on the channel - // Specified as moduleName:eventName - // If its related to your own moduleAlias specify as :eventName - // eslint-disable-next-line no-unused-vars, class-methods-use-this - publish(eventName, data) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Publish to the network by invoking send/broadcast actions in the network - // Specified as actionName for send or broadcast available on the network - // If its related to your own moduleAlias specify as :eventName - // eslint-disable-next-line no-unused-vars, class-methods-use-this - publishToNetwork(actionName, data) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Call action of any moduleAlias through controller - // Specified as moduleName:actionName - // eslint-disable-next-line @typescript-eslint/require-await, no-unused-vars, class-methods-use-this - async invoke(actionName, params) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Call action network module when requesting from the network or a specific peer - // Specified as actionName for request available on the network - // eslint-disable-next-line @typescript-eslint/require-await, no-unused-vars, class-methods-use-this - async invokeFromNetwork(actionName, params) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Call action of any moduleAlias through controller - // Specified as moduleName:actionName - // Specified action must be defined as publicly callable - // eslint-disable-next-line @typescript-eslint/require-await, no-unused-vars, class-methods-use-this - async invokePublic(actionName, params) { - throw new TypeError('This method must be implemented in child classes. '); - } - - isValidEventName(name, throwError = true) { - const result = eventWithModuleNameReg.test(name); - if (throwError && !result) { - throw new Error( - `[${this.moduleAlias.alias}] Invalid event name ${name}.`, - ); - } - return result; - } - - isValidActionName(name, throwError = true) { - const result = eventWithModuleNameReg.test(name); - if (throwError && !result) { - throw new Error( - `[${this.moduleAlias.alias}] Invalid action name ${name}.`, - ); - } - return result; - } -} - -module.exports = BaseChannel; diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts new file mode 100644 index 00000000000..6fe916632f4 --- /dev/null +++ b/framework/src/controller/channels/base_channel.ts @@ -0,0 +1,109 @@ +/* + * Copyright © 2019 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Event, EventCallback } from '../event'; +import { Action, ActionCallback } from '../action'; +import { INTERNAL_EVENTS, eventWithModuleNameReg } from './base/constants'; + +export interface BaseChannelOptions { + readonly skipInternalEvents?: boolean; +} + +export abstract class BaseChannel { + public readonly moduleAlias: string; + public readonly options: object; + + public readonly eventsList: ReadonlyArray; + public readonly actionsList: ReadonlyArray; + public readonly actions: { [key: string]: ActionCallback }; + + protected constructor( + moduleAlias: string, + events: ReadonlyArray, + actions: { [key: string]: ActionCallback }, + options: BaseChannelOptions = {}, + ) { + this.moduleAlias = moduleAlias; + this.options = options; + + const eventList = options.skipInternalEvents + ? events + : [...events, ...INTERNAL_EVENTS]; + + this.eventsList = eventList.map( + eventName => new Event(`${this.moduleAlias}:${eventName}`), + ); + + this.actionsList = Object.keys(actions).map( + actionName => new Action(`${this.moduleAlias}:${actionName}`), + ); + + this.actions = actions; + } + + public isValidEventName(name: string, throwError = true): boolean | never { + const result = eventWithModuleNameReg.test(name); + + if (throwError && !result) { + throw new Error(`[${this.moduleAlias}] Invalid event name ${name}.`); + } + return result; + } + + public isValidActionName(name: string, throwError = true): boolean | never { + const result = eventWithModuleNameReg.test(name); + + if (throwError && !result) { + throw new Error(`[${this.moduleAlias}] Invalid action name ${name}.`); + } + + return result; + } + + abstract async registerToBus(): Promise; + + // Listen to any event happening in the application + // Specified as moduleName:eventName + // If its related to your own moduleAlias specify as :eventName + abstract subscribe(eventName: string, cb: EventCallback): void; + + // Publish the event on the channel + // Specified as moduleName:eventName + // If its related to your own moduleAlias specify as :eventName + abstract publish(eventName: string, data: object): void; + + // Publish to the network by invoking send/broadcast actions in the network + // Specified as actionName for send or broadcast available on the network + // If its related to your own moduleAlias specify as :eventName + abstract publishToNetwork(actionName: string, data: object): void; + + // Call action of any moduleAlias through controller + // Specified as moduleName:actionName + abstract async invoke(actionName: string, params?: object): Promise; + + // Call action network module when requesting from the network or a specific peer + // Specified as actionName for request available on the network + abstract async invokeFromNetwork( + actionName: string, + params?: object, + ): Promise; + + // Call action of any moduleAlias through controller + // Specified as moduleName:actionName + // Specified action must be defined as publicly callable + abstract async invokePublic( + actionName: string, + params?: object, + ): Promise; +} diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 4557d9e3a3f..407e56bc305 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -22,6 +22,8 @@ export interface EventObject { readonly data?: object | string; } +export type EventCallback = (action: EventObject) => void; + export class Event { public module: string; public name: string; From a761fd68d0be095ebcf167b97cef13b32ac3b658 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 16:26:08 +0200 Subject: [PATCH 05/35] Update controller/bus to typescript --- framework/src/controller/action.ts | 5 + framework/src/controller/{bus.js => bus.ts} | 194 +++++++++++++------- framework/src/controller/event.ts | 2 + framework/tsconfig.json | 3 +- framework/types/pm2-axon-rpc/index.d.ts | 16 ++ framework/types/pm2-axon/index.d.ts | 183 ++++++++++++++++++ 6 files changed, 339 insertions(+), 64 deletions(-) rename framework/src/controller/{bus.js => bus.ts} (52%) create mode 100644 framework/types/pm2-axon-rpc/index.d.ts create mode 100644 framework/types/pm2-axon/index.d.ts diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index b5ce3786476..a9bf4aedf95 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -27,11 +27,16 @@ export interface ActionObject { // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ActionCallback = (action: ActionObject) => any; +export interface ActionsObject { + [key: string]: Action; +} + export class Action { public module: string; public name: string; public source?: string; public params?: object; + public isPublic?: boolean; public constructor(name: string, params?: object, source?: string) { assert( diff --git a/framework/src/controller/bus.js b/framework/src/controller/bus.ts similarity index 52% rename from framework/src/controller/bus.js rename to framework/src/controller/bus.ts index a70f47704ee..8d693f1ab2e 100644 --- a/framework/src/controller/bus.js +++ b/framework/src/controller/bus.ts @@ -12,18 +12,67 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const axon = require('pm2-axon'); -const { Server: RPCServer, Client: RPCClient } = require('pm2-axon-rpc'); -const { EventEmitter2 } = require('eventemitter2'); -const Action = require('./action'); +import * as axon from 'pm2-axon'; +import { Server as RPCServer, Client as RPCClient } from 'pm2-axon-rpc'; +import { EventEmitter2, Listener } from 'eventemitter2'; +import { + PubEmitterSocket, + RepSocket, + ReqSocket, + SubEmitterSocket, +} from 'pm2-axon'; +import { Action, ActionObject, ActionsObject } from './action'; +import { Logger } from '../types'; +import { BaseChannel } from './channels/base_channel'; +import { EventsArray } from './event'; const CONTROLLER_IDENTIFIER = 'app'; const SOCKET_TIMEOUT_TIME = 2000; -class Bus extends EventEmitter2 { - constructor(options, logger, config) { +interface BusConfiguration { + ipc: { + readonly enabled: boolean; + }; + socketsPath: { + readonly pub: string; + readonly sub: string; + readonly rpc: string; + }; +} + +interface RegisterChannelOptions { + readonly type: string; + readonly channel: BaseChannel; + readonly rpcSocketPath?: string; +} + +interface ChannelInfo { + readonly channel: BaseChannel; + readonly actions: ActionsObject; + readonly events: EventsArray; + readonly type: string; +} + +export class Bus extends EventEmitter2 { + public logger: Logger; + public config: BusConfiguration; + public actions: { [key: string]: Action }; + public events: { [key: string]: boolean }; + public channels: { + [key: string]: ChannelInfo; + }; + public rpcClients: { [key: string]: ReqSocket }; + public pubSocket?: PubEmitterSocket; + public subSocket?: SubEmitterSocket; + public rpcSocket?: RepSocket; + public rpcServer?: RPCServer; + public channel?: BaseChannel; + + public constructor( + options: object, + logger: Logger, + config: BusConfiguration, + ) { super(options); this.logger = logger; this.config = config; @@ -35,18 +84,18 @@ class Bus extends EventEmitter2 { this.rpcClients = {}; } - async setup() { + public async setup(): Promise { if (!this.config.ipc.enabled) { return true; } - this.pubSocket = axon.socket('pub-emitter'); + this.pubSocket = axon.socket('pub-emitter') as PubEmitterSocket; this.pubSocket.bind(this.config.socketsPath.pub); - this.subSocket = axon.socket('sub-emitter'); + this.subSocket = axon.socket('sub-emitter') as SubEmitterSocket; this.subSocket.bind(this.config.socketsPath.sub); - this.rpcSocket = axon.socket('rep'); + this.rpcSocket = axon.socket('rep') as RepSocket; this.rpcServer = new RPCServer(this.rpcSocket); this.rpcSocket.bind(this.config.socketsPath.rpc); @@ -71,22 +120,23 @@ class Bus extends EventEmitter2 { .catch(error => cb(error)); }); - return Promise.race([ + await Promise.race([ this._resolveWhenAllSocketsBound(), this._rejectWhenAnySocketFailsToBind(), this._rejectWhenTimeout(SOCKET_TIMEOUT_TIME), - ]).finally(() => { - this._removeAllListeners(); - }); + ]); + await this._removeAllListeners(); + + return true; } // eslint-disable-next-line @typescript-eslint/require-await - async registerChannel( - moduleAlias, - events, - actions, - options = { type: 'inMemory' }, - ) { + public async registerChannel( + moduleAlias: string, + events: EventsArray, + actions: ActionsObject, + options: RegisterChannelOptions, + ): Promise { events.forEach(eventName => { const eventFullName = `${moduleAlias}:${eventName}`; if (this.events[eventFullName]) { @@ -112,8 +162,12 @@ class Bus extends EventEmitter2 { let { channel } = options; if (options.rpcSocketPath) { - const rpcSocket = axon.socket('req'); + const rpcSocket = axon.socket('req') as ReqSocket; rpcSocket.connect(options.rpcSocketPath); + + // TODO: Fix this override + // eslint-disable-next-line + // @ts-ignore channel = new RPCClient(rpcSocket); this.rpcClients[moduleAlias] = rpcSocket; } @@ -126,26 +180,37 @@ class Bus extends EventEmitter2 { }; } - async invoke(actionData) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invoke(actionData: string | ActionObject): Promise { const action = Action.deserialize(actionData); + const actionModule = action.module; + const actionFullName = action.key(); + const actionParams = action.params; - if (!this.actions[action.key()]) { + if (this.actions[actionFullName] === undefined) { throw new Error(`Action '${action.key()}' is not registered to bus.`); } - if (action.module === CONTROLLER_IDENTIFIER) { - return this.channels[CONTROLLER_IDENTIFIER].channel.invoke(action); + const channelInfo = this.channels[actionFullName]; + + if (actionModule === CONTROLLER_IDENTIFIER) { + return channelInfo.channel.invoke(actionFullName, actionParams); } - if (this.channels[action.module].type === 'inMemory') { - return this.channels[action.module].channel.invoke(action); + if (channelInfo.type === 'inMemory') { + return channelInfo.channel.invoke(actionFullName, actionParams); } return new Promise((resolve, reject) => { - this.channels[action.module].channel.call( + // TODO: Fix when both channel types are converted to typescript + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + (channelInfo.channel as any).call( 'invoke', action.serialize(), - (err, data) => { + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err?: string | object, data: any) => { if (err) { return reject(err); } @@ -155,11 +220,12 @@ class Bus extends EventEmitter2 { }); } - async invokePublic(actionData) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invokePublic(actionData: string | ActionObject): Promise { const action = Action.deserialize(actionData); // Check if action exists - if (!this.actions[action.key()]) { + if (this.actions[action.key()] === undefined) { throw new Error(`Action '${action.key()}' is not registered to bus.`); } @@ -173,7 +239,8 @@ class Bus extends EventEmitter2 { return this.invoke(actionData); } - publish(eventName, eventValue) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types + public publish(eventName: string, eventValue: any): void | never { if (!this.getEvents().includes(eventName)) { throw new Error(`Event ${eventName} is not registered to bus.`); } @@ -182,11 +249,11 @@ class Bus extends EventEmitter2 { // Communicate through unix socket if (this.config.ipc.enabled) { - this.pubSocket.emit(eventName, eventValue); + this.pubSocket?.emit(eventName, eventValue); } } - subscribe(eventName, cb) { + public subscribe(eventName: string, cb: Listener): void { if (!this.getEvents().includes(eventName)) { this.logger.info( `Event ${eventName} was subscribed but not registered to the bus yet.`, @@ -198,11 +265,13 @@ class Bus extends EventEmitter2 { // Communicate through unix socket if (this.config.ipc.enabled) { - this.subSocket.on(eventName, cb); + this.subSocket?.on(eventName, cb); } } - once(eventName, cb) { + // eslint-disable-next-line + // @ts-ignore + public once(eventName: string, cb: Listener): void { if (!this.getEvents().includes(eventName)) { this.logger.info( `Event ${eventName} was subscribed but not registered to the bus yet.`, @@ -210,25 +279,25 @@ class Bus extends EventEmitter2 { } // Communicate through event emitter - super.once(eventName, cb); + super.once([eventName], cb); // TODO: make it `once` instead of `on` // Communicate through unix socket if (this.config.ipc.enabled) { - this.subSocket.on(eventName, cb); + this.subSocket?.on(eventName, cb); } } - getActions() { + public getActions(): ReadonlyArray { return Object.keys(this.actions); } - getEvents() { + public getEvents(): ReadonlyArray { return Object.keys(this.events); } // eslint-disable-next-line @typescript-eslint/require-await - async cleanup() { + public async cleanup(): Promise { if (this.pubSocket) { this.pubSocket.close(); } @@ -240,8 +309,8 @@ class Bus extends EventEmitter2 { } } - async _resolveWhenAllSocketsBound() { - return Promise.all([ + private async _resolveWhenAllSocketsBound(): Promise { + await Promise.all([ new Promise(resolve => { /* Here, the reason of calling .sock.once instead of pubSocket.once @@ -249,37 +318,37 @@ class Bus extends EventEmitter2 { However the actual socket does, by inheriting it from EventEmitter prototype */ - this.subSocket.sock.once('bind', () => { + this.subSocket?.sock.once('bind', () => { resolve(); }); }), new Promise(resolve => { - this.pubSocket.sock.once('bind', () => { + this.pubSocket?.sock.once('bind', () => { resolve(); }); }), new Promise(resolve => { - this.rpcSocket.once('bind', () => { + this.rpcSocket?.once('bind', () => { resolve(); }); }), ]); } - async _rejectWhenAnySocketFailsToBind() { - return Promise.race([ + private async _rejectWhenAnySocketFailsToBind(): Promise { + await Promise.race([ new Promise((_, reject) => { - this.subSocket.sock.once('error', () => { + this.subSocket?.sock.once('error', () => { reject(); }); }), new Promise((_, reject) => { - this.pubSocket.sock.once('error', () => { + this.pubSocket?.sock.once('error', () => { reject(); }); }), new Promise((_, reject) => { - this.rpcSocket.once('error', () => { + this.rpcSocket?.once('error', () => { reject(); }); }), @@ -287,7 +356,7 @@ class Bus extends EventEmitter2 { } // eslint-disable-next-line class-methods-use-this - async _rejectWhenTimeout(timeInMillis) { + private async _rejectWhenTimeout(timeInMillis: number): Promise { return new Promise((_, reject) => { setTimeout(() => { reject(new Error('Bus sockets setup timeout')); @@ -295,14 +364,13 @@ class Bus extends EventEmitter2 { }); } - _removeAllListeners() { - this.subSocket.sock.removeAllListeners('bind'); - this.subSocket.sock.removeAllListeners('error'); - this.pubSocket.sock.removeAllListeners('bind'); - this.pubSocket.sock.removeAllListeners('error'); - this.rpcSocket.removeAllListeners('bind'); - this.rpcSocket.removeAllListeners('error'); + // eslint-disable-next-line @typescript-eslint/require-await + private async _removeAllListeners(): Promise { + this.subSocket?.sock.removeAllListeners('bind'); + this.subSocket?.sock.removeAllListeners('error'); + this.pubSocket?.sock.removeAllListeners('bind'); + this.pubSocket?.sock.removeAllListeners('error'); + this.rpcSocket?.removeAllListeners('bind'); + this.rpcSocket?.removeAllListeners('error'); } } - -module.exports = Bus; diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 407e56bc305..7fda7e9391f 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -24,6 +24,8 @@ export interface EventObject { export type EventCallback = (action: EventObject) => void; +export type EventsArray = ReadonlyArray; + export class Event { public module: string; public name: string; diff --git a/framework/tsconfig.json b/framework/tsconfig.json index d229aa4d433..8069318b08e 100644 --- a/framework/tsconfig.json +++ b/framework/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig", "compilerOptions": { "allowJs": true, - "outDir": "dist-node" + "outDir": "dist-node", + "typeRoots": ["../node_modules/@types", "node_modules/@types", "./types"] }, "include": ["src/**/*"] } diff --git a/framework/types/pm2-axon-rpc/index.d.ts b/framework/types/pm2-axon-rpc/index.d.ts new file mode 100644 index 00000000000..bff9d00a32e --- /dev/null +++ b/framework/types/pm2-axon-rpc/index.d.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line max-classes-per-file +declare module 'pm2-axon-rpc' { + import { RepSocket, ReqSocket } from 'pm2-axon'; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type ServerExposeCallBack = (...args: any[]) => void; + + export class Server { + public constructor(socket: RepSocket); + public expose(name: string, cb: ServerExposeCallBack): void; + } + + export class Client { + public constructor(socket: ReqSocket); + } +} diff --git a/framework/types/pm2-axon/index.d.ts b/framework/types/pm2-axon/index.d.ts new file mode 100644 index 00000000000..2b1b9133702 --- /dev/null +++ b/framework/types/pm2-axon/index.d.ts @@ -0,0 +1,183 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// eslint-disable-next-line max-classes-per-file +declare module 'pm2-axon' { + import EventEmitter = NodeJS.EventEmitter; + import NetSocket = NodeJS.Socket; + + export class Socket extends EventEmitter { + public set(name: string, val: any): Socket; + + public get(name: string): any; + + public enable(name: string): Socket; + + public disable(name: string): Socket; + + public enabled(name: string): boolean; + + public disabled(name: string): boolean; + + public use(plugin: (socket: Socket) => any): Socket; + + public pack(args: Buffer | Buffer[]): Buffer; + + public closeSockets(): void; + + public close(): void; + + public closeServer(fn: () => any): void; + + public address(): + | { port: number; family: string; address: string; string: string } + | undefined; + + public removeSocket(sock: Socket): void; + + public addSocket(sock: Socket): void; + + public handleErrors(sock: Socket): void; + + public onmessage(sock: NetSocket): (args: Buffer | Buffer[]) => void; + + public connect( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public onconnect(sock: Socket): void; + + bind( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + } + + export class SubSocket extends Socket { + public hasSubscriptions(): boolean; + + public matches(topic: string): boolean; + + public onmessage(sock: NetSocket): (args: Buffer | Buffer[]) => void; + + public subscribe(re: RegExp | string): RegExp; + + public unsubscribe(re: RegExp | string): void; + + public clearSubscriptions(): void; + + /** + * @throws {Error} + */ + public send(): void; + } + + export class SubEmitterSocket { + public sock: Socket; + + public on(event: string, fn: (...args: any[]) => void): SubEmitterSocket; + + public off(event: string): SubEmitterSocket; + + public bind( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public connect( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public close(): void; + } + + export class PubSocket extends Socket { + public send(...args: any[]): PubSocket; + } + + export class PubEmitterSocket { + public sock: PubSocket; + + public send(...args: any[]): PubSocket; + + public bind( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public connect( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public close(): void; + + public emit(event: string, data: any): void; + } + + export class PushSocket extends Socket { + public send(...args: any[]): void; + } + + export class ReqSocket extends Socket { + public id(): string; + + public onmessage(): (args: Buffer | Buffer[]) => void; + + public send(...args: any[]): void; + } + + export class RepSocket extends Socket { + public onmessage(sock: NetSocket): (args: Buffer | Buffer[]) => void; + } + + export class PullSocket extends Socket { + /** + * @throws {Error} + */ + public send(): void; + } + + export type ConnectionPort = + | number + | string + | { + protocol?: string; + hostname?: string; + pathname: string; + port: string | number; + }; + + export function socket( + type: string, + options?: any, + ): + | PubEmitterSocket + | SubEmitterSocket + | PushSocket + | PullSocket + | PubSocket + | SubSocket + | ReqSocket + | RepSocket + | Socket; + + export const types: { + [propName: string]: () => + | PubEmitterSocket + | SubEmitterSocket + | PushSocket + | PullSocket + | PubSocket + | SubSocket + | ReqSocket + | RepSocket + | Socket; + }; +} From 1e749dfdced895a8748a7356e50495547bffbac4 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 11:04:44 +0200 Subject: [PATCH 06/35] Update controller/in_memory_channel to typescript --- framework/src/controller/action.ts | 30 +++-- framework/src/controller/bus.ts | 29 ++--- .../src/controller/channels/base_channel.ts | 40 +++--- .../controller/channels/in_memory_channel.js | 108 ---------------- .../controller/channels/in_memory_channel.ts | 115 ++++++++++++++++++ framework/src/controller/event.ts | 10 +- 6 files changed, 181 insertions(+), 151 deletions(-) delete mode 100644 framework/src/controller/channels/in_memory_channel.js create mode 100644 framework/src/controller/channels/in_memory_channel.ts diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index a9bf4aedf95..f2e488d315c 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -17,7 +17,7 @@ import { strict as assert } from 'assert'; const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; -export interface ActionObject { +export interface ActionInfoObject { readonly module: string; readonly name: string; readonly source?: string; @@ -25,7 +25,11 @@ export interface ActionObject { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ActionCallback = (action: ActionObject) => any; +export type ActionHandler = (action: ActionInfoObject) => any; + +export interface ActionsDefinition { + [key: string]: ActionHandler | { handler: ActionHandler; isPublic?: boolean }; +} export interface ActionsObject { [key: string]: Action; @@ -34,11 +38,20 @@ export interface ActionsObject { export class Action { public module: string; public name: string; + public isPublic: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public handler?: (action: ActionInfoObject) => any; public source?: string; public params?: object; - public isPublic?: boolean; - public constructor(name: string, params?: object, source?: string) { + public constructor( + name: string, + params?: object, + source?: string, + isPublic?: boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler?: (action: ActionInfoObject) => any, + ) { assert( actionWithModuleNameReg.test(name), `Action name "${name}" must be a valid name with module name.`, @@ -53,11 +66,14 @@ export class Action { ); this.source = source; } + + this.handler = handler; + this.isPublic = isPublic ?? false; } - public static deserialize(data: ActionObject | string): Action { + public static deserialize(data: ActionInfoObject | string): Action { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsedAction: ActionObject = + const parsedAction: ActionInfoObject = typeof data === 'string' ? JSON.parse(data) : data; return new Action( @@ -67,7 +83,7 @@ export class Action { ); } - public serialize(): ActionObject { + public serialize(): ActionInfoObject { return { name: this.name, module: this.module, diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 8d693f1ab2e..252ce481419 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -21,7 +21,7 @@ import { ReqSocket, SubEmitterSocket, } from 'pm2-axon'; -import { Action, ActionObject, ActionsObject } from './action'; +import { Action, ActionInfoObject, ActionsObject } from './action'; import { Logger } from '../types'; import { BaseChannel } from './channels/base_channel'; import { EventsArray } from './event'; @@ -138,25 +138,18 @@ export class Bus extends EventEmitter2 { options: RegisterChannelOptions, ): Promise { events.forEach(eventName => { - const eventFullName = `${moduleAlias}:${eventName}`; - if (this.events[eventFullName]) { - throw new Error( - `Event "${eventFullName}" already registered with bus.`, - ); + if (this.events[eventName]) { + throw new Error(`Event "${eventName}" already registered with bus.`); } - this.events[eventFullName] = true; + this.events[eventName] = true; }); Object.keys(actions).forEach(actionName => { - const actionFullName = `${moduleAlias}:${actionName}`; - - if (this.actions[actionFullName]) { - throw new Error( - `Action "${actionFullName}" already registered with bus.`, - ); + if (this.actions[actionName] === undefined) { + throw new Error(`Action "${actionName}" already registered with bus.`); } - this.actions[actionFullName] = actions[actionName]; + this.actions[actionName] = actions[actionName]; }); let { channel } = options; @@ -181,7 +174,7 @@ export class Bus extends EventEmitter2 { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invoke(actionData: string | ActionObject): Promise { + public async invoke(actionData: string | ActionInfoObject): Promise { const action = Action.deserialize(actionData); const actionModule = action.module; const actionFullName = action.key(); @@ -220,8 +213,10 @@ export class Bus extends EventEmitter2 { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invokePublic(actionData: string | ActionObject): Promise { + public async invokePublic( + actionData: string | ActionInfoObject, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { const action = Action.deserialize(actionData); // Check if action exists diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 6fe916632f4..660ba04c87c 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -13,7 +13,7 @@ */ import { Event, EventCallback } from '../event'; -import { Action, ActionCallback } from '../action'; +import { Action, ActionsDefinition, ActionsObject } from '../action'; import { INTERNAL_EVENTS, eventWithModuleNameReg } from './base/constants'; export interface BaseChannelOptions { @@ -24,14 +24,14 @@ export abstract class BaseChannel { public readonly moduleAlias: string; public readonly options: object; - public readonly eventsList: ReadonlyArray; - public readonly actionsList: ReadonlyArray; - public readonly actions: { [key: string]: ActionCallback }; + public readonly eventsList: ReadonlyArray; + public readonly actionsList: ReadonlyArray; + public readonly actions: ActionsObject; protected constructor( moduleAlias: string, events: ReadonlyArray, - actions: { [key: string]: ActionCallback }, + actions: ActionsDefinition, options: BaseChannelOptions = {}, ) { this.moduleAlias = moduleAlias; @@ -41,15 +41,29 @@ export abstract class BaseChannel { ? events : [...events, ...INTERNAL_EVENTS]; - this.eventsList = eventList.map( - eventName => new Event(`${this.moduleAlias}:${eventName}`), + this.eventsList = eventList.map(eventName => + new Event(`${this.moduleAlias}:${eventName}`).key(), ); - this.actionsList = Object.keys(actions).map( - actionName => new Action(`${this.moduleAlias}:${actionName}`), - ); - - this.actions = actions; + this.actions = {}; + for (const actionName of Object.keys(actions)) { + const actionData = actions[actionName]; + + const handler = + typeof actionData === 'object' ? actionData.handler : actionData; + const isPublic = + typeof actionData === 'object' ? actionData.isPublic ?? true : true; + + const action = new Action( + `${this.moduleAlias}:${actionName}`, + undefined, + undefined, + isPublic, + handler, + ); + this.actions[action.key()] = action; + } + this.actionsList = Object.keys(this.actions); } public isValidEventName(name: string, throwError = true): boolean | never { @@ -71,8 +85,6 @@ export abstract class BaseChannel { return result; } - abstract async registerToBus(): Promise; - // Listen to any event happening in the application // Specified as moduleName:eventName // If its related to your own moduleAlias specify as :eventName diff --git a/framework/src/controller/channels/in_memory_channel.js b/framework/src/controller/channels/in_memory_channel.js deleted file mode 100644 index 4138e950373..00000000000 --- a/framework/src/controller/channels/in_memory_channel.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const Event = require('../event'); -const Action = require('../action'); -const BaseChannel = require('./base_channel'); - -class InMemoryChannel extends BaseChannel { - constructor(moduleAlias, events, actions, options = {}) { - super(moduleAlias, events, actions, options); - } - - async registerToBus(bus) { - this.bus = bus; - await this.bus.registerChannel( - this.moduleAlias, - this.eventsList.map(event => event.name), - this.actions, - { type: 'inMemory', channel: this }, - ); - } - - subscribe(eventName, cb) { - this.bus.subscribe(new Event(eventName).key(), data => - setImmediate(cb, Event.deserialize(data)), - ); - } - - once(eventName, cb) { - this.bus.once(new Event(eventName).key(), data => - setImmediate(cb, Event.deserialize(data)), - ); - } - - publish(eventName, data) { - const event = new Event(eventName, data); - - if (event.module !== this.moduleAlias) { - throw new Error( - `Event "${eventName}" not registered in "${this.moduleAlias}" module.`, - ); - } - - this.bus.publish(event.key(), event.serialize()); - } - - // eslint-disable-next-line @typescript-eslint/require-await - async invoke(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; - - if (action.module === this.moduleAlias) { - if (!this.actions[action.name]) { - throw new Error( - `The action '${action.name}' on module '${this.moduleAlias}' does not exist.`, - ); - } - return this.actions[action.name].handler(action); - } - - return this.bus.invoke(action.serialize()); - } - - async invokeFromNetwork(remoteMethod, params) { - return this.invoke(`app:${remoteMethod}`, params); - } - - async publishToNetwork(actionName, data) { - return this.invoke(`app:${actionName}`, data); - } - - // eslint-disable-next-line @typescript-eslint/require-await - async invokePublic(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; - - if (action.module === this.moduleAlias) { - if (!this.actions[action.name].isPublic) { - throw new Error( - `Action '${action.name}' is not allowed because it's not public.`, - ); - } - - return this.actions[action.name].handler(action); - } - - return this.bus.invokePublic(action.serialize()); - } -} - -module.exports = InMemoryChannel; diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts new file mode 100644 index 00000000000..7903d512800 --- /dev/null +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -0,0 +1,115 @@ +/* + * Copyright © 2019 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Event, EventCallback } from '../event'; +import { Action } from '../action'; +import { BaseChannel } from './base_channel'; +import { Bus } from '../bus'; + +export class InMemoryChannel extends BaseChannel { + public bus: Bus | undefined; + + public async registerToBus(bus: Bus): Promise { + this.bus = bus; + + await this.bus.registerChannel( + this.moduleAlias, + this.eventsList, + this.actions, + { type: 'inMemory', channel: this }, + ); + } + + public subscribe(eventName: string, cb: EventCallback): void { + (this.bus as Bus).subscribe(new Event(eventName).key(), data => + setImmediate(cb, Event.deserialize(data)), + ); + } + + public once(eventName: string, cb: EventCallback): void { + (this.bus as Bus).once(new Event(eventName).key(), data => + setImmediate(cb, Event.deserialize(data)), + ); + } + + public publish(eventName: string, data: object): void { + const event = new Event(eventName, data); + + if (event.module !== this.moduleAlias) { + throw new Error( + `Event "${eventName}" not registered in "${this.moduleAlias}" module.`, + ); + } + + (this.bus as Bus).publish(event.key(), event.serialize()); + } + + // eslint-disable-next-line @typescript-eslint/require-await,@typescript-eslint/no-explicit-any + public async invoke(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); + + if (action.module === this.moduleAlias) { + if (this.actions[action.key()] === undefined) { + throw new Error( + `The action '${action.key()}' on module '${ + this.moduleAlias + }' does not exist.`, + ); + } + + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.actions[action.key()].handler(action.serialize()); + } + + return (this.bus as Bus).invoke(action.serialize()); + } + + public async invokeFromNetwork( + remoteMethod: string, + params?: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + return this.invoke(`app:${remoteMethod}`, params); + } + + public async publishToNetwork( + actionName: string, + data: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + return this.invoke(`app:${actionName}`, data); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invokePublic(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); + + if (action.module === this.moduleAlias) { + if (!this.actions[action.name].isPublic) { + throw new Error( + `Action '${action.name}' is not allowed because it's not public.`, + ); + } + + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.actions[action.name].handler(action.serialize()); + } + + return (this.bus as Bus).invokePublic(action.serialize()); + } +} diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 7fda7e9391f..49fe5fb7ba4 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -16,13 +16,13 @@ import { strict as assert } from 'assert'; import { eventWithModuleNameReg } from './channels/base/constants'; -export interface EventObject { +export interface EventInfoObject { readonly module: string; readonly name: string; readonly data?: object | string; } -export type EventCallback = (action: EventObject) => void; +export type EventCallback = (action: EventInfoObject) => void; export type EventsArray = ReadonlyArray; @@ -43,9 +43,9 @@ export class Event { this.data = data; } - public static deserialize(data: EventObject | string): Event { + public static deserialize(data: EventInfoObject | string): Event { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsedEvent: EventObject = + const parsedEvent: EventInfoObject = typeof data === 'string' ? JSON.parse(data) : data; return new Event( @@ -54,7 +54,7 @@ export class Event { ); } - public serialize(): EventObject { + public serialize(): EventInfoObject { return { name: this.name, module: this.module, From c84b687da3be4e08c1065d9c573d5545e5676fbf Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 12:11:35 +0200 Subject: [PATCH 07/35] Update controller/child_process_channel to typescript --- framework/src/controller/bus.ts | 13 +- .../channels/child_process/index.js | 25 --- ...ss_channel.js => child_process_channel.ts} | 194 +++++++++++------- .../channels/{index.js => index.ts} | 14 +- framework/types/pm2-axon-rpc/index.d.ts | 3 + 5 files changed, 135 insertions(+), 114 deletions(-) delete mode 100644 framework/src/controller/channels/child_process/index.js rename framework/src/controller/channels/{child_process_channel.js => child_process_channel.ts} (51%) rename framework/src/controller/channels/{index.js => index.ts} (64%) diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 252ce481419..a090f034b43 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -29,15 +29,18 @@ import { EventsArray } from './event'; const CONTROLLER_IDENTIFIER = 'app'; const SOCKET_TIMEOUT_TIME = 2000; +export interface socketPathObject { + readonly pub: string; + readonly sub: string; + readonly rpc: string; + readonly root: string; +} + interface BusConfiguration { ipc: { readonly enabled: boolean; }; - socketsPath: { - readonly pub: string; - readonly sub: string; - readonly rpc: string; - }; + socketsPath: socketPathObject; } interface RegisterChannelOptions { diff --git a/framework/src/controller/channels/child_process/index.js b/framework/src/controller/channels/child_process/index.js deleted file mode 100644 index 8205b39a583..00000000000 --- a/framework/src/controller/channels/child_process/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const setupProcessHandlers = channel => { - process.once('SIGTERM', () => channel.cleanup(1)); - process.once('SIGINT', () => channel.cleanup(1)); - process.once('exit', (error, code) => channel.cleanup(code, error)); -}; - -module.exports = { - setupProcessHandlers, -}; diff --git a/framework/src/controller/channels/child_process_channel.js b/framework/src/controller/channels/child_process_channel.ts similarity index 51% rename from framework/src/controller/channels/child_process_channel.js rename to framework/src/controller/channels/child_process_channel.ts index 32d735ad421..e58e9a46e1d 100644 --- a/framework/src/controller/channels/child_process_channel.js +++ b/framework/src/controller/channels/child_process_channel.ts @@ -12,39 +12,68 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const { EventEmitter2 } = require('eventemitter2'); -const axon = require('pm2-axon'); -const { Server: RPCServer, Client: RPCClient } = require('pm2-axon-rpc'); -const util = require('util'); -const Action = require('../action'); -const Event = require('../event'); -const BaseChannel = require('./base_channel'); -const { setupProcessHandlers } = require('./child_process'); +import { EventEmitter2 } from 'eventemitter2'; +import * as axon from 'pm2-axon'; +import { Server as RPCServer, Client as RPCClient, Client } from 'pm2-axon-rpc'; +import * as util from 'util'; +import { + PubEmitterSocket, + RepSocket, + ReqSocket, + SubEmitterSocket, +} from 'pm2-axon'; +import { Action, ActionsDefinition } from '../action'; +import { Event, EventCallback, EventInfoObject } from '../event'; +import { BaseChannel, BaseChannelOptions } from './base_channel'; +import { socketPathObject } from '../bus'; + +export const setupProcessHandlers = (channel: ChildProcessChannel): void => { + process.once('SIGTERM', () => channel.cleanup(1)); + process.once('SIGINT', () => channel.cleanup(1)); + process.once('exit', code => channel.cleanup(code)); +}; + +type NodeCallback = (error: Error | null, result?: number) => void; const SOCKET_TIMEOUT_TIME = 2000; -class ChildProcessChannel extends BaseChannel { - constructor(moduleAlias, events, actions, options = {}) { +export class ChildProcessChannel extends BaseChannel { + public localBus: EventEmitter2; + public subSocket?: SubEmitterSocket; + public busRpcSocket?: ReqSocket; + public busRpcClient?: Client; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public busRpcClientCallPromisified: any; + public pubSocket?: PubEmitterSocket; + public rpcSocketPath?: socketPathObject | string; + public rpcSocket?: RepSocket; + public rpcServer?: RPCServer; + + protected constructor( + moduleAlias: string, + events: ReadonlyArray, + actions: ActionsDefinition, + options: BaseChannelOptions = {}, + ) { super(moduleAlias, events, actions, options); this.localBus = new EventEmitter2(); setupProcessHandlers(this); } - async registerToBus(socketsPath) { - this.subSocket = axon.socket('sub-emitter'); + public async registerToBus(socketsPath: socketPathObject): Promise { + this.subSocket = axon.socket('sub-emitter') as SubEmitterSocket; this.subSocket.connect(socketsPath.pub); - this.busRpcSocket = axon.socket('req'); + this.busRpcSocket = axon.socket('req') as ReqSocket; this.busRpcSocket.connect(socketsPath.rpc); this.busRpcClient = new RPCClient(this.busRpcSocket); + // eslint-disable-next-line @typescript-eslint/unbound-method this.busRpcClientCallPromisified = util.promisify(this.busRpcClient.call); // Channel Publish Socket is only required if the module has events if (this.eventsList.length > 0) { - this.pubSocket = axon.socket('pub-emitter'); + this.pubSocket = axon.socket('pub-emitter') as PubEmitterSocket; this.pubSocket.connect(socketsPath.sub); } @@ -52,62 +81,65 @@ class ChildProcessChannel extends BaseChannel { if (this.actionsList.length > 0) { this.rpcSocketPath = `unix://${socketsPath.root}/${this.moduleAlias}_rpc.sock`; - this.rpcSocket = axon.socket('rep'); + this.rpcSocket = axon.socket('rep') as RepSocket; this.rpcSocket.bind(this.rpcSocketPath); this.rpcServer = new RPCServer(this.rpcSocket); - this.rpcServer.expose('invoke', (action, cb) => { + this.rpcServer.expose('invoke', (action: string, cb: NodeCallback) => { this.invoke(action) .then(data => cb(null, data)) .catch(error => cb(error)); }); - this.rpcServer.expose('invokePublic', (action, cb) => { - this.invokePublic(action) - .then(data => cb(null, data)) - .catch(error => cb(error)); - }); + this.rpcServer.expose( + 'invokePublic', + (action: string, cb: NodeCallback) => { + this.invokePublic(action) + .then(data => cb(null, data)) + .catch(error => cb(error)); + }, + ); } return this.setupSockets(); } - async setupSockets() { - return Promise.race([ + public async setupSockets(): Promise { + await Promise.race([ this._resolveWhenAllSocketsBound(), this._rejectWhenAnySocketFailsToBind(), this._rejectWhenTimeout(SOCKET_TIMEOUT_TIME), - ]).finally(() => { - this._removeAllListeners(); - }); + ]); + + await this._removeAllListeners(); } - subscribe(eventName, cb) { + public subscribe(eventName: string, cb: EventCallback): void { const event = new Event(eventName); if (event.module === this.moduleAlias) { this.localBus.on(eventName, cb); } else { - this.subSocket.on(eventName, data => { + this.subSocket?.on(eventName, (data: EventInfoObject) => { cb(data); }); } } - once(eventName, cb) { + public once(eventName: string, cb: EventCallback): void { const event = new Event(eventName); if (event.module === this.moduleAlias) { this.localBus.once(eventName, cb); } else { - this.subSocket.on(eventName, data => { - this.subSocket.off(eventName); + this.subSocket?.on(eventName, (data: EventInfoObject) => { + this.subSocket?.off(eventName); cb(data); }); } } - publish(eventName, data) { + public publish(eventName: string, data: object): void { const event = new Event(eventName, data); if (event.module !== this.moduleAlias) { @@ -123,40 +155,52 @@ class ChildProcessChannel extends BaseChannel { } } - async invoke(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invoke(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.actions[action.name].handler(action); } return new Promise((resolve, reject) => { - this.busRpcClient.call('invoke', action.serialize(), (err, data) => { - if (err) { - return reject(err); - } + this.busRpcClient?.call( + 'invoke', + action.serialize(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: Error, data: any) => { + if (err) { + return reject(err); + } - return resolve(data); - }); + return resolve(data); + }, + ); }); } - async invokeFromNetwork(remoteMethod, params) { + public async invokeFromNetwork( + remoteMethod: string, + params: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { return this.invoke(`app:${remoteMethod}`, params); } - async publishToNetwork(actionName, data) { + public async publishToNetwork( + actionName: string, + data: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { return this.invoke(`app:${actionName}`, data); } - async invokePublic(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invokePublic(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { if (!this.actions[action.name].isPublic) { @@ -165,14 +209,18 @@ class ChildProcessChannel extends BaseChannel { ); } + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.actions[action.name].handler(action); } return new Promise((resolve, reject) => { - this.busRpcClient.call( + this.busRpcClient?.call( 'invokePublic', action.serialize(), - (err, data) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: Error, data: any) => { if (err) { return reject(err); } @@ -184,7 +232,7 @@ class ChildProcessChannel extends BaseChannel { } // eslint-disable-next-line @typescript-eslint/require-await - async cleanup() { + public cleanup(_status?: number, _message?: string): void { if (this.pubSocket) { this.pubSocket.close(); } @@ -199,13 +247,13 @@ class ChildProcessChannel extends BaseChannel { } } - async _resolveWhenAllSocketsBound() { + private async _resolveWhenAllSocketsBound(): Promise { const promises = []; if (this.pubSocket) { promises.push( new Promise(resolve => { - this.pubSocket.sock.once('connect', () => { + this.pubSocket?.sock.once('connect', () => { resolve(); }); }), @@ -215,7 +263,7 @@ class ChildProcessChannel extends BaseChannel { if (this.subSocket) { promises.push( new Promise(resolve => { - this.subSocket.sock.once('connect', () => { + this.subSocket?.sock.once('connect', () => { resolve(); }); }), @@ -225,7 +273,7 @@ class ChildProcessChannel extends BaseChannel { if (this.rpcSocket) { promises.push( new Promise(resolve => { - this.rpcSocket.once('bind', () => { + this.rpcSocket?.once('bind', () => { resolve(); }); }), @@ -235,14 +283,15 @@ class ChildProcessChannel extends BaseChannel { if (this.busRpcSocket && this.busRpcClient) { promises.push( new Promise((resolve, reject) => { - this.busRpcSocket.once('connect', () => { - this.busRpcClient.call( + this.busRpcSocket?.once('connect', () => { + this.busRpcClient?.call( 'registerChannel', this.moduleAlias, - this.eventsList.map(event => event.name), - this.actionsList.map(action => action.name), + this.eventsList.map((event: string) => event), + this.actionsList.map((action: string) => action), { type: 'ipcSocket', rpcSocketPath: this.rpcSocketPath }, - (err, result) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: Error, result: any) => { if (err) { reject(err); } @@ -254,16 +303,16 @@ class ChildProcessChannel extends BaseChannel { ); } - return Promise.all(promises); + await Promise.all(promises); } - async _rejectWhenAnySocketFailsToBind() { + private async _rejectWhenAnySocketFailsToBind() { const promises = []; if (this.pubSocket) { promises.push( new Promise((_, reject) => { - this.pubSocket.sock.once('error', err => { + this.pubSocket?.sock.once('error', (err: Error) => { reject(err); }); }), @@ -273,7 +322,7 @@ class ChildProcessChannel extends BaseChannel { if (this.subSocket) { promises.push( new Promise((_, reject) => { - this.subSocket.sock.once('error', err => { + this.subSocket?.sock.once('error', (err: Error) => { reject(err); }); }), @@ -283,7 +332,7 @@ class ChildProcessChannel extends BaseChannel { if (this.rpcSocket) { promises.push( new Promise((_, reject) => { - this.rpcSocket.once('error', err => { + this.rpcSocket?.once('error', (err: Error) => { reject(err); }); }), @@ -294,7 +343,7 @@ class ChildProcessChannel extends BaseChannel { } // eslint-disable-next-line class-methods-use-this - async _rejectWhenTimeout(timeInMillis) { + private async _rejectWhenTimeout(timeInMillis: number) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error('ChildProcessChannel sockets setup timeout')); @@ -302,7 +351,8 @@ class ChildProcessChannel extends BaseChannel { }); } - _removeAllListeners() { + // eslint-disable-next-line @typescript-eslint/require-await + private async _removeAllListeners(): Promise { if (this.subSocket) { this.subSocket.sock.removeAllListeners('connect'); this.subSocket.sock.removeAllListeners('error'); @@ -324,5 +374,3 @@ class ChildProcessChannel extends BaseChannel { } } } - -module.exports = ChildProcessChannel; diff --git a/framework/src/controller/channels/index.js b/framework/src/controller/channels/index.ts similarity index 64% rename from framework/src/controller/channels/index.js rename to framework/src/controller/channels/index.ts index 0864e17ea22..0331ca24f24 100644 --- a/framework/src/controller/channels/index.js +++ b/framework/src/controller/channels/index.ts @@ -12,14 +12,6 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const BaseChannel = require('./base_channel.js'); -const InMemoryChannel = require('./in_memory_channel.js'); -const ChildProcessChannel = require('./child_process_channel.js'); - -module.exports = { - BaseChannel, - InMemoryChannel, - ChildProcessChannel, -}; +export { BaseChannel } from './base_channel'; +export { InMemoryChannel } from './in_memory_channel'; +export { ChildProcessChannel } from './child_process_channel'; diff --git a/framework/types/pm2-axon-rpc/index.d.ts b/framework/types/pm2-axon-rpc/index.d.ts index bff9d00a32e..4d47ecc0e71 100644 --- a/framework/types/pm2-axon-rpc/index.d.ts +++ b/framework/types/pm2-axon-rpc/index.d.ts @@ -12,5 +12,8 @@ declare module 'pm2-axon-rpc' { export class Client { public constructor(socket: ReqSocket); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public call(...args: any[]): void; } } From ebfe475b53ea49248b658ed0c45db7b439745cf7 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 12:16:55 +0200 Subject: [PATCH 08/35] Update controller/child_process_loader to typescript --- .../controller/channels/child_process_channel.ts | 4 ++-- ...ld_process_loader.js => child_process_loader.ts} | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) rename framework/src/controller/{child_process_loader.js => child_process_loader.ts} (79%) diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index e58e9a46e1d..07eaef3829a 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -49,7 +49,7 @@ export class ChildProcessChannel extends BaseChannel { public rpcSocket?: RepSocket; public rpcServer?: RPCServer; - protected constructor( + public constructor( moduleAlias: string, events: ReadonlyArray, actions: ActionsDefinition, @@ -139,7 +139,7 @@ export class ChildProcessChannel extends BaseChannel { } } - public publish(eventName: string, data: object): void { + public publish(eventName: string, data?: object): void { const event = new Event(eventName, data); if (event.module !== this.moduleAlias) { diff --git a/framework/src/controller/child_process_loader.js b/framework/src/controller/child_process_loader.ts similarity index 79% rename from framework/src/controller/child_process_loader.js rename to framework/src/controller/child_process_loader.ts index e12fac77fc2..0924f58ac2e 100644 --- a/framework/src/controller/child_process_loader.js +++ b/framework/src/controller/child_process_loader.ts @@ -12,16 +12,16 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - // Parameters passed by `child_process.fork(_, parameters)` -const modulePath = process.argv[2]; +import { socketPathObject } from './bus'; + +import { ChildProcessChannel } from './channels'; -const { ChildProcessChannel } = require('./channels'); +const modulePath: string = process.argv[2]; // eslint-disable-next-line import/no-dynamic-require const Klass = require(modulePath); -const _loadModule = async (config, moduleOptions) => { +const _loadModule = async (config: object, moduleOptions: object) => { const module = new Klass(moduleOptions); const moduleAlias = module.constructor.alias; @@ -31,7 +31,8 @@ const _loadModule = async (config, moduleOptions) => { module.actions, ); - await channel.registerToBus(config.socketsPath); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await channel.registerToBus((config as any).socketsPath as socketPathObject); channel.publish(`${moduleAlias}:registeredToBus`); channel.publish(`${moduleAlias}:loading:started`); From f904d5f0e2c582a9d4789020b292d523d2de3904 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 16:31:04 +0200 Subject: [PATCH 09/35] Update controller/controller to typescript --- framework/src/controller/bus.ts | 14 +- .../src/controller/channels/base_channel.ts | 2 +- .../channels/child_process_channel.ts | 6 +- .../controller/channels/in_memory_channel.ts | 2 +- .../src/controller/child_process_loader.ts | 4 +- .../{controller.js => controller.ts} | 166 ++++++++++++++---- framework/src/controller/types.ts | 20 +++ framework/src/modules/base_module.js | 64 ------- framework/src/modules/base_module.ts | 52 ++++++ framework/src/types.ts | 9 + 10 files changed, 219 insertions(+), 120 deletions(-) rename framework/src/controller/{controller.js => controller.ts} (62%) create mode 100644 framework/src/controller/types.ts delete mode 100644 framework/src/modules/base_module.js create mode 100644 framework/src/modules/base_module.ts diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index a090f034b43..50ac7dcd98c 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -13,34 +13,28 @@ */ import * as axon from 'pm2-axon'; -import { Server as RPCServer, Client as RPCClient } from 'pm2-axon-rpc'; -import { EventEmitter2, Listener } from 'eventemitter2'; import { PubEmitterSocket, RepSocket, ReqSocket, SubEmitterSocket, } from 'pm2-axon'; +import { Client as RPCClient, Server as RPCServer } from 'pm2-axon-rpc'; +import { EventEmitter2, Listener } from 'eventemitter2'; import { Action, ActionInfoObject, ActionsObject } from './action'; import { Logger } from '../types'; import { BaseChannel } from './channels/base_channel'; import { EventsArray } from './event'; +import { SocketPaths } from './types'; const CONTROLLER_IDENTIFIER = 'app'; const SOCKET_TIMEOUT_TIME = 2000; -export interface socketPathObject { - readonly pub: string; - readonly sub: string; - readonly rpc: string; - readonly root: string; -} - interface BusConfiguration { ipc: { readonly enabled: boolean; }; - socketsPath: socketPathObject; + socketsPath: SocketPaths; } interface RegisterChannelOptions { diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 660ba04c87c..10612919f2f 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -28,7 +28,7 @@ export abstract class BaseChannel { public readonly actionsList: ReadonlyArray; public readonly actions: ActionsObject; - protected constructor( + public constructor( moduleAlias: string, events: ReadonlyArray, actions: ActionsDefinition, diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index 07eaef3829a..b52d37fa073 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -25,7 +25,7 @@ import { import { Action, ActionsDefinition } from '../action'; import { Event, EventCallback, EventInfoObject } from '../event'; import { BaseChannel, BaseChannelOptions } from './base_channel'; -import { socketPathObject } from '../bus'; +import { SocketPaths } from '../types'; export const setupProcessHandlers = (channel: ChildProcessChannel): void => { process.once('SIGTERM', () => channel.cleanup(1)); @@ -45,7 +45,7 @@ export class ChildProcessChannel extends BaseChannel { // eslint-disable-next-line @typescript-eslint/no-explicit-any public busRpcClientCallPromisified: any; public pubSocket?: PubEmitterSocket; - public rpcSocketPath?: socketPathObject | string; + public rpcSocketPath?: SocketPaths | string; public rpcSocket?: RepSocket; public rpcServer?: RPCServer; @@ -61,7 +61,7 @@ export class ChildProcessChannel extends BaseChannel { setupProcessHandlers(this); } - public async registerToBus(socketsPath: socketPathObject): Promise { + public async registerToBus(socketsPath: SocketPaths): Promise { this.subSocket = axon.socket('sub-emitter') as SubEmitterSocket; this.subSocket.connect(socketsPath.pub); diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index 7903d512800..ba0887450c2 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -43,7 +43,7 @@ export class InMemoryChannel extends BaseChannel { ); } - public publish(eventName: string, data: object): void { + public publish(eventName: string, data?: object): void { const event = new Event(eventName, data); if (event.module !== this.moduleAlias) { diff --git a/framework/src/controller/child_process_loader.ts b/framework/src/controller/child_process_loader.ts index 0924f58ac2e..73eea94e98c 100644 --- a/framework/src/controller/child_process_loader.ts +++ b/framework/src/controller/child_process_loader.ts @@ -13,9 +13,9 @@ */ // Parameters passed by `child_process.fork(_, parameters)` -import { socketPathObject } from './bus'; import { ChildProcessChannel } from './channels'; +import { SocketPaths } from './types'; const modulePath: string = process.argv[2]; // eslint-disable-next-line import/no-dynamic-require @@ -32,7 +32,7 @@ const _loadModule = async (config: object, moduleOptions: object) => { ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - await channel.registerToBus((config as any).socketsPath as socketPathObject); + await channel.registerToBus((config as any).socketsPath as SocketPaths); channel.publish(`${moduleAlias}:registeredToBus`); channel.publish(`${moduleAlias}:loading:started`); diff --git a/framework/src/controller/controller.js b/framework/src/controller/controller.ts similarity index 62% rename from framework/src/controller/controller.js rename to framework/src/controller/controller.ts index 137143a9bf5..c49a68b8055 100644 --- a/framework/src/controller/controller.js +++ b/framework/src/controller/controller.ts @@ -12,33 +12,97 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const fs = require('fs-extra'); -const path = require('path'); -const childProcess = require('child_process'); -const psList = require('ps-list'); -const systemDirs = require('../application/system_dirs'); -const { InMemoryChannel } = require('./channels'); -const Bus = require('./bus'); -const { DuplicateAppInstanceError } = require('../errors'); -const { validateModuleSpec } = require('../application/validator'); - -const isPidRunning = async pid => +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as childProcess from 'child_process'; +import * as psList from 'ps-list'; +import { ChildProcess } from 'child_process'; +import * as systemDirs from '../application/system_dirs'; +import { InMemoryChannel } from './channels'; +import { Bus } from './bus'; +import { DuplicateAppInstanceError } from '../errors'; +import { validateModuleSpec } from '../application/validator'; +import { Logger, Storage } from '../types'; +import { SocketPaths } from './types'; +import { BaseModule } from '../modules/base_module'; + +const isPidRunning = async (pid: number) => psList().then(list => list.some(x => x.pid === pid)); +export interface ControllerOptions { + readonly appLabel: string; + readonly config: { + readonly tempPath: string; + readonly ipc: { + readonly enabled: boolean; + }; + }; + readonly logger: Logger; + readonly storage: Storage; + readonly channel: InMemoryChannel; +} + +interface ControllerConfig { + readonly tempPath: string; + readonly socketsPath: SocketPaths; + readonly dirs: { + readonly temp: string; + readonly sockets: string; + readonly pids: string; + }; + readonly ipc: { + readonly enabled: boolean; + }; +} + +interface ModulesObject { + readonly [key: string]: typeof BaseModule; +} + +interface ModuleOptions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly [key: string]: any; + readonly loadAsChildProcess: boolean; +} + +interface ModulesOptions { + readonly [key: string]: ModuleOptions; +} + +interface Migrations { + readonly [key: string]: ReadonlyArray; +} + class Controller { - constructor({ appLabel, config, logger, storage, channel }) { - this.logger = logger; - this.storage = storage; - this.appLabel = appLabel; - this.channel = channel; + public readonly logger: Logger; + public readonly storage: Storage; + public readonly appLabel: string; + public readonly channel: InMemoryChannel; + public readonly config: ControllerConfig; + public modules: { + [key: string]: BaseModule; + }; + public childrenList: Array; + public bus: Bus | undefined; + + public constructor(options: ControllerOptions) { + this.logger = options.logger; + this.storage = options.storage; + this.appLabel = options.appLabel; + this.channel = options.channel; this.logger.info('Initializing controller'); - const dirs = systemDirs(this.appLabel, config.tempPath); + const dirs = systemDirs(this.appLabel, options.config.tempPath); this.config = { - ...config, - dirs, + tempPath: dirs.temp, + ipc: { + enabled: options.config.ipc.enabled, + }, + dirs: { + temp: dirs.temp, + sockets: dirs.sockets, + pids: dirs.pids, + }, socketsPath: { root: `unix://${dirs.sockets}`, pub: `unix://${dirs.sockets}/lisk_pub.sock`, @@ -49,10 +113,13 @@ class Controller { this.modules = {}; this.childrenList = []; - this.bus = null; } - async load(modules, moduleOptions, migrations = {}) { + public async load( + modules: ModulesObject, + moduleOptions: ModulesOptions, + migrations: Migrations = {}, + ): Promise { this.logger.info('Loading controller'); await this._setupDirectories(); await this._validatePidFile(); @@ -60,23 +127,23 @@ class Controller { await this._loadMigrations({ ...migrations }); await this._loadModules(modules, moduleOptions); - this.logger.debug(this.bus.getEvents(), 'Bus listening to events'); - this.logger.debug(this.bus.getActions(), 'Bus ready for actions'); + this.logger.debug(this.bus?.getEvents(), 'Bus listening to events'); + this.logger.debug(this.bus?.getActions(), 'Bus ready for actions'); } // eslint-disable-next-line class-methods-use-this - async _setupDirectories() { + private async _setupDirectories(): Promise { // Make sure all directories exists await fs.ensureDir(this.config.dirs.temp); await fs.ensureDir(this.config.dirs.sockets); await fs.ensureDir(this.config.dirs.pids); } - async _validatePidFile() { + private async _validatePidFile() { const pidPath = `${this.config.dirs.pids}/controller.pid`; const pidExists = await fs.pathExists(pidPath); if (pidExists) { - const pid = parseInt(await fs.readFile(pidPath), 10); + const pid = parseInt((await fs.readFile(pidPath)).toString(), 10); const pidRunning = await isPidRunning(pid); this.logger.info({ pid }, 'Previous Lisk PID'); @@ -93,7 +160,7 @@ class Controller { await fs.writeFile(pidPath, process.pid); } - async _setupBus() { + private async _setupBus() { this.bus = new Bus( { wildcard: true, @@ -117,11 +184,14 @@ class Controller { } // eslint-disable-next-line @typescript-eslint/require-await - async _loadMigrations(migrationsObj) { + private async _loadMigrations(migrationsObj: Migrations) { return this.storage.entities.Migration.applyAll(migrationsObj); } - async _loadModules(modules, moduleOptions) { + private async _loadModules( + modules: ModulesObject, + moduleOptions: ModulesOptions, + ): Promise { // To perform operations in sequence and not using bluebird for (const alias of Object.keys(modules)) { const klass = modules[alias]; @@ -142,10 +212,16 @@ class Controller { } } - async _loadInMemoryModule(alias, Klass, options) { + private async _loadInMemoryModule( + alias: string, + Klass: typeof BaseModule, + options: ModuleOptions, + ): Promise { const moduleAlias = alias || Klass.alias; const { name, version } = Klass.info; + // eslint-disable-next-line + // @ts-ignore const module = new Klass(options); validateModuleSpec(module); @@ -160,7 +236,7 @@ class Controller { module.actions, ); - await channel.registerToBus(this.bus); + await channel.registerToBus(this.bus as Bus); channel.publish(`${moduleAlias}:registeredToBus`); channel.publish(`${moduleAlias}:loading:started`); @@ -174,7 +250,13 @@ class Controller { this.logger.info({ name, version, moduleAlias }, 'Loaded in-memory module'); } - async _loadChildProcessModule(alias, Klass, options) { + private async _loadChildProcessModule( + alias: string, + Klass: typeof BaseModule, + options: ModuleOptions, + ): Promise { + // eslint-disable-next-line + // @ts-ignore const module = new Klass(options); validateModuleSpec(module); @@ -197,10 +279,14 @@ class Controller { const parameters = [modulePath]; // Avoid child processes and the main process sharing the same debugging ports causing a conflict - const forkedProcessOptions = {}; + const forkedProcessOptions = { + execArgv: undefined, + }; const maxPort = 20000; const minPort = 10000; if (process.env.NODE_DEBUG) { + // eslint-disable-next-line + // @ts-ignore forkedProcessOptions.execArgv = [ `--inspect=${Math.floor( Math.random() * (maxPort - minPort) + minPort, @@ -228,7 +314,7 @@ class Controller { process.exit(1); }); - return Promise.race([ + await Promise.race([ new Promise(resolve => { this.channel.once(`${moduleAlias}:loading:finished`, () => { this.logger.info( @@ -244,7 +330,9 @@ class Controller { ]); } - async unloadModules(modules = Object.keys(this.modules)) { + public async unloadModules( + modules = Object.keys(this.modules), + ): Promise { // To perform operations in sequence and not using bluebird for (const alias of modules) { @@ -253,7 +341,7 @@ class Controller { } } - async cleanup(code, reason) { + public async cleanup(_code: number, reason: string): Promise { this.logger.info('Cleanup controller...'); if (reason) { @@ -263,7 +351,7 @@ class Controller { this.childrenList.forEach(child => child.kill()); try { - await this.bus.cleanup(); + await this.bus?.cleanup(); await this.unloadModules(); this.logger.info('Unload completed'); } catch (err) { diff --git a/framework/src/controller/types.ts b/framework/src/controller/types.ts new file mode 100644 index 00000000000..cd2ea695df4 --- /dev/null +++ b/framework/src/controller/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright © 2020 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export interface SocketPaths { + readonly pub: string; + readonly sub: string; + readonly rpc: string; + readonly root: string; +} diff --git a/framework/src/modules/base_module.js b/framework/src/modules/base_module.js deleted file mode 100644 index 7dbd92c32fa..00000000000 --- a/framework/src/modules/base_module.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const { ImplementationMissingError } = require('../errors'); - -/* eslint-disable class-methods-use-this,no-unused-vars */ - -module.exports = class BaseModule { - constructor(options) { - this.options = options; - } - - static get alias() { - throw new ImplementationMissingError(); - } - - static get info() { - throw new ImplementationMissingError(); - } - - // Array of migrations to be executed before loading the module. Expected format: ['yyyyMMddHHmmss_name_of_migration.sql'] - static get migrations() { - return []; - } - - get defaults() { - // This interface is not required to be implemented - return {}; - } - - get events() { - // This interface is not required to be implemented - return []; - } - - get actions() { - // This interface is not required to be implemented - return {}; - } - - // eslint-disable-next-line @typescript-eslint/require-await - async load(channel) { - throw new ImplementationMissingError(); - } - - // eslint-disable-next-line @typescript-eslint/require-await - async unload() { - // This interface is not required. - return true; - } -}; diff --git a/framework/src/modules/base_module.ts b/framework/src/modules/base_module.ts new file mode 100644 index 00000000000..48739d144df --- /dev/null +++ b/framework/src/modules/base_module.ts @@ -0,0 +1,52 @@ +/* + * Copyright © 2019 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { ImplementationMissingError } from '../errors'; +import { EventsArray } from '../controller/event'; +import { ActionsDefinition } from '../controller/action'; +import { BaseChannel } from '../controller/channels/base_channel'; + +export interface ModuleInfo { + readonly author: string; + readonly version: string; + readonly name: string; +} + +export abstract class BaseModule { + public readonly options: object; + + public constructor(options: object) { + this.options = options; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public static get alias(): string { + throw new ImplementationMissingError(); + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + public static get info(): ModuleInfo { + throw new ImplementationMissingError(); + } + + // Array of migrations to be executed before loading the module. Expected format: ['yyyyMMddHHmmss_name_of_migration.sql'] + public static get migrations(): ReadonlyArray { + return []; + } + + public abstract get defaults(): object; + public abstract get events(): EventsArray; + public abstract get actions(): ActionsDefinition; + public abstract async load(channel: BaseChannel): Promise; + public abstract async unload(): Promise; +} diff --git a/framework/src/types.ts b/framework/src/types.ts index e1db197c7a2..6ad2b146c69 100644 --- a/framework/src/types.ts +++ b/framework/src/types.ts @@ -24,17 +24,26 @@ export interface Logger { readonly trace: (data?: object | unknown, message?: string) => void; readonly debug: (data?: object | unknown, message?: string) => void; readonly info: (data?: object | unknown, message?: string) => void; + readonly warn: (data?: object | unknown, message?: string) => void; readonly error: (data?: object | unknown, message?: string) => void; readonly fatal: (data?: object | unknown, message?: string) => void; + readonly level: () => number; } /* Start Database */ export interface Storage { readonly entities: { readonly NetworkInfo: NetworkInfoEntity; + readonly Migration: MigrationEntity; }; } +export interface MigrationEntity { + readonly applyAll: (migrations: { + readonly [key: string]: ReadonlyArray; + }) => Promise; +} + export interface NetworkInfoEntity { readonly getKey: ( key: string, From fa99d79a3f2fda4c3dcfd24cdb5cbd93d408aeef Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 16:54:37 +0200 Subject: [PATCH 10/35] Update controller constatns to top level file --- framework/src/controller/action.ts | 4 +--- framework/src/controller/bus.ts | 6 ++---- framework/src/controller/channels/base_channel.ts | 2 +- framework/src/controller/{channels/base => }/constants.ts | 4 ++++ framework/src/controller/event.ts | 2 +- .../unit/specs/controller/channels/base/constants.spec.js | 2 +- .../unit/specs/controller/channels/base_channel.spec.js | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename framework/src/controller/{channels/base => }/constants.ts (74%) diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index f2e488d315c..35608671a00 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -13,9 +13,7 @@ */ import { strict as assert } from 'assert'; - -const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; -const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; +import { actionWithModuleNameReg, moduleNameReg } from './constants'; export interface ActionInfoObject { readonly module: string; diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 50ac7dcd98c..619a82310f9 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -23,12 +23,10 @@ import { Client as RPCClient, Server as RPCServer } from 'pm2-axon-rpc'; import { EventEmitter2, Listener } from 'eventemitter2'; import { Action, ActionInfoObject, ActionsObject } from './action'; import { Logger } from '../types'; -import { BaseChannel } from './channels/base_channel'; +import { BaseChannel } from './channels'; import { EventsArray } from './event'; import { SocketPaths } from './types'; - -const CONTROLLER_IDENTIFIER = 'app'; -const SOCKET_TIMEOUT_TIME = 2000; +import { CONTROLLER_IDENTIFIER, SOCKET_TIMEOUT_TIME } from './constants'; interface BusConfiguration { ipc: { diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 10612919f2f..085b70044e4 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -14,7 +14,7 @@ import { Event, EventCallback } from '../event'; import { Action, ActionsDefinition, ActionsObject } from '../action'; -import { INTERNAL_EVENTS, eventWithModuleNameReg } from './base/constants'; +import { INTERNAL_EVENTS, eventWithModuleNameReg } from '../constants'; export interface BaseChannelOptions { readonly skipInternalEvents?: boolean; diff --git a/framework/src/controller/channels/base/constants.ts b/framework/src/controller/constants.ts similarity index 74% rename from framework/src/controller/channels/base/constants.ts rename to framework/src/controller/constants.ts index 78d957ef137..8a2fd97a85d 100644 --- a/framework/src/controller/channels/base/constants.ts +++ b/framework/src/controller/constants.ts @@ -19,3 +19,7 @@ export const INTERNAL_EVENTS = Object.freeze([ ]); export const eventWithModuleNameReg = /^([^\d][\w]+)((?::[^\d][\w]+)+)$/; +export const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; +export const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; +export const CONTROLLER_IDENTIFIER = 'app'; +export const SOCKET_TIMEOUT_TIME = 2000; diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 49fe5fb7ba4..9151703ddb5 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -14,7 +14,7 @@ import { strict as assert } from 'assert'; -import { eventWithModuleNameReg } from './channels/base/constants'; +import { eventWithModuleNameReg } from './constants'; export interface EventInfoObject { readonly module: string; diff --git a/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js b/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js index 0d9e27325f1..ef9fb14e685 100644 --- a/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js +++ b/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js @@ -16,7 +16,7 @@ const { INTERNAL_EVENTS, -} = require('../../../../../../../src/controller/channels/base/constants'); +} = require('../../../../../../../src/controller/constants'); describe('base/constants.js', () => { it('INTERNAL_EVENTS must match to the snapshot.', () => { diff --git a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js index c43cc296322..80681df746e 100644 --- a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js +++ b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js @@ -20,7 +20,7 @@ jest.mock('../../../../../../src/controller/event'); const BaseChannel = require('../../../../../../src/controller/channels/base_channel'); const { INTERNAL_EVENTS, -} = require('../../../../../../src/controller/channels/base/constants'); +} = require('../../../../../../src/controller/constants'); const Action = require('../../../../../../src/controller/action'); const Event = require('../../../../../../src/controller/event'); From 6877b2c22b1e9b6f803063eb50ea7ad3558f2d37 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 17:44:29 +0200 Subject: [PATCH 11/35] Fix eslint dependencies --- framework/src/application/application.js | 2 ++ framework/src/controller/channels/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/framework/src/application/application.js b/framework/src/application/application.js index 4119e29dfee..713f9895fb3 100644 --- a/framework/src/application/application.js +++ b/framework/src/application/application.js @@ -443,9 +443,11 @@ class Application { handler: action => this._network.broadcast(action.params), }, requestFromNetwork: { + // eslint-disable-next-line @typescript-eslint/require-await handler: async action => this._network.request(action.params), }, requestFromPeer: { + // eslint-disable-next-line @typescript-eslint/require-await handler: async action => this._network.requestFromPeer(action.params), }, getConnectedPeers: { diff --git a/framework/src/controller/channels/index.ts b/framework/src/controller/channels/index.ts index 0331ca24f24..698847869fa 100644 --- a/framework/src/controller/channels/index.ts +++ b/framework/src/controller/channels/index.ts @@ -13,5 +13,7 @@ */ export { BaseChannel } from './base_channel'; +// TODO: Fix cycle dependencies +// eslint-disable-next-line import/no-cycle export { InMemoryChannel } from './in_memory_channel'; export { ChildProcessChannel } from './child_process_channel'; From 0fe16803624f086cd261f05bc3fd29a4c862f8c3 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 10:53:27 +0200 Subject: [PATCH 12/35] Update controller/action to typescript --- .../src/controller/{action.js => action.ts} | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) rename framework/src/controller/{action.js => action.ts} (61%) diff --git a/framework/src/controller/action.js b/framework/src/controller/action.ts similarity index 61% rename from framework/src/controller/action.js rename to framework/src/controller/action.ts index 006b0073fc6..d6ca226ee8f 100644 --- a/framework/src/controller/action.js +++ b/framework/src/controller/action.ts @@ -12,15 +12,29 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const assert = require('assert'); +import { strict as assert } from 'assert'; const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; -class Action { - constructor(name, params = null, source = null) { +export interface ActionObject { + readonly module: string; + readonly name: string; + readonly source?: string; + readonly params?: object; +} + +export class Action { + public module: string; + public name: string; + public source?: string; + public params?: object; + + public constructor( + name: string, + params: object | undefined, + source: string | undefined, + ) { assert( actionWithModuleNameReg.test(name), `Action name "${name}" must be a valid name with module name.`, @@ -37,17 +51,11 @@ class Action { } } - serialize() { - return { - name: this.name, - module: this.module, - source: this.source, - params: this.params, - }; - } + public static deserialize(data: ActionObject | string): Action { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsedAction: ActionObject = + typeof data === 'string' ? JSON.parse(data) : data; - static deserialize(data) { - const parsedAction = typeof data === 'string' ? JSON.parse(data) : data; return new Action( `${parsedAction.module}:${parsedAction.name}`, parsedAction.params, @@ -55,13 +63,20 @@ class Action { ); } - toString() { - return `${this.source} -> ${this.module}:${this.name}`; + public serialize(): ActionObject { + return { + name: this.name, + module: this.module, + source: this.source, + params: this.params, + }; + } + + public toString(): string { + return `${this.source ?? 'undefined'} -> ${this.module}:${this.name}`; } - key() { + public key(): string { return `${this.module}:${this.name}`; } } - -module.exports = Action; From 57fbed041b8048bc98bf6ae80d351d853fa85694 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 10:56:33 +0200 Subject: [PATCH 13/35] Update controller/channels/constants to typescript --- .../channels/base/{constants.js => constants.ts} | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) rename framework/src/controller/channels/base/{constants.js => constants.ts} (74%) diff --git a/framework/src/controller/channels/base/constants.js b/framework/src/controller/channels/base/constants.ts similarity index 74% rename from framework/src/controller/channels/base/constants.js rename to framework/src/controller/channels/base/constants.ts index 9ced1e52887..78d957ef137 100644 --- a/framework/src/controller/channels/base/constants.js +++ b/framework/src/controller/channels/base/constants.ts @@ -12,17 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const INTERNAL_EVENTS = Object.freeze([ +export const INTERNAL_EVENTS = Object.freeze([ 'registeredToBus', 'loading:started', 'loading:finished', ]); -const eventWithModuleNameReg = /^([^\d][\w]+)((?::[^\d][\w]+)+)$/; - -module.exports = { - eventWithModuleNameReg, - INTERNAL_EVENTS, -}; +export const eventWithModuleNameReg = /^([^\d][\w]+)((?::[^\d][\w]+)+)$/; From 90eee505d18566c0051aeeab54c724fd40653c6e Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 11:12:45 +0200 Subject: [PATCH 14/35] Update controller/event to typescript --- .../src/controller/{event.js => event.ts} | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) rename framework/src/controller/{event.js => event.ts} (52%) diff --git a/framework/src/controller/event.js b/framework/src/controller/event.ts similarity index 52% rename from framework/src/controller/event.js rename to framework/src/controller/event.ts index abd5085d19d..4557d9e3a3f 100644 --- a/framework/src/controller/event.js +++ b/framework/src/controller/event.ts @@ -12,25 +12,45 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; +import { strict as assert } from 'assert'; -const assert = require('assert'); +import { eventWithModuleNameReg } from './channels/base/constants'; -const { eventWithModuleNameReg } = require('./channels/base/constants'); +export interface EventObject { + readonly module: string; + readonly name: string; + readonly data?: object | string; +} + +export class Event { + public module: string; + public name: string; + public data?: object | string; -class Event { - constructor(name, data = null) { + public constructor(name: string, data?: object | string) { assert( eventWithModuleNameReg.test(name), `Event name "${name}" must be a valid name with module name.`, ); + + const [moduleName, ...eventName] = name.split(':'); + this.module = moduleName; + this.name = eventName.join(':'); this.data = data; - [, this.module, this.name] = eventWithModuleNameReg.exec(name); - // Remove the first prefixed ':' symbol - this.name = this.name.substring(1); } - serialize() { + public static deserialize(data: EventObject | string): Event { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsedEvent: EventObject = + typeof data === 'string' ? JSON.parse(data) : data; + + return new Event( + `${parsedEvent.module}:${parsedEvent.name}`, + parsedEvent.data, + ); + } + + public serialize(): EventObject { return { name: this.name, module: this.module, @@ -38,27 +58,11 @@ class Event { }; } - toString() { + public toString(): string { return `${this.module}:${this.name}`; } - key() { + public key(): string { return `${this.module}:${this.name}`; } - - static deserialize(data) { - let parsedEvent = null; - if (typeof data === 'string') { - parsedEvent = JSON.parse(data); - } else { - parsedEvent = data; - } - - return new Event( - `${parsedEvent.module}:${parsedEvent.name}`, - parsedEvent.data, - ); - } } - -module.exports = Event; From e709201009bc3941387346b45b8eeab8a4bcddfc Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 12:14:10 +0200 Subject: [PATCH 15/35] Update controller/channels/base_channel to typescript --- framework/src/controller/action.ts | 9 +- .../src/controller/channels/base_channel.js | 133 ------------------ .../src/controller/channels/base_channel.ts | 109 ++++++++++++++ framework/src/controller/event.ts | 2 + 4 files changed, 115 insertions(+), 138 deletions(-) delete mode 100644 framework/src/controller/channels/base_channel.js create mode 100644 framework/src/controller/channels/base_channel.ts diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index d6ca226ee8f..b5ce3786476 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -24,17 +24,16 @@ export interface ActionObject { readonly params?: object; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ActionCallback = (action: ActionObject) => any; + export class Action { public module: string; public name: string; public source?: string; public params?: object; - public constructor( - name: string, - params: object | undefined, - source: string | undefined, - ) { + public constructor(name: string, params?: object, source?: string) { assert( actionWithModuleNameReg.test(name), `Action name "${name}" must be a valid name with module name.`, diff --git a/framework/src/controller/channels/base_channel.js b/framework/src/controller/channels/base_channel.js deleted file mode 100644 index 54582c9cf4f..00000000000 --- a/framework/src/controller/channels/base_channel.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const Event = require('../event'); -const Action = require('../action'); -const { INTERNAL_EVENTS, eventWithModuleNameReg } = require('./base/constants'); - -const _eventsList = new WeakMap(); -const _actionsList = new WeakMap(); -const _actions = new WeakMap(); - -class BaseChannel { - constructor(moduleAlias, events, actions, options = {}) { - this.moduleAlias = moduleAlias; - this.options = options; - - const eventList = options.skipInternalEvents - ? events - : [...events, ...INTERNAL_EVENTS]; - - _eventsList.set( - this, - eventList.map(eventName => new Event(`${this.moduleAlias}:${eventName}`)), - ); - - _actionsList.set( - this, - Object.keys(actions).map( - actionName => new Action(`${this.moduleAlias}:${actionName}`), - ), - ); - - _actions.set(this, actions); - } - - get actionsList() { - return _actionsList.get(this); - } - - get eventsList() { - return _eventsList.get(this); - } - - get actions() { - return _actions.get(this); - } - - // eslint-disable-next-line @typescript-eslint/require-await,class-methods-use-this - async registerToBus() { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Listen to any event happening in the application - // Specified as moduleName:eventName - // If its related to your own moduleAlias specify as :eventName - // eslint-disable-next-line no-unused-vars, class-methods-use-this - subscribe(eventName, cb) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Publish the event on the channel - // Specified as moduleName:eventName - // If its related to your own moduleAlias specify as :eventName - // eslint-disable-next-line no-unused-vars, class-methods-use-this - publish(eventName, data) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Publish to the network by invoking send/broadcast actions in the network - // Specified as actionName for send or broadcast available on the network - // If its related to your own moduleAlias specify as :eventName - // eslint-disable-next-line no-unused-vars, class-methods-use-this - publishToNetwork(actionName, data) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Call action of any moduleAlias through controller - // Specified as moduleName:actionName - // eslint-disable-next-line @typescript-eslint/require-await, no-unused-vars, class-methods-use-this - async invoke(actionName, params) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Call action network module when requesting from the network or a specific peer - // Specified as actionName for request available on the network - // eslint-disable-next-line @typescript-eslint/require-await, no-unused-vars, class-methods-use-this - async invokeFromNetwork(actionName, params) { - throw new TypeError('This method must be implemented in child classes. '); - } - - // Call action of any moduleAlias through controller - // Specified as moduleName:actionName - // Specified action must be defined as publicly callable - // eslint-disable-next-line @typescript-eslint/require-await, no-unused-vars, class-methods-use-this - async invokePublic(actionName, params) { - throw new TypeError('This method must be implemented in child classes. '); - } - - isValidEventName(name, throwError = true) { - const result = eventWithModuleNameReg.test(name); - if (throwError && !result) { - throw new Error( - `[${this.moduleAlias.alias}] Invalid event name ${name}.`, - ); - } - return result; - } - - isValidActionName(name, throwError = true) { - const result = eventWithModuleNameReg.test(name); - if (throwError && !result) { - throw new Error( - `[${this.moduleAlias.alias}] Invalid action name ${name}.`, - ); - } - return result; - } -} - -module.exports = BaseChannel; diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts new file mode 100644 index 00000000000..6fe916632f4 --- /dev/null +++ b/framework/src/controller/channels/base_channel.ts @@ -0,0 +1,109 @@ +/* + * Copyright © 2019 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Event, EventCallback } from '../event'; +import { Action, ActionCallback } from '../action'; +import { INTERNAL_EVENTS, eventWithModuleNameReg } from './base/constants'; + +export interface BaseChannelOptions { + readonly skipInternalEvents?: boolean; +} + +export abstract class BaseChannel { + public readonly moduleAlias: string; + public readonly options: object; + + public readonly eventsList: ReadonlyArray; + public readonly actionsList: ReadonlyArray; + public readonly actions: { [key: string]: ActionCallback }; + + protected constructor( + moduleAlias: string, + events: ReadonlyArray, + actions: { [key: string]: ActionCallback }, + options: BaseChannelOptions = {}, + ) { + this.moduleAlias = moduleAlias; + this.options = options; + + const eventList = options.skipInternalEvents + ? events + : [...events, ...INTERNAL_EVENTS]; + + this.eventsList = eventList.map( + eventName => new Event(`${this.moduleAlias}:${eventName}`), + ); + + this.actionsList = Object.keys(actions).map( + actionName => new Action(`${this.moduleAlias}:${actionName}`), + ); + + this.actions = actions; + } + + public isValidEventName(name: string, throwError = true): boolean | never { + const result = eventWithModuleNameReg.test(name); + + if (throwError && !result) { + throw new Error(`[${this.moduleAlias}] Invalid event name ${name}.`); + } + return result; + } + + public isValidActionName(name: string, throwError = true): boolean | never { + const result = eventWithModuleNameReg.test(name); + + if (throwError && !result) { + throw new Error(`[${this.moduleAlias}] Invalid action name ${name}.`); + } + + return result; + } + + abstract async registerToBus(): Promise; + + // Listen to any event happening in the application + // Specified as moduleName:eventName + // If its related to your own moduleAlias specify as :eventName + abstract subscribe(eventName: string, cb: EventCallback): void; + + // Publish the event on the channel + // Specified as moduleName:eventName + // If its related to your own moduleAlias specify as :eventName + abstract publish(eventName: string, data: object): void; + + // Publish to the network by invoking send/broadcast actions in the network + // Specified as actionName for send or broadcast available on the network + // If its related to your own moduleAlias specify as :eventName + abstract publishToNetwork(actionName: string, data: object): void; + + // Call action of any moduleAlias through controller + // Specified as moduleName:actionName + abstract async invoke(actionName: string, params?: object): Promise; + + // Call action network module when requesting from the network or a specific peer + // Specified as actionName for request available on the network + abstract async invokeFromNetwork( + actionName: string, + params?: object, + ): Promise; + + // Call action of any moduleAlias through controller + // Specified as moduleName:actionName + // Specified action must be defined as publicly callable + abstract async invokePublic( + actionName: string, + params?: object, + ): Promise; +} diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 4557d9e3a3f..407e56bc305 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -22,6 +22,8 @@ export interface EventObject { readonly data?: object | string; } +export type EventCallback = (action: EventObject) => void; + export class Event { public module: string; public name: string; From 6edfa7800c554c01122813ec91dcc086ddf59a01 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 30 Apr 2020 16:26:08 +0200 Subject: [PATCH 16/35] Update controller/bus to typescript --- framework/src/controller/action.ts | 5 + framework/src/controller/{bus.js => bus.ts} | 194 +++++++++++++------- framework/src/controller/event.ts | 2 + framework/tsconfig.json | 3 +- framework/types/pm2-axon-rpc/index.d.ts | 16 ++ framework/types/pm2-axon/index.d.ts | 183 ++++++++++++++++++ 6 files changed, 339 insertions(+), 64 deletions(-) rename framework/src/controller/{bus.js => bus.ts} (52%) create mode 100644 framework/types/pm2-axon-rpc/index.d.ts create mode 100644 framework/types/pm2-axon/index.d.ts diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index b5ce3786476..a9bf4aedf95 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -27,11 +27,16 @@ export interface ActionObject { // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ActionCallback = (action: ActionObject) => any; +export interface ActionsObject { + [key: string]: Action; +} + export class Action { public module: string; public name: string; public source?: string; public params?: object; + public isPublic?: boolean; public constructor(name: string, params?: object, source?: string) { assert( diff --git a/framework/src/controller/bus.js b/framework/src/controller/bus.ts similarity index 52% rename from framework/src/controller/bus.js rename to framework/src/controller/bus.ts index a70f47704ee..8d693f1ab2e 100644 --- a/framework/src/controller/bus.js +++ b/framework/src/controller/bus.ts @@ -12,18 +12,67 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const axon = require('pm2-axon'); -const { Server: RPCServer, Client: RPCClient } = require('pm2-axon-rpc'); -const { EventEmitter2 } = require('eventemitter2'); -const Action = require('./action'); +import * as axon from 'pm2-axon'; +import { Server as RPCServer, Client as RPCClient } from 'pm2-axon-rpc'; +import { EventEmitter2, Listener } from 'eventemitter2'; +import { + PubEmitterSocket, + RepSocket, + ReqSocket, + SubEmitterSocket, +} from 'pm2-axon'; +import { Action, ActionObject, ActionsObject } from './action'; +import { Logger } from '../types'; +import { BaseChannel } from './channels/base_channel'; +import { EventsArray } from './event'; const CONTROLLER_IDENTIFIER = 'app'; const SOCKET_TIMEOUT_TIME = 2000; -class Bus extends EventEmitter2 { - constructor(options, logger, config) { +interface BusConfiguration { + ipc: { + readonly enabled: boolean; + }; + socketsPath: { + readonly pub: string; + readonly sub: string; + readonly rpc: string; + }; +} + +interface RegisterChannelOptions { + readonly type: string; + readonly channel: BaseChannel; + readonly rpcSocketPath?: string; +} + +interface ChannelInfo { + readonly channel: BaseChannel; + readonly actions: ActionsObject; + readonly events: EventsArray; + readonly type: string; +} + +export class Bus extends EventEmitter2 { + public logger: Logger; + public config: BusConfiguration; + public actions: { [key: string]: Action }; + public events: { [key: string]: boolean }; + public channels: { + [key: string]: ChannelInfo; + }; + public rpcClients: { [key: string]: ReqSocket }; + public pubSocket?: PubEmitterSocket; + public subSocket?: SubEmitterSocket; + public rpcSocket?: RepSocket; + public rpcServer?: RPCServer; + public channel?: BaseChannel; + + public constructor( + options: object, + logger: Logger, + config: BusConfiguration, + ) { super(options); this.logger = logger; this.config = config; @@ -35,18 +84,18 @@ class Bus extends EventEmitter2 { this.rpcClients = {}; } - async setup() { + public async setup(): Promise { if (!this.config.ipc.enabled) { return true; } - this.pubSocket = axon.socket('pub-emitter'); + this.pubSocket = axon.socket('pub-emitter') as PubEmitterSocket; this.pubSocket.bind(this.config.socketsPath.pub); - this.subSocket = axon.socket('sub-emitter'); + this.subSocket = axon.socket('sub-emitter') as SubEmitterSocket; this.subSocket.bind(this.config.socketsPath.sub); - this.rpcSocket = axon.socket('rep'); + this.rpcSocket = axon.socket('rep') as RepSocket; this.rpcServer = new RPCServer(this.rpcSocket); this.rpcSocket.bind(this.config.socketsPath.rpc); @@ -71,22 +120,23 @@ class Bus extends EventEmitter2 { .catch(error => cb(error)); }); - return Promise.race([ + await Promise.race([ this._resolveWhenAllSocketsBound(), this._rejectWhenAnySocketFailsToBind(), this._rejectWhenTimeout(SOCKET_TIMEOUT_TIME), - ]).finally(() => { - this._removeAllListeners(); - }); + ]); + await this._removeAllListeners(); + + return true; } // eslint-disable-next-line @typescript-eslint/require-await - async registerChannel( - moduleAlias, - events, - actions, - options = { type: 'inMemory' }, - ) { + public async registerChannel( + moduleAlias: string, + events: EventsArray, + actions: ActionsObject, + options: RegisterChannelOptions, + ): Promise { events.forEach(eventName => { const eventFullName = `${moduleAlias}:${eventName}`; if (this.events[eventFullName]) { @@ -112,8 +162,12 @@ class Bus extends EventEmitter2 { let { channel } = options; if (options.rpcSocketPath) { - const rpcSocket = axon.socket('req'); + const rpcSocket = axon.socket('req') as ReqSocket; rpcSocket.connect(options.rpcSocketPath); + + // TODO: Fix this override + // eslint-disable-next-line + // @ts-ignore channel = new RPCClient(rpcSocket); this.rpcClients[moduleAlias] = rpcSocket; } @@ -126,26 +180,37 @@ class Bus extends EventEmitter2 { }; } - async invoke(actionData) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invoke(actionData: string | ActionObject): Promise { const action = Action.deserialize(actionData); + const actionModule = action.module; + const actionFullName = action.key(); + const actionParams = action.params; - if (!this.actions[action.key()]) { + if (this.actions[actionFullName] === undefined) { throw new Error(`Action '${action.key()}' is not registered to bus.`); } - if (action.module === CONTROLLER_IDENTIFIER) { - return this.channels[CONTROLLER_IDENTIFIER].channel.invoke(action); + const channelInfo = this.channels[actionFullName]; + + if (actionModule === CONTROLLER_IDENTIFIER) { + return channelInfo.channel.invoke(actionFullName, actionParams); } - if (this.channels[action.module].type === 'inMemory') { - return this.channels[action.module].channel.invoke(action); + if (channelInfo.type === 'inMemory') { + return channelInfo.channel.invoke(actionFullName, actionParams); } return new Promise((resolve, reject) => { - this.channels[action.module].channel.call( + // TODO: Fix when both channel types are converted to typescript + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + (channelInfo.channel as any).call( 'invoke', action.serialize(), - (err, data) => { + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err?: string | object, data: any) => { if (err) { return reject(err); } @@ -155,11 +220,12 @@ class Bus extends EventEmitter2 { }); } - async invokePublic(actionData) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invokePublic(actionData: string | ActionObject): Promise { const action = Action.deserialize(actionData); // Check if action exists - if (!this.actions[action.key()]) { + if (this.actions[action.key()] === undefined) { throw new Error(`Action '${action.key()}' is not registered to bus.`); } @@ -173,7 +239,8 @@ class Bus extends EventEmitter2 { return this.invoke(actionData); } - publish(eventName, eventValue) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types + public publish(eventName: string, eventValue: any): void | never { if (!this.getEvents().includes(eventName)) { throw new Error(`Event ${eventName} is not registered to bus.`); } @@ -182,11 +249,11 @@ class Bus extends EventEmitter2 { // Communicate through unix socket if (this.config.ipc.enabled) { - this.pubSocket.emit(eventName, eventValue); + this.pubSocket?.emit(eventName, eventValue); } } - subscribe(eventName, cb) { + public subscribe(eventName: string, cb: Listener): void { if (!this.getEvents().includes(eventName)) { this.logger.info( `Event ${eventName} was subscribed but not registered to the bus yet.`, @@ -198,11 +265,13 @@ class Bus extends EventEmitter2 { // Communicate through unix socket if (this.config.ipc.enabled) { - this.subSocket.on(eventName, cb); + this.subSocket?.on(eventName, cb); } } - once(eventName, cb) { + // eslint-disable-next-line + // @ts-ignore + public once(eventName: string, cb: Listener): void { if (!this.getEvents().includes(eventName)) { this.logger.info( `Event ${eventName} was subscribed but not registered to the bus yet.`, @@ -210,25 +279,25 @@ class Bus extends EventEmitter2 { } // Communicate through event emitter - super.once(eventName, cb); + super.once([eventName], cb); // TODO: make it `once` instead of `on` // Communicate through unix socket if (this.config.ipc.enabled) { - this.subSocket.on(eventName, cb); + this.subSocket?.on(eventName, cb); } } - getActions() { + public getActions(): ReadonlyArray { return Object.keys(this.actions); } - getEvents() { + public getEvents(): ReadonlyArray { return Object.keys(this.events); } // eslint-disable-next-line @typescript-eslint/require-await - async cleanup() { + public async cleanup(): Promise { if (this.pubSocket) { this.pubSocket.close(); } @@ -240,8 +309,8 @@ class Bus extends EventEmitter2 { } } - async _resolveWhenAllSocketsBound() { - return Promise.all([ + private async _resolveWhenAllSocketsBound(): Promise { + await Promise.all([ new Promise(resolve => { /* Here, the reason of calling .sock.once instead of pubSocket.once @@ -249,37 +318,37 @@ class Bus extends EventEmitter2 { However the actual socket does, by inheriting it from EventEmitter prototype */ - this.subSocket.sock.once('bind', () => { + this.subSocket?.sock.once('bind', () => { resolve(); }); }), new Promise(resolve => { - this.pubSocket.sock.once('bind', () => { + this.pubSocket?.sock.once('bind', () => { resolve(); }); }), new Promise(resolve => { - this.rpcSocket.once('bind', () => { + this.rpcSocket?.once('bind', () => { resolve(); }); }), ]); } - async _rejectWhenAnySocketFailsToBind() { - return Promise.race([ + private async _rejectWhenAnySocketFailsToBind(): Promise { + await Promise.race([ new Promise((_, reject) => { - this.subSocket.sock.once('error', () => { + this.subSocket?.sock.once('error', () => { reject(); }); }), new Promise((_, reject) => { - this.pubSocket.sock.once('error', () => { + this.pubSocket?.sock.once('error', () => { reject(); }); }), new Promise((_, reject) => { - this.rpcSocket.once('error', () => { + this.rpcSocket?.once('error', () => { reject(); }); }), @@ -287,7 +356,7 @@ class Bus extends EventEmitter2 { } // eslint-disable-next-line class-methods-use-this - async _rejectWhenTimeout(timeInMillis) { + private async _rejectWhenTimeout(timeInMillis: number): Promise { return new Promise((_, reject) => { setTimeout(() => { reject(new Error('Bus sockets setup timeout')); @@ -295,14 +364,13 @@ class Bus extends EventEmitter2 { }); } - _removeAllListeners() { - this.subSocket.sock.removeAllListeners('bind'); - this.subSocket.sock.removeAllListeners('error'); - this.pubSocket.sock.removeAllListeners('bind'); - this.pubSocket.sock.removeAllListeners('error'); - this.rpcSocket.removeAllListeners('bind'); - this.rpcSocket.removeAllListeners('error'); + // eslint-disable-next-line @typescript-eslint/require-await + private async _removeAllListeners(): Promise { + this.subSocket?.sock.removeAllListeners('bind'); + this.subSocket?.sock.removeAllListeners('error'); + this.pubSocket?.sock.removeAllListeners('bind'); + this.pubSocket?.sock.removeAllListeners('error'); + this.rpcSocket?.removeAllListeners('bind'); + this.rpcSocket?.removeAllListeners('error'); } } - -module.exports = Bus; diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 407e56bc305..7fda7e9391f 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -24,6 +24,8 @@ export interface EventObject { export type EventCallback = (action: EventObject) => void; +export type EventsArray = ReadonlyArray; + export class Event { public module: string; public name: string; diff --git a/framework/tsconfig.json b/framework/tsconfig.json index d229aa4d433..8069318b08e 100644 --- a/framework/tsconfig.json +++ b/framework/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig", "compilerOptions": { "allowJs": true, - "outDir": "dist-node" + "outDir": "dist-node", + "typeRoots": ["../node_modules/@types", "node_modules/@types", "./types"] }, "include": ["src/**/*"] } diff --git a/framework/types/pm2-axon-rpc/index.d.ts b/framework/types/pm2-axon-rpc/index.d.ts new file mode 100644 index 00000000000..bff9d00a32e --- /dev/null +++ b/framework/types/pm2-axon-rpc/index.d.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line max-classes-per-file +declare module 'pm2-axon-rpc' { + import { RepSocket, ReqSocket } from 'pm2-axon'; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type ServerExposeCallBack = (...args: any[]) => void; + + export class Server { + public constructor(socket: RepSocket); + public expose(name: string, cb: ServerExposeCallBack): void; + } + + export class Client { + public constructor(socket: ReqSocket); + } +} diff --git a/framework/types/pm2-axon/index.d.ts b/framework/types/pm2-axon/index.d.ts new file mode 100644 index 00000000000..2b1b9133702 --- /dev/null +++ b/framework/types/pm2-axon/index.d.ts @@ -0,0 +1,183 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// eslint-disable-next-line max-classes-per-file +declare module 'pm2-axon' { + import EventEmitter = NodeJS.EventEmitter; + import NetSocket = NodeJS.Socket; + + export class Socket extends EventEmitter { + public set(name: string, val: any): Socket; + + public get(name: string): any; + + public enable(name: string): Socket; + + public disable(name: string): Socket; + + public enabled(name: string): boolean; + + public disabled(name: string): boolean; + + public use(plugin: (socket: Socket) => any): Socket; + + public pack(args: Buffer | Buffer[]): Buffer; + + public closeSockets(): void; + + public close(): void; + + public closeServer(fn: () => any): void; + + public address(): + | { port: number; family: string; address: string; string: string } + | undefined; + + public removeSocket(sock: Socket): void; + + public addSocket(sock: Socket): void; + + public handleErrors(sock: Socket): void; + + public onmessage(sock: NetSocket): (args: Buffer | Buffer[]) => void; + + public connect( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public onconnect(sock: Socket): void; + + bind( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + } + + export class SubSocket extends Socket { + public hasSubscriptions(): boolean; + + public matches(topic: string): boolean; + + public onmessage(sock: NetSocket): (args: Buffer | Buffer[]) => void; + + public subscribe(re: RegExp | string): RegExp; + + public unsubscribe(re: RegExp | string): void; + + public clearSubscriptions(): void; + + /** + * @throws {Error} + */ + public send(): void; + } + + export class SubEmitterSocket { + public sock: Socket; + + public on(event: string, fn: (...args: any[]) => void): SubEmitterSocket; + + public off(event: string): SubEmitterSocket; + + public bind( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public connect( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public close(): void; + } + + export class PubSocket extends Socket { + public send(...args: any[]): PubSocket; + } + + export class PubEmitterSocket { + public sock: PubSocket; + + public send(...args: any[]): PubSocket; + + public bind( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public connect( + port: ConnectionPort, + host?: string | (() => void), + fn?: () => void, + ): Socket; + + public close(): void; + + public emit(event: string, data: any): void; + } + + export class PushSocket extends Socket { + public send(...args: any[]): void; + } + + export class ReqSocket extends Socket { + public id(): string; + + public onmessage(): (args: Buffer | Buffer[]) => void; + + public send(...args: any[]): void; + } + + export class RepSocket extends Socket { + public onmessage(sock: NetSocket): (args: Buffer | Buffer[]) => void; + } + + export class PullSocket extends Socket { + /** + * @throws {Error} + */ + public send(): void; + } + + export type ConnectionPort = + | number + | string + | { + protocol?: string; + hostname?: string; + pathname: string; + port: string | number; + }; + + export function socket( + type: string, + options?: any, + ): + | PubEmitterSocket + | SubEmitterSocket + | PushSocket + | PullSocket + | PubSocket + | SubSocket + | ReqSocket + | RepSocket + | Socket; + + export const types: { + [propName: string]: () => + | PubEmitterSocket + | SubEmitterSocket + | PushSocket + | PullSocket + | PubSocket + | SubSocket + | ReqSocket + | RepSocket + | Socket; + }; +} From e4015dc0e479e6980874d6b01e9b2ac4747c230a Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 11:04:44 +0200 Subject: [PATCH 17/35] Update controller/in_memory_channel to typescript --- framework/src/controller/action.ts | 30 +++-- framework/src/controller/bus.ts | 29 ++--- .../src/controller/channels/base_channel.ts | 40 +++--- .../controller/channels/in_memory_channel.js | 108 ---------------- .../controller/channels/in_memory_channel.ts | 115 ++++++++++++++++++ framework/src/controller/event.ts | 10 +- 6 files changed, 181 insertions(+), 151 deletions(-) delete mode 100644 framework/src/controller/channels/in_memory_channel.js create mode 100644 framework/src/controller/channels/in_memory_channel.ts diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index a9bf4aedf95..f2e488d315c 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -17,7 +17,7 @@ import { strict as assert } from 'assert'; const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; -export interface ActionObject { +export interface ActionInfoObject { readonly module: string; readonly name: string; readonly source?: string; @@ -25,7 +25,11 @@ export interface ActionObject { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ActionCallback = (action: ActionObject) => any; +export type ActionHandler = (action: ActionInfoObject) => any; + +export interface ActionsDefinition { + [key: string]: ActionHandler | { handler: ActionHandler; isPublic?: boolean }; +} export interface ActionsObject { [key: string]: Action; @@ -34,11 +38,20 @@ export interface ActionsObject { export class Action { public module: string; public name: string; + public isPublic: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public handler?: (action: ActionInfoObject) => any; public source?: string; public params?: object; - public isPublic?: boolean; - public constructor(name: string, params?: object, source?: string) { + public constructor( + name: string, + params?: object, + source?: string, + isPublic?: boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler?: (action: ActionInfoObject) => any, + ) { assert( actionWithModuleNameReg.test(name), `Action name "${name}" must be a valid name with module name.`, @@ -53,11 +66,14 @@ export class Action { ); this.source = source; } + + this.handler = handler; + this.isPublic = isPublic ?? false; } - public static deserialize(data: ActionObject | string): Action { + public static deserialize(data: ActionInfoObject | string): Action { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsedAction: ActionObject = + const parsedAction: ActionInfoObject = typeof data === 'string' ? JSON.parse(data) : data; return new Action( @@ -67,7 +83,7 @@ export class Action { ); } - public serialize(): ActionObject { + public serialize(): ActionInfoObject { return { name: this.name, module: this.module, diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 8d693f1ab2e..252ce481419 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -21,7 +21,7 @@ import { ReqSocket, SubEmitterSocket, } from 'pm2-axon'; -import { Action, ActionObject, ActionsObject } from './action'; +import { Action, ActionInfoObject, ActionsObject } from './action'; import { Logger } from '../types'; import { BaseChannel } from './channels/base_channel'; import { EventsArray } from './event'; @@ -138,25 +138,18 @@ export class Bus extends EventEmitter2 { options: RegisterChannelOptions, ): Promise { events.forEach(eventName => { - const eventFullName = `${moduleAlias}:${eventName}`; - if (this.events[eventFullName]) { - throw new Error( - `Event "${eventFullName}" already registered with bus.`, - ); + if (this.events[eventName]) { + throw new Error(`Event "${eventName}" already registered with bus.`); } - this.events[eventFullName] = true; + this.events[eventName] = true; }); Object.keys(actions).forEach(actionName => { - const actionFullName = `${moduleAlias}:${actionName}`; - - if (this.actions[actionFullName]) { - throw new Error( - `Action "${actionFullName}" already registered with bus.`, - ); + if (this.actions[actionName] === undefined) { + throw new Error(`Action "${actionName}" already registered with bus.`); } - this.actions[actionFullName] = actions[actionName]; + this.actions[actionName] = actions[actionName]; }); let { channel } = options; @@ -181,7 +174,7 @@ export class Bus extends EventEmitter2 { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invoke(actionData: string | ActionObject): Promise { + public async invoke(actionData: string | ActionInfoObject): Promise { const action = Action.deserialize(actionData); const actionModule = action.module; const actionFullName = action.key(); @@ -220,8 +213,10 @@ export class Bus extends EventEmitter2 { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invokePublic(actionData: string | ActionObject): Promise { + public async invokePublic( + actionData: string | ActionInfoObject, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { const action = Action.deserialize(actionData); // Check if action exists diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 6fe916632f4..660ba04c87c 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -13,7 +13,7 @@ */ import { Event, EventCallback } from '../event'; -import { Action, ActionCallback } from '../action'; +import { Action, ActionsDefinition, ActionsObject } from '../action'; import { INTERNAL_EVENTS, eventWithModuleNameReg } from './base/constants'; export interface BaseChannelOptions { @@ -24,14 +24,14 @@ export abstract class BaseChannel { public readonly moduleAlias: string; public readonly options: object; - public readonly eventsList: ReadonlyArray; - public readonly actionsList: ReadonlyArray; - public readonly actions: { [key: string]: ActionCallback }; + public readonly eventsList: ReadonlyArray; + public readonly actionsList: ReadonlyArray; + public readonly actions: ActionsObject; protected constructor( moduleAlias: string, events: ReadonlyArray, - actions: { [key: string]: ActionCallback }, + actions: ActionsDefinition, options: BaseChannelOptions = {}, ) { this.moduleAlias = moduleAlias; @@ -41,15 +41,29 @@ export abstract class BaseChannel { ? events : [...events, ...INTERNAL_EVENTS]; - this.eventsList = eventList.map( - eventName => new Event(`${this.moduleAlias}:${eventName}`), + this.eventsList = eventList.map(eventName => + new Event(`${this.moduleAlias}:${eventName}`).key(), ); - this.actionsList = Object.keys(actions).map( - actionName => new Action(`${this.moduleAlias}:${actionName}`), - ); - - this.actions = actions; + this.actions = {}; + for (const actionName of Object.keys(actions)) { + const actionData = actions[actionName]; + + const handler = + typeof actionData === 'object' ? actionData.handler : actionData; + const isPublic = + typeof actionData === 'object' ? actionData.isPublic ?? true : true; + + const action = new Action( + `${this.moduleAlias}:${actionName}`, + undefined, + undefined, + isPublic, + handler, + ); + this.actions[action.key()] = action; + } + this.actionsList = Object.keys(this.actions); } public isValidEventName(name: string, throwError = true): boolean | never { @@ -71,8 +85,6 @@ export abstract class BaseChannel { return result; } - abstract async registerToBus(): Promise; - // Listen to any event happening in the application // Specified as moduleName:eventName // If its related to your own moduleAlias specify as :eventName diff --git a/framework/src/controller/channels/in_memory_channel.js b/framework/src/controller/channels/in_memory_channel.js deleted file mode 100644 index 4138e950373..00000000000 --- a/framework/src/controller/channels/in_memory_channel.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const Event = require('../event'); -const Action = require('../action'); -const BaseChannel = require('./base_channel'); - -class InMemoryChannel extends BaseChannel { - constructor(moduleAlias, events, actions, options = {}) { - super(moduleAlias, events, actions, options); - } - - async registerToBus(bus) { - this.bus = bus; - await this.bus.registerChannel( - this.moduleAlias, - this.eventsList.map(event => event.name), - this.actions, - { type: 'inMemory', channel: this }, - ); - } - - subscribe(eventName, cb) { - this.bus.subscribe(new Event(eventName).key(), data => - setImmediate(cb, Event.deserialize(data)), - ); - } - - once(eventName, cb) { - this.bus.once(new Event(eventName).key(), data => - setImmediate(cb, Event.deserialize(data)), - ); - } - - publish(eventName, data) { - const event = new Event(eventName, data); - - if (event.module !== this.moduleAlias) { - throw new Error( - `Event "${eventName}" not registered in "${this.moduleAlias}" module.`, - ); - } - - this.bus.publish(event.key(), event.serialize()); - } - - // eslint-disable-next-line @typescript-eslint/require-await - async invoke(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; - - if (action.module === this.moduleAlias) { - if (!this.actions[action.name]) { - throw new Error( - `The action '${action.name}' on module '${this.moduleAlias}' does not exist.`, - ); - } - return this.actions[action.name].handler(action); - } - - return this.bus.invoke(action.serialize()); - } - - async invokeFromNetwork(remoteMethod, params) { - return this.invoke(`app:${remoteMethod}`, params); - } - - async publishToNetwork(actionName, data) { - return this.invoke(`app:${actionName}`, data); - } - - // eslint-disable-next-line @typescript-eslint/require-await - async invokePublic(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; - - if (action.module === this.moduleAlias) { - if (!this.actions[action.name].isPublic) { - throw new Error( - `Action '${action.name}' is not allowed because it's not public.`, - ); - } - - return this.actions[action.name].handler(action); - } - - return this.bus.invokePublic(action.serialize()); - } -} - -module.exports = InMemoryChannel; diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts new file mode 100644 index 00000000000..7903d512800 --- /dev/null +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -0,0 +1,115 @@ +/* + * Copyright © 2019 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { Event, EventCallback } from '../event'; +import { Action } from '../action'; +import { BaseChannel } from './base_channel'; +import { Bus } from '../bus'; + +export class InMemoryChannel extends BaseChannel { + public bus: Bus | undefined; + + public async registerToBus(bus: Bus): Promise { + this.bus = bus; + + await this.bus.registerChannel( + this.moduleAlias, + this.eventsList, + this.actions, + { type: 'inMemory', channel: this }, + ); + } + + public subscribe(eventName: string, cb: EventCallback): void { + (this.bus as Bus).subscribe(new Event(eventName).key(), data => + setImmediate(cb, Event.deserialize(data)), + ); + } + + public once(eventName: string, cb: EventCallback): void { + (this.bus as Bus).once(new Event(eventName).key(), data => + setImmediate(cb, Event.deserialize(data)), + ); + } + + public publish(eventName: string, data: object): void { + const event = new Event(eventName, data); + + if (event.module !== this.moduleAlias) { + throw new Error( + `Event "${eventName}" not registered in "${this.moduleAlias}" module.`, + ); + } + + (this.bus as Bus).publish(event.key(), event.serialize()); + } + + // eslint-disable-next-line @typescript-eslint/require-await,@typescript-eslint/no-explicit-any + public async invoke(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); + + if (action.module === this.moduleAlias) { + if (this.actions[action.key()] === undefined) { + throw new Error( + `The action '${action.key()}' on module '${ + this.moduleAlias + }' does not exist.`, + ); + } + + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.actions[action.key()].handler(action.serialize()); + } + + return (this.bus as Bus).invoke(action.serialize()); + } + + public async invokeFromNetwork( + remoteMethod: string, + params?: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + return this.invoke(`app:${remoteMethod}`, params); + } + + public async publishToNetwork( + actionName: string, + data: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + return this.invoke(`app:${actionName}`, data); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invokePublic(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); + + if (action.module === this.moduleAlias) { + if (!this.actions[action.name].isPublic) { + throw new Error( + `Action '${action.name}' is not allowed because it's not public.`, + ); + } + + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.actions[action.name].handler(action.serialize()); + } + + return (this.bus as Bus).invokePublic(action.serialize()); + } +} diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 7fda7e9391f..49fe5fb7ba4 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -16,13 +16,13 @@ import { strict as assert } from 'assert'; import { eventWithModuleNameReg } from './channels/base/constants'; -export interface EventObject { +export interface EventInfoObject { readonly module: string; readonly name: string; readonly data?: object | string; } -export type EventCallback = (action: EventObject) => void; +export type EventCallback = (action: EventInfoObject) => void; export type EventsArray = ReadonlyArray; @@ -43,9 +43,9 @@ export class Event { this.data = data; } - public static deserialize(data: EventObject | string): Event { + public static deserialize(data: EventInfoObject | string): Event { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsedEvent: EventObject = + const parsedEvent: EventInfoObject = typeof data === 'string' ? JSON.parse(data) : data; return new Event( @@ -54,7 +54,7 @@ export class Event { ); } - public serialize(): EventObject { + public serialize(): EventInfoObject { return { name: this.name, module: this.module, From e5ee959d37fdc113411f912039881e0ce47d926a Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 12:11:35 +0200 Subject: [PATCH 18/35] Update controller/child_process_channel to typescript --- framework/src/controller/bus.ts | 13 +- .../channels/child_process/index.js | 25 --- ...ss_channel.js => child_process_channel.ts} | 194 +++++++++++------- .../channels/{index.js => index.ts} | 14 +- framework/types/pm2-axon-rpc/index.d.ts | 3 + 5 files changed, 135 insertions(+), 114 deletions(-) delete mode 100644 framework/src/controller/channels/child_process/index.js rename framework/src/controller/channels/{child_process_channel.js => child_process_channel.ts} (51%) rename framework/src/controller/channels/{index.js => index.ts} (64%) diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 252ce481419..a090f034b43 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -29,15 +29,18 @@ import { EventsArray } from './event'; const CONTROLLER_IDENTIFIER = 'app'; const SOCKET_TIMEOUT_TIME = 2000; +export interface socketPathObject { + readonly pub: string; + readonly sub: string; + readonly rpc: string; + readonly root: string; +} + interface BusConfiguration { ipc: { readonly enabled: boolean; }; - socketsPath: { - readonly pub: string; - readonly sub: string; - readonly rpc: string; - }; + socketsPath: socketPathObject; } interface RegisterChannelOptions { diff --git a/framework/src/controller/channels/child_process/index.js b/framework/src/controller/channels/child_process/index.js deleted file mode 100644 index 8205b39a583..00000000000 --- a/framework/src/controller/channels/child_process/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const setupProcessHandlers = channel => { - process.once('SIGTERM', () => channel.cleanup(1)); - process.once('SIGINT', () => channel.cleanup(1)); - process.once('exit', (error, code) => channel.cleanup(code, error)); -}; - -module.exports = { - setupProcessHandlers, -}; diff --git a/framework/src/controller/channels/child_process_channel.js b/framework/src/controller/channels/child_process_channel.ts similarity index 51% rename from framework/src/controller/channels/child_process_channel.js rename to framework/src/controller/channels/child_process_channel.ts index 32d735ad421..e58e9a46e1d 100644 --- a/framework/src/controller/channels/child_process_channel.js +++ b/framework/src/controller/channels/child_process_channel.ts @@ -12,39 +12,68 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const { EventEmitter2 } = require('eventemitter2'); -const axon = require('pm2-axon'); -const { Server: RPCServer, Client: RPCClient } = require('pm2-axon-rpc'); -const util = require('util'); -const Action = require('../action'); -const Event = require('../event'); -const BaseChannel = require('./base_channel'); -const { setupProcessHandlers } = require('./child_process'); +import { EventEmitter2 } from 'eventemitter2'; +import * as axon from 'pm2-axon'; +import { Server as RPCServer, Client as RPCClient, Client } from 'pm2-axon-rpc'; +import * as util from 'util'; +import { + PubEmitterSocket, + RepSocket, + ReqSocket, + SubEmitterSocket, +} from 'pm2-axon'; +import { Action, ActionsDefinition } from '../action'; +import { Event, EventCallback, EventInfoObject } from '../event'; +import { BaseChannel, BaseChannelOptions } from './base_channel'; +import { socketPathObject } from '../bus'; + +export const setupProcessHandlers = (channel: ChildProcessChannel): void => { + process.once('SIGTERM', () => channel.cleanup(1)); + process.once('SIGINT', () => channel.cleanup(1)); + process.once('exit', code => channel.cleanup(code)); +}; + +type NodeCallback = (error: Error | null, result?: number) => void; const SOCKET_TIMEOUT_TIME = 2000; -class ChildProcessChannel extends BaseChannel { - constructor(moduleAlias, events, actions, options = {}) { +export class ChildProcessChannel extends BaseChannel { + public localBus: EventEmitter2; + public subSocket?: SubEmitterSocket; + public busRpcSocket?: ReqSocket; + public busRpcClient?: Client; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public busRpcClientCallPromisified: any; + public pubSocket?: PubEmitterSocket; + public rpcSocketPath?: socketPathObject | string; + public rpcSocket?: RepSocket; + public rpcServer?: RPCServer; + + protected constructor( + moduleAlias: string, + events: ReadonlyArray, + actions: ActionsDefinition, + options: BaseChannelOptions = {}, + ) { super(moduleAlias, events, actions, options); this.localBus = new EventEmitter2(); setupProcessHandlers(this); } - async registerToBus(socketsPath) { - this.subSocket = axon.socket('sub-emitter'); + public async registerToBus(socketsPath: socketPathObject): Promise { + this.subSocket = axon.socket('sub-emitter') as SubEmitterSocket; this.subSocket.connect(socketsPath.pub); - this.busRpcSocket = axon.socket('req'); + this.busRpcSocket = axon.socket('req') as ReqSocket; this.busRpcSocket.connect(socketsPath.rpc); this.busRpcClient = new RPCClient(this.busRpcSocket); + // eslint-disable-next-line @typescript-eslint/unbound-method this.busRpcClientCallPromisified = util.promisify(this.busRpcClient.call); // Channel Publish Socket is only required if the module has events if (this.eventsList.length > 0) { - this.pubSocket = axon.socket('pub-emitter'); + this.pubSocket = axon.socket('pub-emitter') as PubEmitterSocket; this.pubSocket.connect(socketsPath.sub); } @@ -52,62 +81,65 @@ class ChildProcessChannel extends BaseChannel { if (this.actionsList.length > 0) { this.rpcSocketPath = `unix://${socketsPath.root}/${this.moduleAlias}_rpc.sock`; - this.rpcSocket = axon.socket('rep'); + this.rpcSocket = axon.socket('rep') as RepSocket; this.rpcSocket.bind(this.rpcSocketPath); this.rpcServer = new RPCServer(this.rpcSocket); - this.rpcServer.expose('invoke', (action, cb) => { + this.rpcServer.expose('invoke', (action: string, cb: NodeCallback) => { this.invoke(action) .then(data => cb(null, data)) .catch(error => cb(error)); }); - this.rpcServer.expose('invokePublic', (action, cb) => { - this.invokePublic(action) - .then(data => cb(null, data)) - .catch(error => cb(error)); - }); + this.rpcServer.expose( + 'invokePublic', + (action: string, cb: NodeCallback) => { + this.invokePublic(action) + .then(data => cb(null, data)) + .catch(error => cb(error)); + }, + ); } return this.setupSockets(); } - async setupSockets() { - return Promise.race([ + public async setupSockets(): Promise { + await Promise.race([ this._resolveWhenAllSocketsBound(), this._rejectWhenAnySocketFailsToBind(), this._rejectWhenTimeout(SOCKET_TIMEOUT_TIME), - ]).finally(() => { - this._removeAllListeners(); - }); + ]); + + await this._removeAllListeners(); } - subscribe(eventName, cb) { + public subscribe(eventName: string, cb: EventCallback): void { const event = new Event(eventName); if (event.module === this.moduleAlias) { this.localBus.on(eventName, cb); } else { - this.subSocket.on(eventName, data => { + this.subSocket?.on(eventName, (data: EventInfoObject) => { cb(data); }); } } - once(eventName, cb) { + public once(eventName: string, cb: EventCallback): void { const event = new Event(eventName); if (event.module === this.moduleAlias) { this.localBus.once(eventName, cb); } else { - this.subSocket.on(eventName, data => { - this.subSocket.off(eventName); + this.subSocket?.on(eventName, (data: EventInfoObject) => { + this.subSocket?.off(eventName); cb(data); }); } } - publish(eventName, data) { + public publish(eventName: string, data: object): void { const event = new Event(eventName, data); if (event.module !== this.moduleAlias) { @@ -123,40 +155,52 @@ class ChildProcessChannel extends BaseChannel { } } - async invoke(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invoke(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.actions[action.name].handler(action); } return new Promise((resolve, reject) => { - this.busRpcClient.call('invoke', action.serialize(), (err, data) => { - if (err) { - return reject(err); - } + this.busRpcClient?.call( + 'invoke', + action.serialize(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: Error, data: any) => { + if (err) { + return reject(err); + } - return resolve(data); - }); + return resolve(data); + }, + ); }); } - async invokeFromNetwork(remoteMethod, params) { + public async invokeFromNetwork( + remoteMethod: string, + params: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { return this.invoke(`app:${remoteMethod}`, params); } - async publishToNetwork(actionName, data) { + public async publishToNetwork( + actionName: string, + data: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { return this.invoke(`app:${actionName}`, data); } - async invokePublic(actionName, params) { - const action = - typeof actionName === 'string' - ? new Action(actionName, params, this.moduleAlias) - : actionName; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async invokePublic(actionName: string, params?: object): Promise { + const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { if (!this.actions[action.name].isPublic) { @@ -165,14 +209,18 @@ class ChildProcessChannel extends BaseChannel { ); } + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.actions[action.name].handler(action); } return new Promise((resolve, reject) => { - this.busRpcClient.call( + this.busRpcClient?.call( 'invokePublic', action.serialize(), - (err, data) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: Error, data: any) => { if (err) { return reject(err); } @@ -184,7 +232,7 @@ class ChildProcessChannel extends BaseChannel { } // eslint-disable-next-line @typescript-eslint/require-await - async cleanup() { + public cleanup(_status?: number, _message?: string): void { if (this.pubSocket) { this.pubSocket.close(); } @@ -199,13 +247,13 @@ class ChildProcessChannel extends BaseChannel { } } - async _resolveWhenAllSocketsBound() { + private async _resolveWhenAllSocketsBound(): Promise { const promises = []; if (this.pubSocket) { promises.push( new Promise(resolve => { - this.pubSocket.sock.once('connect', () => { + this.pubSocket?.sock.once('connect', () => { resolve(); }); }), @@ -215,7 +263,7 @@ class ChildProcessChannel extends BaseChannel { if (this.subSocket) { promises.push( new Promise(resolve => { - this.subSocket.sock.once('connect', () => { + this.subSocket?.sock.once('connect', () => { resolve(); }); }), @@ -225,7 +273,7 @@ class ChildProcessChannel extends BaseChannel { if (this.rpcSocket) { promises.push( new Promise(resolve => { - this.rpcSocket.once('bind', () => { + this.rpcSocket?.once('bind', () => { resolve(); }); }), @@ -235,14 +283,15 @@ class ChildProcessChannel extends BaseChannel { if (this.busRpcSocket && this.busRpcClient) { promises.push( new Promise((resolve, reject) => { - this.busRpcSocket.once('connect', () => { - this.busRpcClient.call( + this.busRpcSocket?.once('connect', () => { + this.busRpcClient?.call( 'registerChannel', this.moduleAlias, - this.eventsList.map(event => event.name), - this.actionsList.map(action => action.name), + this.eventsList.map((event: string) => event), + this.actionsList.map((action: string) => action), { type: 'ipcSocket', rpcSocketPath: this.rpcSocketPath }, - (err, result) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err: Error, result: any) => { if (err) { reject(err); } @@ -254,16 +303,16 @@ class ChildProcessChannel extends BaseChannel { ); } - return Promise.all(promises); + await Promise.all(promises); } - async _rejectWhenAnySocketFailsToBind() { + private async _rejectWhenAnySocketFailsToBind() { const promises = []; if (this.pubSocket) { promises.push( new Promise((_, reject) => { - this.pubSocket.sock.once('error', err => { + this.pubSocket?.sock.once('error', (err: Error) => { reject(err); }); }), @@ -273,7 +322,7 @@ class ChildProcessChannel extends BaseChannel { if (this.subSocket) { promises.push( new Promise((_, reject) => { - this.subSocket.sock.once('error', err => { + this.subSocket?.sock.once('error', (err: Error) => { reject(err); }); }), @@ -283,7 +332,7 @@ class ChildProcessChannel extends BaseChannel { if (this.rpcSocket) { promises.push( new Promise((_, reject) => { - this.rpcSocket.once('error', err => { + this.rpcSocket?.once('error', (err: Error) => { reject(err); }); }), @@ -294,7 +343,7 @@ class ChildProcessChannel extends BaseChannel { } // eslint-disable-next-line class-methods-use-this - async _rejectWhenTimeout(timeInMillis) { + private async _rejectWhenTimeout(timeInMillis: number) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error('ChildProcessChannel sockets setup timeout')); @@ -302,7 +351,8 @@ class ChildProcessChannel extends BaseChannel { }); } - _removeAllListeners() { + // eslint-disable-next-line @typescript-eslint/require-await + private async _removeAllListeners(): Promise { if (this.subSocket) { this.subSocket.sock.removeAllListeners('connect'); this.subSocket.sock.removeAllListeners('error'); @@ -324,5 +374,3 @@ class ChildProcessChannel extends BaseChannel { } } } - -module.exports = ChildProcessChannel; diff --git a/framework/src/controller/channels/index.js b/framework/src/controller/channels/index.ts similarity index 64% rename from framework/src/controller/channels/index.js rename to framework/src/controller/channels/index.ts index 0864e17ea22..0331ca24f24 100644 --- a/framework/src/controller/channels/index.js +++ b/framework/src/controller/channels/index.ts @@ -12,14 +12,6 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const BaseChannel = require('./base_channel.js'); -const InMemoryChannel = require('./in_memory_channel.js'); -const ChildProcessChannel = require('./child_process_channel.js'); - -module.exports = { - BaseChannel, - InMemoryChannel, - ChildProcessChannel, -}; +export { BaseChannel } from './base_channel'; +export { InMemoryChannel } from './in_memory_channel'; +export { ChildProcessChannel } from './child_process_channel'; diff --git a/framework/types/pm2-axon-rpc/index.d.ts b/framework/types/pm2-axon-rpc/index.d.ts index bff9d00a32e..4d47ecc0e71 100644 --- a/framework/types/pm2-axon-rpc/index.d.ts +++ b/framework/types/pm2-axon-rpc/index.d.ts @@ -12,5 +12,8 @@ declare module 'pm2-axon-rpc' { export class Client { public constructor(socket: ReqSocket); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public call(...args: any[]): void; } } From 8d290cb2b303939f16e5a64a25fe987701f9e111 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 12:16:55 +0200 Subject: [PATCH 19/35] Update controller/child_process_loader to typescript --- .../controller/channels/child_process_channel.ts | 4 ++-- ...ld_process_loader.js => child_process_loader.ts} | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) rename framework/src/controller/{child_process_loader.js => child_process_loader.ts} (79%) diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index e58e9a46e1d..07eaef3829a 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -49,7 +49,7 @@ export class ChildProcessChannel extends BaseChannel { public rpcSocket?: RepSocket; public rpcServer?: RPCServer; - protected constructor( + public constructor( moduleAlias: string, events: ReadonlyArray, actions: ActionsDefinition, @@ -139,7 +139,7 @@ export class ChildProcessChannel extends BaseChannel { } } - public publish(eventName: string, data: object): void { + public publish(eventName: string, data?: object): void { const event = new Event(eventName, data); if (event.module !== this.moduleAlias) { diff --git a/framework/src/controller/child_process_loader.js b/framework/src/controller/child_process_loader.ts similarity index 79% rename from framework/src/controller/child_process_loader.js rename to framework/src/controller/child_process_loader.ts index e12fac77fc2..0924f58ac2e 100644 --- a/framework/src/controller/child_process_loader.js +++ b/framework/src/controller/child_process_loader.ts @@ -12,16 +12,16 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - // Parameters passed by `child_process.fork(_, parameters)` -const modulePath = process.argv[2]; +import { socketPathObject } from './bus'; + +import { ChildProcessChannel } from './channels'; -const { ChildProcessChannel } = require('./channels'); +const modulePath: string = process.argv[2]; // eslint-disable-next-line import/no-dynamic-require const Klass = require(modulePath); -const _loadModule = async (config, moduleOptions) => { +const _loadModule = async (config: object, moduleOptions: object) => { const module = new Klass(moduleOptions); const moduleAlias = module.constructor.alias; @@ -31,7 +31,8 @@ const _loadModule = async (config, moduleOptions) => { module.actions, ); - await channel.registerToBus(config.socketsPath); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await channel.registerToBus((config as any).socketsPath as socketPathObject); channel.publish(`${moduleAlias}:registeredToBus`); channel.publish(`${moduleAlias}:loading:started`); From 19487f79c9561215ba9ddecfdfe17e501542ff6f Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 16:31:04 +0200 Subject: [PATCH 20/35] Update controller/controller to typescript --- framework/src/controller/bus.ts | 14 +- .../src/controller/channels/base_channel.ts | 2 +- .../channels/child_process_channel.ts | 6 +- .../controller/channels/in_memory_channel.ts | 2 +- .../src/controller/child_process_loader.ts | 4 +- .../{controller.js => controller.ts} | 166 ++++++++++++++---- framework/src/controller/types.ts | 20 +++ framework/src/modules/base_module.js | 64 ------- framework/src/modules/base_module.ts | 52 ++++++ framework/src/types.ts | 12 ++ 10 files changed, 222 insertions(+), 120 deletions(-) rename framework/src/controller/{controller.js => controller.ts} (62%) create mode 100644 framework/src/controller/types.ts delete mode 100644 framework/src/modules/base_module.js create mode 100644 framework/src/modules/base_module.ts diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index a090f034b43..50ac7dcd98c 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -13,34 +13,28 @@ */ import * as axon from 'pm2-axon'; -import { Server as RPCServer, Client as RPCClient } from 'pm2-axon-rpc'; -import { EventEmitter2, Listener } from 'eventemitter2'; import { PubEmitterSocket, RepSocket, ReqSocket, SubEmitterSocket, } from 'pm2-axon'; +import { Client as RPCClient, Server as RPCServer } from 'pm2-axon-rpc'; +import { EventEmitter2, Listener } from 'eventemitter2'; import { Action, ActionInfoObject, ActionsObject } from './action'; import { Logger } from '../types'; import { BaseChannel } from './channels/base_channel'; import { EventsArray } from './event'; +import { SocketPaths } from './types'; const CONTROLLER_IDENTIFIER = 'app'; const SOCKET_TIMEOUT_TIME = 2000; -export interface socketPathObject { - readonly pub: string; - readonly sub: string; - readonly rpc: string; - readonly root: string; -} - interface BusConfiguration { ipc: { readonly enabled: boolean; }; - socketsPath: socketPathObject; + socketsPath: SocketPaths; } interface RegisterChannelOptions { diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 660ba04c87c..10612919f2f 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -28,7 +28,7 @@ export abstract class BaseChannel { public readonly actionsList: ReadonlyArray; public readonly actions: ActionsObject; - protected constructor( + public constructor( moduleAlias: string, events: ReadonlyArray, actions: ActionsDefinition, diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index 07eaef3829a..b52d37fa073 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -25,7 +25,7 @@ import { import { Action, ActionsDefinition } from '../action'; import { Event, EventCallback, EventInfoObject } from '../event'; import { BaseChannel, BaseChannelOptions } from './base_channel'; -import { socketPathObject } from '../bus'; +import { SocketPaths } from '../types'; export const setupProcessHandlers = (channel: ChildProcessChannel): void => { process.once('SIGTERM', () => channel.cleanup(1)); @@ -45,7 +45,7 @@ export class ChildProcessChannel extends BaseChannel { // eslint-disable-next-line @typescript-eslint/no-explicit-any public busRpcClientCallPromisified: any; public pubSocket?: PubEmitterSocket; - public rpcSocketPath?: socketPathObject | string; + public rpcSocketPath?: SocketPaths | string; public rpcSocket?: RepSocket; public rpcServer?: RPCServer; @@ -61,7 +61,7 @@ export class ChildProcessChannel extends BaseChannel { setupProcessHandlers(this); } - public async registerToBus(socketsPath: socketPathObject): Promise { + public async registerToBus(socketsPath: SocketPaths): Promise { this.subSocket = axon.socket('sub-emitter') as SubEmitterSocket; this.subSocket.connect(socketsPath.pub); diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index 7903d512800..ba0887450c2 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -43,7 +43,7 @@ export class InMemoryChannel extends BaseChannel { ); } - public publish(eventName: string, data: object): void { + public publish(eventName: string, data?: object): void { const event = new Event(eventName, data); if (event.module !== this.moduleAlias) { diff --git a/framework/src/controller/child_process_loader.ts b/framework/src/controller/child_process_loader.ts index 0924f58ac2e..73eea94e98c 100644 --- a/framework/src/controller/child_process_loader.ts +++ b/framework/src/controller/child_process_loader.ts @@ -13,9 +13,9 @@ */ // Parameters passed by `child_process.fork(_, parameters)` -import { socketPathObject } from './bus'; import { ChildProcessChannel } from './channels'; +import { SocketPaths } from './types'; const modulePath: string = process.argv[2]; // eslint-disable-next-line import/no-dynamic-require @@ -32,7 +32,7 @@ const _loadModule = async (config: object, moduleOptions: object) => { ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - await channel.registerToBus((config as any).socketsPath as socketPathObject); + await channel.registerToBus((config as any).socketsPath as SocketPaths); channel.publish(`${moduleAlias}:registeredToBus`); channel.publish(`${moduleAlias}:loading:started`); diff --git a/framework/src/controller/controller.js b/framework/src/controller/controller.ts similarity index 62% rename from framework/src/controller/controller.js rename to framework/src/controller/controller.ts index 137143a9bf5..c49a68b8055 100644 --- a/framework/src/controller/controller.js +++ b/framework/src/controller/controller.ts @@ -12,33 +12,97 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const fs = require('fs-extra'); -const path = require('path'); -const childProcess = require('child_process'); -const psList = require('ps-list'); -const systemDirs = require('../application/system_dirs'); -const { InMemoryChannel } = require('./channels'); -const Bus = require('./bus'); -const { DuplicateAppInstanceError } = require('../errors'); -const { validateModuleSpec } = require('../application/validator'); - -const isPidRunning = async pid => +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as childProcess from 'child_process'; +import * as psList from 'ps-list'; +import { ChildProcess } from 'child_process'; +import * as systemDirs from '../application/system_dirs'; +import { InMemoryChannel } from './channels'; +import { Bus } from './bus'; +import { DuplicateAppInstanceError } from '../errors'; +import { validateModuleSpec } from '../application/validator'; +import { Logger, Storage } from '../types'; +import { SocketPaths } from './types'; +import { BaseModule } from '../modules/base_module'; + +const isPidRunning = async (pid: number) => psList().then(list => list.some(x => x.pid === pid)); +export interface ControllerOptions { + readonly appLabel: string; + readonly config: { + readonly tempPath: string; + readonly ipc: { + readonly enabled: boolean; + }; + }; + readonly logger: Logger; + readonly storage: Storage; + readonly channel: InMemoryChannel; +} + +interface ControllerConfig { + readonly tempPath: string; + readonly socketsPath: SocketPaths; + readonly dirs: { + readonly temp: string; + readonly sockets: string; + readonly pids: string; + }; + readonly ipc: { + readonly enabled: boolean; + }; +} + +interface ModulesObject { + readonly [key: string]: typeof BaseModule; +} + +interface ModuleOptions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly [key: string]: any; + readonly loadAsChildProcess: boolean; +} + +interface ModulesOptions { + readonly [key: string]: ModuleOptions; +} + +interface Migrations { + readonly [key: string]: ReadonlyArray; +} + class Controller { - constructor({ appLabel, config, logger, storage, channel }) { - this.logger = logger; - this.storage = storage; - this.appLabel = appLabel; - this.channel = channel; + public readonly logger: Logger; + public readonly storage: Storage; + public readonly appLabel: string; + public readonly channel: InMemoryChannel; + public readonly config: ControllerConfig; + public modules: { + [key: string]: BaseModule; + }; + public childrenList: Array; + public bus: Bus | undefined; + + public constructor(options: ControllerOptions) { + this.logger = options.logger; + this.storage = options.storage; + this.appLabel = options.appLabel; + this.channel = options.channel; this.logger.info('Initializing controller'); - const dirs = systemDirs(this.appLabel, config.tempPath); + const dirs = systemDirs(this.appLabel, options.config.tempPath); this.config = { - ...config, - dirs, + tempPath: dirs.temp, + ipc: { + enabled: options.config.ipc.enabled, + }, + dirs: { + temp: dirs.temp, + sockets: dirs.sockets, + pids: dirs.pids, + }, socketsPath: { root: `unix://${dirs.sockets}`, pub: `unix://${dirs.sockets}/lisk_pub.sock`, @@ -49,10 +113,13 @@ class Controller { this.modules = {}; this.childrenList = []; - this.bus = null; } - async load(modules, moduleOptions, migrations = {}) { + public async load( + modules: ModulesObject, + moduleOptions: ModulesOptions, + migrations: Migrations = {}, + ): Promise { this.logger.info('Loading controller'); await this._setupDirectories(); await this._validatePidFile(); @@ -60,23 +127,23 @@ class Controller { await this._loadMigrations({ ...migrations }); await this._loadModules(modules, moduleOptions); - this.logger.debug(this.bus.getEvents(), 'Bus listening to events'); - this.logger.debug(this.bus.getActions(), 'Bus ready for actions'); + this.logger.debug(this.bus?.getEvents(), 'Bus listening to events'); + this.logger.debug(this.bus?.getActions(), 'Bus ready for actions'); } // eslint-disable-next-line class-methods-use-this - async _setupDirectories() { + private async _setupDirectories(): Promise { // Make sure all directories exists await fs.ensureDir(this.config.dirs.temp); await fs.ensureDir(this.config.dirs.sockets); await fs.ensureDir(this.config.dirs.pids); } - async _validatePidFile() { + private async _validatePidFile() { const pidPath = `${this.config.dirs.pids}/controller.pid`; const pidExists = await fs.pathExists(pidPath); if (pidExists) { - const pid = parseInt(await fs.readFile(pidPath), 10); + const pid = parseInt((await fs.readFile(pidPath)).toString(), 10); const pidRunning = await isPidRunning(pid); this.logger.info({ pid }, 'Previous Lisk PID'); @@ -93,7 +160,7 @@ class Controller { await fs.writeFile(pidPath, process.pid); } - async _setupBus() { + private async _setupBus() { this.bus = new Bus( { wildcard: true, @@ -117,11 +184,14 @@ class Controller { } // eslint-disable-next-line @typescript-eslint/require-await - async _loadMigrations(migrationsObj) { + private async _loadMigrations(migrationsObj: Migrations) { return this.storage.entities.Migration.applyAll(migrationsObj); } - async _loadModules(modules, moduleOptions) { + private async _loadModules( + modules: ModulesObject, + moduleOptions: ModulesOptions, + ): Promise { // To perform operations in sequence and not using bluebird for (const alias of Object.keys(modules)) { const klass = modules[alias]; @@ -142,10 +212,16 @@ class Controller { } } - async _loadInMemoryModule(alias, Klass, options) { + private async _loadInMemoryModule( + alias: string, + Klass: typeof BaseModule, + options: ModuleOptions, + ): Promise { const moduleAlias = alias || Klass.alias; const { name, version } = Klass.info; + // eslint-disable-next-line + // @ts-ignore const module = new Klass(options); validateModuleSpec(module); @@ -160,7 +236,7 @@ class Controller { module.actions, ); - await channel.registerToBus(this.bus); + await channel.registerToBus(this.bus as Bus); channel.publish(`${moduleAlias}:registeredToBus`); channel.publish(`${moduleAlias}:loading:started`); @@ -174,7 +250,13 @@ class Controller { this.logger.info({ name, version, moduleAlias }, 'Loaded in-memory module'); } - async _loadChildProcessModule(alias, Klass, options) { + private async _loadChildProcessModule( + alias: string, + Klass: typeof BaseModule, + options: ModuleOptions, + ): Promise { + // eslint-disable-next-line + // @ts-ignore const module = new Klass(options); validateModuleSpec(module); @@ -197,10 +279,14 @@ class Controller { const parameters = [modulePath]; // Avoid child processes and the main process sharing the same debugging ports causing a conflict - const forkedProcessOptions = {}; + const forkedProcessOptions = { + execArgv: undefined, + }; const maxPort = 20000; const minPort = 10000; if (process.env.NODE_DEBUG) { + // eslint-disable-next-line + // @ts-ignore forkedProcessOptions.execArgv = [ `--inspect=${Math.floor( Math.random() * (maxPort - minPort) + minPort, @@ -228,7 +314,7 @@ class Controller { process.exit(1); }); - return Promise.race([ + await Promise.race([ new Promise(resolve => { this.channel.once(`${moduleAlias}:loading:finished`, () => { this.logger.info( @@ -244,7 +330,9 @@ class Controller { ]); } - async unloadModules(modules = Object.keys(this.modules)) { + public async unloadModules( + modules = Object.keys(this.modules), + ): Promise { // To perform operations in sequence and not using bluebird for (const alias of modules) { @@ -253,7 +341,7 @@ class Controller { } } - async cleanup(code, reason) { + public async cleanup(_code: number, reason: string): Promise { this.logger.info('Cleanup controller...'); if (reason) { @@ -263,7 +351,7 @@ class Controller { this.childrenList.forEach(child => child.kill()); try { - await this.bus.cleanup(); + await this.bus?.cleanup(); await this.unloadModules(); this.logger.info('Unload completed'); } catch (err) { diff --git a/framework/src/controller/types.ts b/framework/src/controller/types.ts new file mode 100644 index 00000000000..cd2ea695df4 --- /dev/null +++ b/framework/src/controller/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright © 2020 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +export interface SocketPaths { + readonly pub: string; + readonly sub: string; + readonly rpc: string; + readonly root: string; +} diff --git a/framework/src/modules/base_module.js b/framework/src/modules/base_module.js deleted file mode 100644 index 7dbd92c32fa..00000000000 --- a/framework/src/modules/base_module.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const { ImplementationMissingError } = require('../errors'); - -/* eslint-disable class-methods-use-this,no-unused-vars */ - -module.exports = class BaseModule { - constructor(options) { - this.options = options; - } - - static get alias() { - throw new ImplementationMissingError(); - } - - static get info() { - throw new ImplementationMissingError(); - } - - // Array of migrations to be executed before loading the module. Expected format: ['yyyyMMddHHmmss_name_of_migration.sql'] - static get migrations() { - return []; - } - - get defaults() { - // This interface is not required to be implemented - return {}; - } - - get events() { - // This interface is not required to be implemented - return []; - } - - get actions() { - // This interface is not required to be implemented - return {}; - } - - // eslint-disable-next-line @typescript-eslint/require-await - async load(channel) { - throw new ImplementationMissingError(); - } - - // eslint-disable-next-line @typescript-eslint/require-await - async unload() { - // This interface is not required. - return true; - } -}; diff --git a/framework/src/modules/base_module.ts b/framework/src/modules/base_module.ts new file mode 100644 index 00000000000..48739d144df --- /dev/null +++ b/framework/src/modules/base_module.ts @@ -0,0 +1,52 @@ +/* + * Copyright © 2019 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { ImplementationMissingError } from '../errors'; +import { EventsArray } from '../controller/event'; +import { ActionsDefinition } from '../controller/action'; +import { BaseChannel } from '../controller/channels/base_channel'; + +export interface ModuleInfo { + readonly author: string; + readonly version: string; + readonly name: string; +} + +export abstract class BaseModule { + public readonly options: object; + + public constructor(options: object) { + this.options = options; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public static get alias(): string { + throw new ImplementationMissingError(); + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + public static get info(): ModuleInfo { + throw new ImplementationMissingError(); + } + + // Array of migrations to be executed before loading the module. Expected format: ['yyyyMMddHHmmss_name_of_migration.sql'] + public static get migrations(): ReadonlyArray { + return []; + } + + public abstract get defaults(): object; + public abstract get events(): EventsArray; + public abstract get actions(): ActionsDefinition; + public abstract async load(channel: BaseChannel): Promise; + public abstract async unload(): Promise; +} diff --git a/framework/src/types.ts b/framework/src/types.ts index f106229f0f0..d9007dd9007 100644 --- a/framework/src/types.ts +++ b/framework/src/types.ts @@ -45,6 +45,7 @@ export interface Logger { readonly warn: (data?: object | unknown, message?: string) => void; readonly error: (data?: object | unknown, message?: string) => void; readonly fatal: (data?: object | unknown, message?: string) => void; + readonly level: () => number; } /* Start Database */ @@ -56,6 +57,17 @@ export interface Storage { } export interface KeyValEntity { + readonly NetworkInfo: NetworkInfoEntity; + readonly Migration: MigrationEntity; +} + +export interface MigrationEntity { + readonly applyAll: (migrations: { + readonly [key: string]: ReadonlyArray; + }) => Promise; +} + +export interface NetworkInfoEntity { readonly getKey: ( key: string, tx?: StorageTransaction, From f5b8a0b96584b58e651b9a75c561c0077e3ec153 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 16:54:37 +0200 Subject: [PATCH 21/35] Update controller constatns to top level file --- framework/src/controller/action.ts | 4 +--- framework/src/controller/bus.ts | 6 ++---- framework/src/controller/channels/base_channel.ts | 2 +- framework/src/controller/{channels/base => }/constants.ts | 4 ++++ framework/src/controller/event.ts | 2 +- .../unit/specs/controller/channels/base/constants.spec.js | 2 +- .../unit/specs/controller/channels/base_channel.spec.js | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename framework/src/controller/{channels/base => }/constants.ts (74%) diff --git a/framework/src/controller/action.ts b/framework/src/controller/action.ts index f2e488d315c..35608671a00 100644 --- a/framework/src/controller/action.ts +++ b/framework/src/controller/action.ts @@ -13,9 +13,7 @@ */ import { strict as assert } from 'assert'; - -const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; -const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; +import { actionWithModuleNameReg, moduleNameReg } from './constants'; export interface ActionInfoObject { readonly module: string; diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 50ac7dcd98c..619a82310f9 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -23,12 +23,10 @@ import { Client as RPCClient, Server as RPCServer } from 'pm2-axon-rpc'; import { EventEmitter2, Listener } from 'eventemitter2'; import { Action, ActionInfoObject, ActionsObject } from './action'; import { Logger } from '../types'; -import { BaseChannel } from './channels/base_channel'; +import { BaseChannel } from './channels'; import { EventsArray } from './event'; import { SocketPaths } from './types'; - -const CONTROLLER_IDENTIFIER = 'app'; -const SOCKET_TIMEOUT_TIME = 2000; +import { CONTROLLER_IDENTIFIER, SOCKET_TIMEOUT_TIME } from './constants'; interface BusConfiguration { ipc: { diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 10612919f2f..085b70044e4 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -14,7 +14,7 @@ import { Event, EventCallback } from '../event'; import { Action, ActionsDefinition, ActionsObject } from '../action'; -import { INTERNAL_EVENTS, eventWithModuleNameReg } from './base/constants'; +import { INTERNAL_EVENTS, eventWithModuleNameReg } from '../constants'; export interface BaseChannelOptions { readonly skipInternalEvents?: boolean; diff --git a/framework/src/controller/channels/base/constants.ts b/framework/src/controller/constants.ts similarity index 74% rename from framework/src/controller/channels/base/constants.ts rename to framework/src/controller/constants.ts index 78d957ef137..8a2fd97a85d 100644 --- a/framework/src/controller/channels/base/constants.ts +++ b/framework/src/controller/constants.ts @@ -19,3 +19,7 @@ export const INTERNAL_EVENTS = Object.freeze([ ]); export const eventWithModuleNameReg = /^([^\d][\w]+)((?::[^\d][\w]+)+)$/; +export const moduleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*$/; +export const actionWithModuleNameReg = /^[a-zA-Z][a-zA-Z0-9_]*:[a-zA-Z][a-zA-Z0-9]*$/; +export const CONTROLLER_IDENTIFIER = 'app'; +export const SOCKET_TIMEOUT_TIME = 2000; diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 49fe5fb7ba4..9151703ddb5 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -14,7 +14,7 @@ import { strict as assert } from 'assert'; -import { eventWithModuleNameReg } from './channels/base/constants'; +import { eventWithModuleNameReg } from './constants'; export interface EventInfoObject { readonly module: string; diff --git a/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js b/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js index 0d9e27325f1..ef9fb14e685 100644 --- a/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js +++ b/framework/test/jest/unit/specs/controller/channels/base/constants.spec.js @@ -16,7 +16,7 @@ const { INTERNAL_EVENTS, -} = require('../../../../../../../src/controller/channels/base/constants'); +} = require('../../../../../../../src/controller/constants'); describe('base/constants.js', () => { it('INTERNAL_EVENTS must match to the snapshot.', () => { diff --git a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js index c43cc296322..80681df746e 100644 --- a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js +++ b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js @@ -20,7 +20,7 @@ jest.mock('../../../../../../src/controller/event'); const BaseChannel = require('../../../../../../src/controller/channels/base_channel'); const { INTERNAL_EVENTS, -} = require('../../../../../../src/controller/channels/base/constants'); +} = require('../../../../../../src/controller/constants'); const Action = require('../../../../../../src/controller/action'); const Event = require('../../../../../../src/controller/event'); From dfebd470a3ae049014e220a2cae6a06064d1a4ff Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Mon, 4 May 2020 17:44:29 +0200 Subject: [PATCH 22/35] Fix eslint dependencies --- framework/src/application/application.js | 2 ++ framework/src/controller/channels/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/framework/src/application/application.js b/framework/src/application/application.js index 4119e29dfee..713f9895fb3 100644 --- a/framework/src/application/application.js +++ b/framework/src/application/application.js @@ -443,9 +443,11 @@ class Application { handler: action => this._network.broadcast(action.params), }, requestFromNetwork: { + // eslint-disable-next-line @typescript-eslint/require-await handler: async action => this._network.request(action.params), }, requestFromPeer: { + // eslint-disable-next-line @typescript-eslint/require-await handler: async action => this._network.requestFromPeer(action.params), }, getConnectedPeers: { diff --git a/framework/src/controller/channels/index.ts b/framework/src/controller/channels/index.ts index 0331ca24f24..698847869fa 100644 --- a/framework/src/controller/channels/index.ts +++ b/framework/src/controller/channels/index.ts @@ -13,5 +13,7 @@ */ export { BaseChannel } from './base_channel'; +// TODO: Fix cycle dependencies +// eslint-disable-next-line import/no-cycle export { InMemoryChannel } from './in_memory_channel'; export { ChildProcessChannel } from './child_process_channel'; From a1d095fe2cfadf5b3e44cc255af27d4c46566995 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 6 May 2020 09:56:11 +0200 Subject: [PATCH 23/35] Update the storage types interface --- framework/src/types.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/framework/src/types.ts b/framework/src/types.ts index d9007dd9007..f85da73f9f1 100644 --- a/framework/src/types.ts +++ b/framework/src/types.ts @@ -35,7 +35,10 @@ export interface Channel { procedure: string, params?: object, ) => Promise; - readonly publishToNetwork: (eventName: string, data?: object) => Promise; + readonly publishToNetwork: ( + eventName: string, + data?: object, + ) => Promise; } export interface Logger { @@ -53,21 +56,11 @@ export interface Storage { readonly entities: { readonly NetworkInfo: KeyValEntity; readonly ForgerInfo: KeyValEntity; + readonly Migration: MigrationEntity; }; } export interface KeyValEntity { - readonly NetworkInfo: NetworkInfoEntity; - readonly Migration: MigrationEntity; -} - -export interface MigrationEntity { - readonly applyAll: (migrations: { - readonly [key: string]: ReadonlyArray; - }) => Promise; -} - -export interface NetworkInfoEntity { readonly getKey: ( key: string, tx?: StorageTransaction, @@ -79,6 +72,12 @@ export interface NetworkInfoEntity { ) => Promise; } +export interface MigrationEntity { + readonly applyAll: (migrations: { + readonly [key: string]: ReadonlyArray; + }) => Promise; +} + export interface StorageTransaction { // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly batch: (input: any[]) => Promise; From b3946570dd81e66d52fa9483601a8d125666ca50 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 6 May 2020 11:25:41 +0200 Subject: [PATCH 24/35] Update jest configuration to run from IDEs --- framework/test/jest/config/jest.config.base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/test/jest/config/jest.config.base.js b/framework/test/jest/config/jest.config.base.js index b88387f74fa..1d5f8461e92 100644 --- a/framework/test/jest/config/jest.config.base.js +++ b/framework/test/jest/config/jest.config.base.js @@ -14,7 +14,7 @@ module.exports = { globals: { 'ts-jest': { - tsConfig: './test/tsconfig.json', + tsConfig: '/test/tsconfig.json', }, }, verbose: true, From 89a56b55405beb8202d166f10a83e09795745194 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 6 May 2020 17:25:00 +0200 Subject: [PATCH 25/35] Fix jest unit tests for framework --- framework/src/application/application.js | 2 +- framework/src/controller/bus.ts | 9 +- .../src/controller/channels/base_channel.ts | 8 +- .../channels/child_process_channel.ts | 2 +- .../controller/channels/in_memory_channel.ts | 10 +- framework/src/controller/controller.ts | 70 ++-- framework/src/modules/base_module.ts | 5 +- framework/src/modules/http_api/index.js | 2 +- .../test/jest/config/jest.config.base.js | 2 +- .../block_synchronization_mechanism.spec.ts | 2 +- .../node/transport/transport_private.spec.ts | 1 + .../action/{action.spec.js => action.spec.ts} | 27 +- .../unit/specs/controller/action/constants.js | 28 -- .../constants.js => action/constants.ts} | 19 +- .../controller/{bus.spec.js => bus.spec.ts} | 94 ++--- ...e_channel.spec.js => base_channel.spec.ts} | 108 +++--- ....spec.js => child_process_channel.spec.ts} | 148 ++++---- ...nnel.spec.js => in_memory_channel.spec.ts} | 78 ++--- .../unit/specs/controller/controller.spec.js | 320 ------------------ .../unit/specs/controller/controller.spec.ts | 313 +++++++++++++++++ .../index.spec.js => event/constants.ts} | 12 +- .../event/{event.spec.js => event.spec.ts} | 16 +- .../jest.config.unit.spec.js.snap | 2 +- framework/types/pm2-axon/index.d.ts | 1 + 24 files changed, 558 insertions(+), 721 deletions(-) rename framework/test/jest/unit/specs/controller/action/{action.spec.js => action.spec.ts} (86%) delete mode 100644 framework/test/jest/unit/specs/controller/action/constants.js rename framework/test/jest/unit/specs/controller/{event/constants.js => action/constants.ts} (60%) rename framework/test/jest/unit/specs/controller/{bus.spec.js => bus.spec.ts} (80%) rename framework/test/jest/unit/specs/controller/channels/{base_channel.spec.js => base_channel.spec.ts} (63%) rename framework/test/jest/unit/specs/controller/channels/{child_process_channel.spec.js => child_process_channel.spec.ts} (70%) rename framework/test/jest/unit/specs/controller/channels/{in_memory_channel.spec.js => in_memory_channel.spec.ts} (67%) delete mode 100644 framework/test/jest/unit/specs/controller/controller.spec.js create mode 100644 framework/test/jest/unit/specs/controller/controller.spec.ts rename framework/test/jest/unit/specs/controller/{channels/child_process/index.spec.js => event/constants.ts} (65%) rename framework/test/jest/unit/specs/controller/event/{event.spec.js => event.spec.ts} (91%) diff --git a/framework/src/application/application.js b/framework/src/application/application.js index 713f9895fb3..5725d38a4f3 100644 --- a/framework/src/application/application.js +++ b/framework/src/application/application.js @@ -27,7 +27,7 @@ const { const { getNetworkIdentifier } = require('@liskhq/lisk-cryptography'); const { validator: liskValidator } = require('@liskhq/lisk-validator'); const _ = require('lodash'); -const Controller = require('../controller/controller'); +const { Controller } = require('../controller/controller'); const version = require('../version'); const validator = require('./validator'); const configurator = require('./default_configurator'); diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 619a82310f9..b85cfd9e410 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -128,23 +128,24 @@ export class Bus extends EventEmitter2 { // eslint-disable-next-line @typescript-eslint/require-await public async registerChannel( moduleAlias: string, + // Events should also include the module alias events: EventsArray, actions: ActionsObject, options: RegisterChannelOptions, ): Promise { events.forEach(eventName => { - if (this.events[eventName]) { + if (this.events[`${moduleAlias}:${eventName}`] !== undefined) { throw new Error(`Event "${eventName}" already registered with bus.`); } - this.events[eventName] = true; + this.events[`${moduleAlias}:${eventName}`] = true; }); Object.keys(actions).forEach(actionName => { - if (this.actions[actionName] === undefined) { + if (this.actions[`${moduleAlias}:${actionName}`] !== undefined) { throw new Error(`Action "${actionName}" already registered with bus.`); } - this.actions[actionName] = actions[actionName]; + this.actions[`${moduleAlias}:${actionName}`] = actions[actionName]; }); let { channel } = options; diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 085b70044e4..e7dcee8c3de 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -12,7 +12,7 @@ * Removal or modification of this copyright notice is prohibited. */ -import { Event, EventCallback } from '../event'; +import { EventCallback } from '../event'; import { Action, ActionsDefinition, ActionsObject } from '../action'; import { INTERNAL_EVENTS, eventWithModuleNameReg } from '../constants'; @@ -41,9 +41,7 @@ export abstract class BaseChannel { ? events : [...events, ...INTERNAL_EVENTS]; - this.eventsList = eventList.map(eventName => - new Event(`${this.moduleAlias}:${eventName}`).key(), - ); + this.eventsList = eventList; this.actions = {}; for (const actionName of Object.keys(actions)) { @@ -61,7 +59,7 @@ export abstract class BaseChannel { isPublic, handler, ); - this.actions[action.key()] = action; + this.actions[actionName] = action; } this.actionsList = Object.keys(this.actions); } diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index b52d37fa073..90c21d60488 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -163,7 +163,7 @@ export class ChildProcessChannel extends BaseChannel { // eslint-disable-next-line // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.actions[action.name].handler(action); + return this.actions[action.name].handler(action.serialize()); } return new Promise((resolve, reject) => { diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index ba0887450c2..0a0217d34a3 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -59,19 +59,19 @@ export class InMemoryChannel extends BaseChannel { public async invoke(actionName: string, params?: object): Promise { const action = new Action(actionName, params, this.moduleAlias); + console.info(this.actions); + if (action.module === this.moduleAlias) { - if (this.actions[action.key()] === undefined) { + if (this.actions[action.name] === undefined) { throw new Error( - `The action '${action.key()}' on module '${ - this.moduleAlias - }' does not exist.`, + `The action '${action.name}' on module '${this.moduleAlias}' does not exist.`, ); } // eslint-disable-next-line // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.actions[action.key()].handler(action.serialize()); + return this.actions[action.name].handler(action.serialize()); } return (this.bus as Bus).invoke(action.serialize()); diff --git a/framework/src/controller/controller.ts b/framework/src/controller/controller.ts index c49a68b8055..00a62006c45 100644 --- a/framework/src/controller/controller.ts +++ b/framework/src/controller/controller.ts @@ -73,7 +73,7 @@ interface Migrations { readonly [key: string]: ReadonlyArray; } -class Controller { +export class Controller { public readonly logger: Logger; public readonly storage: Storage; public readonly appLabel: string; @@ -131,6 +131,35 @@ class Controller { this.logger.debug(this.bus?.getActions(), 'Bus ready for actions'); } + public async unloadModules( + modules = Object.keys(this.modules), + ): Promise { + // To perform operations in sequence and not using bluebird + + for (const alias of modules) { + await this.modules[alias].unload(); + delete this.modules[alias]; + } + } + + public async cleanup(_code?: number, reason?: string): Promise { + this.logger.info('Cleanup controller...'); + + if (reason) { + this.logger.error(`Reason: ${reason}`); + } + + this.childrenList.forEach(child => child.kill()); + + try { + await this.bus?.cleanup(); + await this.unloadModules(); + this.logger.info('Unload completed'); + } catch (err) { + this.logger.error({ err }, 'Caused error during modules cleanup'); + } + } + // eslint-disable-next-line class-methods-use-this private async _setupDirectories(): Promise { // Make sure all directories exists @@ -139,7 +168,7 @@ class Controller { await fs.ensureDir(this.config.dirs.pids); } - private async _validatePidFile() { + private async _validatePidFile(): Promise { const pidPath = `${this.config.dirs.pids}/controller.pid`; const pidExists = await fs.pathExists(pidPath); if (pidExists) { @@ -160,7 +189,7 @@ class Controller { await fs.writeFile(pidPath, process.pid); } - private async _setupBus() { + private async _setupBus(): Promise { this.bus = new Bus( { wildcard: true, @@ -176,7 +205,7 @@ class Controller { await this.channel.registerToBus(this.bus); // If log level is greater than info - if (this.logger.level && this.logger.level() < 30) { + if (this.logger.level !== undefined && this.logger.level() < 30) { this.bus.onAny(event => { this.logger.trace(`eventName: ${event},`, 'Monitor Bus Channel'); }); @@ -184,7 +213,7 @@ class Controller { } // eslint-disable-next-line @typescript-eslint/require-await - private async _loadMigrations(migrationsObj: Migrations) { + private async _loadMigrations(migrationsObj: Migrations): Promise { return this.storage.entities.Migration.applyAll(migrationsObj); } @@ -329,35 +358,4 @@ class Controller { }), ]); } - - public async unloadModules( - modules = Object.keys(this.modules), - ): Promise { - // To perform operations in sequence and not using bluebird - - for (const alias of modules) { - await this.modules[alias].unload(); - delete this.modules[alias]; - } - } - - public async cleanup(_code: number, reason: string): Promise { - this.logger.info('Cleanup controller...'); - - if (reason) { - this.logger.error(`Reason: ${reason}`); - } - - this.childrenList.forEach(child => child.kill()); - - try { - await this.bus?.cleanup(); - await this.unloadModules(); - this.logger.info('Unload completed'); - } catch (err) { - this.logger.error({ err }, 'Caused error during modules cleanup'); - } - } } - -module.exports = Controller; diff --git a/framework/src/modules/base_module.ts b/framework/src/modules/base_module.ts index 48739d144df..cae4de0195e 100644 --- a/framework/src/modules/base_module.ts +++ b/framework/src/modules/base_module.ts @@ -44,7 +44,10 @@ export abstract class BaseModule { return []; } - public abstract get defaults(): object; + // eslint-disable-next-line class-methods-use-this + public get defaults(): object { + return {}; + } public abstract get events(): EventsArray; public abstract get actions(): ActionsDefinition; public abstract async load(channel: BaseChannel): Promise; diff --git a/framework/src/modules/http_api/index.js b/framework/src/modules/http_api/index.js index cf0f967b3d3..3765d9db112 100644 --- a/framework/src/modules/http_api/index.js +++ b/framework/src/modules/http_api/index.js @@ -15,7 +15,7 @@ 'use strict'; const HttpApi = require('./http_api'); -const BaseModule = require('../base_module'); +const { BaseModule } = require('../base_module'); const { config: defaultConfig } = require('./defaults'); /* eslint-disable class-methods-use-this */ diff --git a/framework/test/jest/config/jest.config.base.js b/framework/test/jest/config/jest.config.base.js index 1d5f8461e92..baa428bb295 100644 --- a/framework/test/jest/config/jest.config.base.js +++ b/framework/test/jest/config/jest.config.base.js @@ -18,7 +18,7 @@ module.exports = { }, }, verbose: true, - collectCoverage: true, + collectCoverage: false, coverageReporters: ['json', 'lcov', 'cobertura'], rootDir: '../../../', setupFilesAfterEnv: ['/test/jest/config/setup.js'], diff --git a/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts b/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts index 835f552acd3..6271bb55ef8 100644 --- a/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts +++ b/framework/test/jest/unit/specs/application/node/synchronizer/block_synchronization_mechanism/block_synchronization_mechanism.spec.ts @@ -31,7 +31,7 @@ import { registeredTransactions } from '../../../../../../../utils/registered_tr import * as genesisBlockDevnet from '../../../../../../../fixtures/config/devnet/genesis_block.json'; import { peersList } from './peers'; -const ChannelMock: any = jest.genMockFromModule( +const { InMemoryChannel: ChannelMock } = jest.genMockFromModule( '../../../../../../../../src/controller/channels/in_memory_channel', ); diff --git a/framework/test/jest/unit/specs/application/node/transport/transport_private.spec.ts b/framework/test/jest/unit/specs/application/node/transport/transport_private.spec.ts index 58a0477420e..f6df01c56c3 100644 --- a/framework/test/jest/unit/specs/application/node/transport/transport_private.spec.ts +++ b/framework/test/jest/unit/specs/application/node/transport/transport_private.spec.ts @@ -91,6 +91,7 @@ describe('transport', () => { trace: jest.fn(), warn: jest.fn(), fatal: jest.fn(), + level: jest.fn(), }; channelStub = { diff --git a/framework/test/jest/unit/specs/controller/action/action.spec.js b/framework/test/jest/unit/specs/controller/action/action.spec.ts similarity index 86% rename from framework/test/jest/unit/specs/controller/action/action.spec.js rename to framework/test/jest/unit/specs/controller/action/action.spec.ts index 44dd4becbc5..3f53719c481 100644 --- a/framework/test/jest/unit/specs/controller/action/action.spec.js +++ b/framework/test/jest/unit/specs/controller/action/action.spec.ts @@ -12,10 +12,8 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const Action = require('../../../../../../src/controller/action'); -const { +import { Action } from '../../../../../../src/controller/action'; +import { ACTION_NAME, MODULE_NAME, INVALID_ACTION_NAME_ARG, @@ -23,16 +21,10 @@ const { VALID_ACTION_NAME_ARG, VALID_ACTION_SOURCE_ARG, PARAMS, -} = require('./constants'); +} from './constants'; describe('Action class', () => { describe('#constructor', () => { - // Act & Assert - it('should throw an error when name argument was not provided.', () => { - expect(() => new Action()).toThrow( - 'Action name "undefined" must be a valid name with module name.', - ); - }); it('should throw an error when invalid name was provided.', () => { // Act & Assert expect(() => new Action(INVALID_ACTION_NAME_ARG)).toThrow( @@ -43,8 +35,7 @@ describe('Action class', () => { it('should throw an error when invalid source was provided.', () => { // Act & Assert expect( - () => - new Action(VALID_ACTION_NAME_ARG, null, INVALID_ACTION_SOURCE_ARG), + () => new Action(VALID_ACTION_NAME_ARG, {}, INVALID_ACTION_SOURCE_ARG), ).toThrow( `Source name "${INVALID_ACTION_SOURCE_ARG}" must be a valid module name.`, ); @@ -75,7 +66,7 @@ describe('Action class', () => { }); describe('methods', () => { - let action; + let action: Action; beforeEach(() => { // Arrange action = new Action( @@ -146,10 +137,10 @@ describe('Action class', () => { // Assert expect(action).toBeInstanceOf(Action); - expect(action.module).toBe(MODULE_NAME); - expect(action.name).toBe(ACTION_NAME); - expect(action.params).toBe(PARAMS); - expect(action.source).toBe(VALID_ACTION_SOURCE_ARG); + expect(action.module).toEqual(MODULE_NAME); + expect(action.name).toEqual(ACTION_NAME); + expect(action.params).toEqual(PARAMS); + expect(action.source).toEqual(VALID_ACTION_SOURCE_ARG); }); it('should return action instance with given object config.', () => { diff --git a/framework/test/jest/unit/specs/controller/action/constants.js b/framework/test/jest/unit/specs/controller/action/constants.js deleted file mode 100644 index 63eb4d28b1b..00000000000 --- a/framework/test/jest/unit/specs/controller/action/constants.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -const MODULE_NAME = 'module'; -const ACTION_NAME = 'action'; - -module.exports = Object.freeze({ - ACTION_NAME, - MODULE_NAME, - INVALID_ACTION_NAME_ARG: '09', - INVALID_ACTION_SOURCE_ARG: '123', - VALID_ACTION_NAME_ARG: `${MODULE_NAME}:${ACTION_NAME}`, - VALID_ACTION_SOURCE_ARG: 'source', - PARAMS: '#params', -}); diff --git a/framework/test/jest/unit/specs/controller/event/constants.js b/framework/test/jest/unit/specs/controller/action/constants.ts similarity index 60% rename from framework/test/jest/unit/specs/controller/event/constants.js rename to framework/test/jest/unit/specs/controller/action/constants.ts index a4361d69594..d95dd548b05 100644 --- a/framework/test/jest/unit/specs/controller/event/constants.js +++ b/framework/test/jest/unit/specs/controller/action/constants.ts @@ -12,15 +12,10 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -const MODULE_NAME = 'module'; -const EVENT_NAME = 'event'; - -module.exports = Object.freeze({ - MODULE_NAME, - EVENT_NAME, - VALID_EVENT_NAME_ARG: `${MODULE_NAME}:${EVENT_NAME}`, - INVALID_EVENT_NAME_ARG: `${MODULE_NAME}`, - DATA: '#data', -}); +export const MODULE_NAME = 'module'; +export const ACTION_NAME = 'action'; +export const INVALID_ACTION_NAME_ARG = '09'; +export const INVALID_ACTION_SOURCE_ARG = '123'; +export const VALID_ACTION_NAME_ARG = `${MODULE_NAME}:${ACTION_NAME}`; +export const VALID_ACTION_SOURCE_ARG = 'source'; +export const PARAMS = {}; diff --git a/framework/test/jest/unit/specs/controller/bus.spec.js b/framework/test/jest/unit/specs/controller/bus.spec.ts similarity index 80% rename from framework/test/jest/unit/specs/controller/bus.spec.js rename to framework/test/jest/unit/specs/controller/bus.spec.ts index 99d61953b6a..e6190cd76d9 100644 --- a/framework/test/jest/unit/specs/controller/bus.spec.js +++ b/framework/test/jest/unit/specs/controller/bus.spec.ts @@ -12,41 +12,37 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - jest.mock('eventemitter2'); jest.mock('pm2-axon'); jest.mock('pm2-axon-rpc'); -const { EventEmitter2 } = require('eventemitter2'); - -const Bus = require('../../../../../src/controller/bus'); +// eslint-disable-next-line import/first +import { EventEmitter2 } from 'eventemitter2'; +// eslint-disable-next-line import/first +import { Bus } from '../../../../../src/controller/bus'; +// eslint-disable-next-line import/first +import { Action, ActionInfoObject } from '../../../../../src/controller/action'; describe('Bus', () => { const options = {}; - const config = { + const config: any = { ipc: { enabled: false, }, }; + + const channelMock: any = {}; + const channelOptions = { type: 'inMemory', - channel: { - alias: { - action1: { - isPublic: true, - }, - action2: { - isPublic: true, - }, - }, - }, + channel: channelMock, }; - const logger = { + const logger: any = { info: jest.fn(), }; - let bus = null; + let bus: Bus; + beforeEach(() => { bus = new Bus(options, logger, config); }); @@ -72,10 +68,11 @@ describe('Bus', () => { const events = ['event1', 'event2']; // Act - await bus.registerChannel(moduleAlias, events, [], channelOptions); + await bus.registerChannel(moduleAlias, events, {}, channelOptions); // Assert expect(Object.keys(bus.events)).toHaveLength(2); + console.info(bus.events); events.forEach(eventName => { expect(bus.events[`${moduleAlias}:${eventName}`]).toBe(true); }); @@ -88,22 +85,16 @@ describe('Bus', () => { // Act && Assert await expect( - bus.registerChannel(moduleAlias, events, [], channelOptions), + bus.registerChannel(moduleAlias, events, {}, channelOptions), ).rejects.toThrow(Error); }); it('should register actions.', async () => { // Arrange const moduleAlias = 'alias'; - const actions = { - action1: { - isPublic: false, - handler: jest.fn(), - }, - action2: { - isPublic: false, - handler: jest.fn(), - }, + const actions: any = { + action1: new Action('alias:action1', {}, '', false, jest.fn()), + action2: new Action('alias:action2', {}, '', false, jest.fn()), }; // Act @@ -122,10 +113,7 @@ describe('Bus', () => { // Arrange const moduleAlias = 'alias'; const actions = { - action1: { - isPublic: false, - handler: jest.fn(), - }, + action1: new Action('alias:action1', {}, '', false, jest.fn()), }; // Act && Assert @@ -142,11 +130,11 @@ describe('Bus', () => { it('should throw error if action was not registered', async () => { // Arrange - const actionData = { + const actionData: ActionInfoObject = { name: 'nonExistentAction', module: 'app', source: 'chain', - params: 'logger', + params: {}, }; // Act && Assert @@ -157,11 +145,11 @@ describe('Bus', () => { it('should throw error if module does not exist', async () => { // Arrange - const actionData = { + const actionData: ActionInfoObject = { name: 'getComponentConfig', module: 'invalidModule', source: 'chain', - params: 'logger', + params: {}, }; // Act && Assert @@ -174,11 +162,11 @@ describe('Bus', () => { describe('#invokePublic', () => { it('should throw error if action was not registered', async () => { // Arrange - const actionData = { + const actionData: ActionInfoObject = { name: 'nonExistentAction', module: 'app', source: 'chain', - params: 'logger', + params: {}, }; // Act && Assert @@ -189,11 +177,11 @@ describe('Bus', () => { it('should throw error if module does not exist', async () => { // Arrange - const actionData = { + const actionData: ActionInfoObject = { name: 'getComponentConfig', module: 'invalidModule', source: 'chain', - params: 'logger', + params: {}, }; // Act && Assert @@ -206,15 +194,13 @@ describe('Bus', () => { // Arrange const moduleAlias = 'alias'; const actions = { - action1: { - handler: jest.fn(), - }, + action1: new Action('alias:action1', {}, '', false, jest.fn()), }; - const actionData = { + const actionData: ActionInfoObject = { name: 'action1', module: moduleAlias, source: 'chain', - params: 'logger', + params: {}, }; // Act @@ -235,7 +221,7 @@ describe('Bus', () => { const eventName = `${moduleAlias}:${events[0]}`; const eventData = '#DATA'; - await bus.registerChannel(moduleAlias, events, [], channelOptions); + await bus.registerChannel(moduleAlias, events, {}, channelOptions); // Act bus.publish(eventName, eventData); @@ -252,15 +238,9 @@ describe('Bus', () => { it('should return the registered actions', async () => { // Arrange const moduleAlias = 'alias'; - const actions = { - action1: { - public: false, - handler: jest.fn(), - }, - action2: { - public: false, - handler: jest.fn(), - }, + const actions: any = { + action1: new Action('alias:action1', {}, '', false, jest.fn()), + action2: new Action('alias:action2', {}, '', false, jest.fn()), }; const expectedActions = Object.keys(actions).map( actionName => `${moduleAlias}:${actionName}`, @@ -283,7 +263,7 @@ describe('Bus', () => { const events = ['event1', 'event2']; const expectedEvents = events.map(event => `${moduleAlias}:${event}`); - await bus.registerChannel(moduleAlias, events, [], channelOptions); + await bus.registerChannel(moduleAlias, events, {}, channelOptions); // Act const registeredEvent = bus.getEvents(); diff --git a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.ts similarity index 63% rename from framework/test/jest/unit/specs/controller/channels/base_channel.spec.js rename to framework/test/jest/unit/specs/controller/channels/base_channel.spec.ts index 80681df746e..9567c9fceb9 100644 --- a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.js +++ b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.ts @@ -12,35 +12,37 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - jest.mock('../../../../../../src/controller/action'); -jest.mock('../../../../../../src/controller/event'); -const BaseChannel = require('../../../../../../src/controller/channels/base_channel'); -const { - INTERNAL_EVENTS, -} = require('../../../../../../src/controller/constants'); -const Action = require('../../../../../../src/controller/action'); -const Event = require('../../../../../../src/controller/event'); +// eslint-disable-next-line import/first +import { BaseChannel } from '../../../../../../src/controller/channels'; +// eslint-disable-next-line import/first +import { INTERNAL_EVENTS } from '../../../../../../src/controller/constants'; +// eslint-disable-next-line import/first +import { Action } from '../../../../../../src/controller/action'; + +// eslint-disable-next-line +// @ts-ignore +class MyChannel extends BaseChannel {} describe('Base Channel', () => { // Arrange + const actionHandler = jest.fn(); const params = { moduleAlias: 'alias', events: ['event1', 'event2'], actions: { - action1: jest.fn(), - action2: jest.fn(), - action3: jest.fn(), + action1: actionHandler, + action2: actionHandler, + action3: actionHandler, }, options: {}, }; - let baseChannel = null; + let baseChannel: BaseChannel; beforeEach(() => { // Act - baseChannel = new BaseChannel( + baseChannel = new MyChannel( params.moduleAlias, params.events, params.actions, @@ -54,28 +56,38 @@ describe('Base Channel', () => { expect(baseChannel.moduleAlias).toBe(params.moduleAlias); expect(baseChannel.options).toBe(params.options); - params.events.forEach(event => { - expect(Event).toHaveBeenCalledWith(`${params.moduleAlias}:${event}`); - }); - Object.keys(params.actions).forEach(action => { - expect(Action).toHaveBeenCalledWith(`${params.moduleAlias}:${action}`); + expect(Action).toHaveBeenCalledWith( + `${params.moduleAlias}:${action}`, + undefined, + undefined, + true, + actionHandler, + ); }); }); }); describe('getters', () => { - it('base.actionList should contain list of Action Objects', () => { + it('base.actions should contain list of Action Objects', () => { + // Assert + expect(Object.keys(baseChannel.actions)).toHaveLength(3); + Object.keys(baseChannel.actions).forEach(action => { + expect(baseChannel.actions[action]).toBeInstanceOf(Action); + }); + }); + + it('base.actionList should contain list of actions', () => { // Assert expect(baseChannel.actionsList).toHaveLength(3); baseChannel.actionsList.forEach(action => { - expect(action).toBeInstanceOf(Action); + expect(typeof action).toBe('string'); }); }); - it('base.eventsList should contain list of Event Objects with internal events', () => { + it('base.eventsList be list of events', () => { // Arrange & Act - baseChannel = new BaseChannel( + baseChannel = new MyChannel( params.moduleAlias, params.events, params.actions, @@ -86,13 +98,13 @@ describe('Base Channel', () => { params.events.length + INTERNAL_EVENTS.length, ); baseChannel.eventsList.forEach(event => { - expect(event).toBeInstanceOf(Event); + expect(typeof event).toBe('string'); }); }); it('base.eventsList should contain internal events when skipInternalEvents option was set to FALSE', () => { // Arrange & Act - baseChannel = new BaseChannel( + baseChannel = new MyChannel( params.moduleAlias, params.events, params.actions, @@ -109,7 +121,7 @@ describe('Base Channel', () => { it('base.eventsList should NOT contain internal events when skipInternalEvents option was set TRUE', () => { // Arrange & Act - baseChannel = new BaseChannel( + baseChannel = new MyChannel( params.moduleAlias, params.events, params.actions, @@ -121,50 +133,6 @@ describe('Base Channel', () => { // Assert expect(baseChannel.eventsList).toHaveLength(params.events.length); }); - - it('base.actions should return given actions object as it is', () => { - // Assert - expect(baseChannel.actions).toEqual(params.actions); - }); - }); - - describe('#registerToBus', () => { - it('should throw TypeError', () => { - // Assert - return expect(baseChannel.registerToBus()).rejects.toBeInstanceOf( - TypeError, - ); - }); - }); - - describe('#subscribe', () => { - it('should throw TypeError', () => { - // Assert - expect(baseChannel.subscribe).toThrow(TypeError); - }); - }); - - describe('#publish', () => { - it('should throw TypeError', () => { - // Assert - expect(baseChannel.publish).toThrow(TypeError); - }); - }); - - describe('#invoke', () => { - it('should throw TypeError', () => { - // Assert - return expect(baseChannel.invoke()).rejects.toBeInstanceOf(TypeError); - }); - }); - - describe('#invokePublic', () => { - it('should throw TypeError', () => { - // Assert - return expect(baseChannel.invokePublic()).rejects.toBeInstanceOf( - TypeError, - ); - }); }); describe('#isValidEventName', () => { diff --git a/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.js b/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.ts similarity index 70% rename from framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.js rename to framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.ts index 295d920d4ee..704899c3835 100644 --- a/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.js +++ b/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.ts @@ -12,8 +12,6 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - jest.mock('eventemitter2'); jest.mock('pm2-axon-rpc', () => ({ Client: jest.fn(() => ({ @@ -24,13 +22,18 @@ jest.mock('pm2-axon-rpc', () => ({ })), })); jest.mock('pm2-axon'); -jest.mock('../../../../../../src/controller/channels/child_process'); -const EventEmitter2 = require('eventemitter2'); -const ChildProcessChannel = require('../../../../../../src/controller/channels/child_process_channel'); -const BaseChannel = require('../../../../../../src/controller/channels/base_channel'); -const Event = require('../../../../../../src/controller/event'); -const Action = require('../../../../../../src/controller/action'); +// eslint-disable-next-line import/first +import { EventEmitter2 } from 'eventemitter2'; +// eslint-disable-next-line import/first +import { + ChildProcessChannel, + BaseChannel, +} from '../../../../../../src/controller/channels'; +// eslint-disable-next-line import/first +import { Event } from '../../../../../../src/controller/event'; +// eslint-disable-next-line import/first +import { Action } from '../../../../../../src/controller/action'; describe('ChildProcessChannel Channel', () => { // Arrange @@ -60,8 +63,8 @@ describe('ChildProcessChannel Channel', () => { rpc: 'rpc', }; - let childProcessChannel; - let spies; + let childProcessChannel: ChildProcessChannel; + let spies: any; beforeEach(() => { childProcessChannel = new ChildProcessChannel( @@ -78,42 +81,14 @@ describe('ChildProcessChannel Channel', () => { }; }); + afterEach(async () => { + childProcessChannel.cleanup(); + }); + describe('inheritance', () => { it('should be extended from BaseChannel class', () => { // Assert - expect(Object.getPrototypeOf(childProcessChannel)).toBeInstanceOf( - BaseChannel, - ); - }); - - it('should call BaseChannel class constructor with arguments', () => { - // Arrange - let IsolatedChildProcessChannel = null; - let IsolatedBaseChannel = null; - - jest.isolateModules(() => { - jest.doMock('../../../../../../src/controller/channels/base_channel'); - // eslint-disable-next-line global-require - IsolatedChildProcessChannel = require('../../../../../../src/controller/channels/child_process_channel'); - // eslint-disable-next-line global-require - IsolatedBaseChannel = require('../../../../../../src/controller/channels/base_channel'); - }); - - // Act - childProcessChannel = new IsolatedChildProcessChannel( - params.moduleAlias, - params.events, - params.actions, - params.options, - ); - - // Assert - expect(IsolatedBaseChannel).toHaveBeenCalledWith( - params.moduleAlias, - params.events, - params.actions, - params.options, - ); + expect(ChildProcessChannel.prototype).toBeInstanceOf(BaseChannel); }); }); @@ -125,32 +100,32 @@ describe('ChildProcessChannel Channel', () => { }); describe('#registerToBus', () => { - beforeEach(() => childProcessChannel.registerToBus(socketsPath)); + beforeEach(async () => childProcessChannel.registerToBus(socketsPath)); it('should connect pubSocket', async () => { // Assert - expect(childProcessChannel.pubSocket.connect).toHaveBeenCalledWith( + expect(childProcessChannel.pubSocket?.connect).toHaveBeenCalledWith( socketsPath.sub, ); }); it('should connect subSocket', () => { // Assert - expect(childProcessChannel.subSocket.connect).toHaveBeenCalledWith( + expect(childProcessChannel.subSocket?.connect).toHaveBeenCalledWith( socketsPath.pub, ); }); it('should connect busRpcSocket', () => { // Assert - expect(childProcessChannel.busRpcSocket.connect).toHaveBeenCalledWith( + expect(childProcessChannel.busRpcSocket?.connect).toHaveBeenCalledWith( socketsPath.rpc, ); }); it('should expose "invoke" event on rpcServer and call this.invoke with action', () => { // Assert - expect(childProcessChannel.rpcServer.expose).toHaveBeenCalledWith( + expect(childProcessChannel.rpcServer?.expose).toHaveBeenCalledWith( 'invoke', expect.any(Function), ); @@ -158,7 +133,7 @@ describe('ChildProcessChannel Channel', () => { it('should bind the rpcSocket to rpcSocketPath', () => { // Assert - expect(childProcessChannel.rpcSocket.bind).toHaveBeenCalledWith( + expect(childProcessChannel.rpcSocket?.bind).toHaveBeenCalledWith( childProcessChannel.rpcSocketPath, ); }); @@ -193,7 +168,7 @@ describe('ChildProcessChannel Channel', () => { childProcessChannel.subscribe(invalidEventName, () => {}); // Assert - expect(childProcessChannel.subSocket.on).toHaveBeenCalledWith( + expect(childProcessChannel.subSocket?.on).toHaveBeenCalledWith( invalidEventName, expect.any(Function), ); @@ -223,7 +198,7 @@ describe('ChildProcessChannel Channel', () => { childProcessChannel.once(invalidEventName, () => {}); // Assert - expect(childProcessChannel.subSocket.on).toHaveBeenCalledWith( + expect(childProcessChannel.subSocket?.on).toHaveBeenCalledWith( invalidEventName, expect.any(Function), ); @@ -249,7 +224,7 @@ describe('ChildProcessChannel Channel', () => { it('should call localBus.emit with proper arguments', async () => { // Arrange - const data = '#DATA'; + const data = { data: '#DATA' }; const event = new Event(validEventName, data); // Act @@ -264,14 +239,14 @@ describe('ChildProcessChannel Channel', () => { it('should call pubSocket.emit with proper arguments', async () => { // Arrange - const data = '#DATA'; + const data = { data: '#DATA' }; const event = new Event(validEventName, data); // Act childProcessChannel.publish(validEventName, data); // Assert - expect(childProcessChannel.pubSocket.emit).toHaveBeenCalledWith( + expect(childProcessChannel.pubSocket?.emit).toHaveBeenCalledWith( event.key(), event.serialize(), ); @@ -279,7 +254,7 @@ describe('ChildProcessChannel Channel', () => { it('should not call pubSocket.emit when eventList is empty', async () => { // Arrange - const data = '#DATA'; + const data = { data: '#DATA' }; const anotherChildProcessChannel = new ChildProcessChannel( params.moduleAlias, [], @@ -317,10 +292,13 @@ describe('ChildProcessChannel Channel', () => { // Act await childProcessChannel.registerToBus(socketsPath); const action = new Action(actionName, actionParams); - await childProcessChannel.invoke(action, actionParams); + await childProcessChannel.invoke(action.key(), actionParams); // Assert - expect(params.actions.action1.handler).toHaveBeenCalledWith(action); + expect(params.actions.action1.handler).toHaveBeenCalledWith({ + ...action.serialize(), + source: childProcessChannel.moduleAlias, + }); }); }); @@ -333,19 +311,19 @@ describe('ChildProcessChannel Channel', () => { childProcessChannel.cleanup(); // Assert - expect(childProcessChannel.rpcSocket.close).toHaveBeenCalled(); + expect(childProcessChannel.rpcSocket?.close).toHaveBeenCalled(); }); }); describe('#_resolveWhenAllSocketsBound', () => { beforeEach(async () => { await childProcessChannel.registerToBus(socketsPath); - childProcessChannel._resolveWhenAllSocketsBound(); + (childProcessChannel as any)._resolveWhenAllSocketsBound(); }); it('should call pubSocket.sock.once with proper arguments', () => { // Assert - expect(childProcessChannel.pubSocket.sock.once).toHaveBeenCalledWith( + expect(childProcessChannel.pubSocket?.sock.once).toHaveBeenCalledWith( 'connect', expect.any(Function), ); @@ -353,7 +331,7 @@ describe('ChildProcessChannel Channel', () => { it('should call subSocket.sock.once with proper arguments', () => { // Assert - expect(childProcessChannel.subSocket.sock.once).toHaveBeenCalledWith( + expect(childProcessChannel.subSocket?.sock.once).toHaveBeenCalledWith( 'connect', expect.any(Function), ); @@ -361,7 +339,7 @@ describe('ChildProcessChannel Channel', () => { it('should call rpcSocket.once with proper arguments', () => { // Assert - expect(childProcessChannel.rpcSocket.once).toHaveBeenCalledWith( + expect(childProcessChannel.rpcSocket?.once).toHaveBeenCalledWith( 'bind', expect.any(Function), ); @@ -369,7 +347,7 @@ describe('ChildProcessChannel Channel', () => { it('should call busRpcSocket.once with proper arguments', () => { // Assert - expect(childProcessChannel.busRpcSocket.once).toHaveBeenCalledWith( + expect(childProcessChannel.busRpcSocket?.once).toHaveBeenCalledWith( 'connect', expect.any(Function), ); @@ -377,11 +355,11 @@ describe('ChildProcessChannel Channel', () => { it('should call busRpcClient.call with proper arguments when busRpcSocket receives a "connect" event', () => { // Assert - expect(childProcessChannel.busRpcClient.call).toHaveBeenCalledWith( + expect(childProcessChannel.busRpcClient?.call).toHaveBeenCalledWith( 'registerChannel', childProcessChannel.moduleAlias, - childProcessChannel.eventsList.map(event => event.name), - childProcessChannel.actionsList.map(action => action.name), + childProcessChannel.eventsList, + childProcessChannel.actionsList, { type: 'ipcSocket', rpcSocketPath: childProcessChannel.rpcSocketPath }, expect.any(Function), ); @@ -394,17 +372,17 @@ describe('ChildProcessChannel Channel', () => { it('should reject if any of the sockets receive an "error" event', () => { // Assert return expect( - childProcessChannel._rejectWhenAnySocketFailsToBind(), + (childProcessChannel as any)._rejectWhenAnySocketFailsToBind(), ).rejects.toBe('#MOCKED_ONCE'); }); it('should call pubSocket.sock.once with proper arguments', async () => { // Act && Assert await expect( - childProcessChannel._rejectWhenAnySocketFailsToBind(), + (childProcessChannel as any)._rejectWhenAnySocketFailsToBind(), ).toReject(); - expect(childProcessChannel.pubSocket.sock.once).toHaveBeenCalledWith( + expect(childProcessChannel.pubSocket?.sock.once).toHaveBeenCalledWith( 'error', expect.any(Function), ); @@ -413,10 +391,10 @@ describe('ChildProcessChannel Channel', () => { it('should call subSocket.sock.once with proper arguments', async () => { // Act && Assert await expect( - childProcessChannel._rejectWhenAnySocketFailsToBind(), + (childProcessChannel as any)._rejectWhenAnySocketFailsToBind(), ).toReject(); - expect(childProcessChannel.subSocket.sock.once).toHaveBeenCalledWith( + expect(childProcessChannel.subSocket?.sock.once).toHaveBeenCalledWith( 'error', expect.any(Function), ); @@ -425,10 +403,10 @@ describe('ChildProcessChannel Channel', () => { it('should call rpcSocket.once with proper arguments', async () => { // Act && Assert await expect( - childProcessChannel._rejectWhenAnySocketFailsToBind(), + (childProcessChannel as any)._rejectWhenAnySocketFailsToBind(), ).toReject(); - expect(childProcessChannel.rpcSocket.sock.once).toHaveBeenCalledWith( + expect(childProcessChannel.rpcSocket?.sock.once).toHaveBeenCalledWith( 'error', expect.any(Function), ); @@ -440,9 +418,9 @@ describe('ChildProcessChannel Channel', () => { it('should reject with an Error object with proper message', () => { // Assert - return expect(childProcessChannel._rejectWhenTimeout(1)).rejects.toThrow( - 'ChildProcessChannel sockets setup timeout', - ); + return expect( + (childProcessChannel as any)._rejectWhenTimeout(1), + ).rejects.toThrow('ChildProcessChannel sockets setup timeout'); }); }); @@ -452,46 +430,46 @@ describe('ChildProcessChannel Channel', () => { await childProcessChannel.registerToBus(socketsPath); // Act - childProcessChannel._removeAllListeners(); + (childProcessChannel as any)._removeAllListeners(); }); it('should remove all listeners on subSocket ', () => { // Assert expect( - childProcessChannel.subSocket.sock.removeAllListeners, + childProcessChannel.subSocket?.sock.removeAllListeners, ).toHaveBeenCalledWith('connect'); expect( - childProcessChannel.subSocket.sock.removeAllListeners, + childProcessChannel.subSocket?.sock.removeAllListeners, ).toHaveBeenCalledWith('error'); }); it('should remove all listeners on pubSocket', () => { // Assert expect( - childProcessChannel.pubSocket.sock.removeAllListeners, + childProcessChannel.pubSocket?.sock.removeAllListeners, ).toHaveBeenCalledWith('connect'); expect( - childProcessChannel.pubSocket.sock.removeAllListeners, + childProcessChannel.pubSocket?.sock.removeAllListeners, ).toHaveBeenCalledWith('error'); }); it('should remove all listeners on busRpcSocket', () => { // Assert expect( - childProcessChannel.busRpcSocket.removeAllListeners, + childProcessChannel.busRpcSocket?.removeAllListeners, ).toHaveBeenCalledWith('connect'); expect( - childProcessChannel.busRpcSocket.removeAllListeners, + childProcessChannel.busRpcSocket?.removeAllListeners, ).toHaveBeenCalledWith('error'); }); it('should remove all listeners on rpcSocket', () => { // Assert expect( - childProcessChannel.rpcSocket.removeAllListeners, + childProcessChannel.rpcSocket?.removeAllListeners, ).toHaveBeenCalledWith('bind'); expect( - childProcessChannel.rpcSocket.removeAllListeners, + childProcessChannel.rpcSocket?.removeAllListeners, ).toHaveBeenCalledWith('error'); }); }); diff --git a/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.js b/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.ts similarity index 67% rename from framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.js rename to framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.ts index 4d7ed5fc3b6..6cb953100b9 100644 --- a/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.js +++ b/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.ts @@ -12,14 +12,16 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - jest.mock('../../../../../../src/controller/bus'); -const InMemoryChannel = require('../../../../../../src/controller/channels/in_memory_channel'); -const BaseChannel = require('../../../../../../src/controller/channels/base_channel'); -const Bus = require('../../../../../../src/controller/bus'); -const Event = require('../../../../../../src/controller/event'); +/* eslint-disable import/first */ + +import { + InMemoryChannel, + BaseChannel, +} from '../../../../../../src/controller/channels'; +import { Bus } from '../../../../../../src/controller/bus'; +import { Event } from '../../../../../../src/controller/event'; describe('InMemoryChannel Channel', () => { // Arrange @@ -42,8 +44,10 @@ describe('InMemoryChannel Channel', () => { }, options: {}, }; - let inMemoryChannel = null; - const bus = new Bus(); + const logger: any = {}; + const config: any = {}; + let inMemoryChannel: InMemoryChannel; + const bus: Bus = new Bus({}, logger, config); beforeEach(() => { // Act @@ -60,42 +64,6 @@ describe('InMemoryChannel Channel', () => { // Assert expect(InMemoryChannel.prototype).toBeInstanceOf(BaseChannel); }); - - it('should call BaseChannel class constructor with arguments', () => { - // Arrange - let IsolatedInMemoryChannel; - let IsolatedBaseChannel; - - /** - * Since `event_emitter` and `BaseChannel` was required on top of the test file, - * we have to isolate the modules to using the same module state from previous - * require calls. - */ - jest.isolateModules(() => { - // no need to restore mock since, `restoreMocks` option was set to true in unit test config file. - jest.doMock('../../../../../../src/controller/channels/base_channel'); - // eslint-disable-next-line global-require - IsolatedInMemoryChannel = require('../../../../../../src/controller/channels/in_memory_channel'); - // eslint-disable-next-line global-require - IsolatedBaseChannel = require('../../../../../../src/controller/channels/base_channel'); - }); - - // Act - inMemoryChannel = new IsolatedInMemoryChannel( - params.moduleAlias, - params.events, - params.actions, - params.options, - ); - - // Assert - expect(IsolatedBaseChannel).toHaveBeenCalledWith( - params.moduleAlias, - params.events, - params.actions, - params.options, - ); - }); }); describe('#constructor', () => { @@ -113,9 +81,11 @@ describe('InMemoryChannel Channel', () => { // Assert expect(inMemoryChannel.bus).toBe(bus); - expect(inMemoryChannel.bus.registerChannel).toHaveBeenCalledWith( + expect( + inMemoryChannel.bus?.registerChannel, + ).toHaveBeenCalledWith( inMemoryChannel.moduleAlias, - inMemoryChannel.eventsList.map(event => event.name), + inMemoryChannel.eventsList, inMemoryChannel.actions, { type: 'inMemory', channel: inMemoryChannel }, ); @@ -134,10 +104,10 @@ describe('InMemoryChannel Channel', () => { await inMemoryChannel.registerToBus(bus); // Act - await inMemoryChannel.once(eventName); + inMemoryChannel.once(eventName, () => {}); // Assert - expect(inMemoryChannel.bus.once).toHaveBeenCalledWith( + expect(inMemoryChannel.bus?.once).toHaveBeenCalledWith( event.key(), expect.any(Function), ); @@ -157,10 +127,10 @@ describe('InMemoryChannel Channel', () => { // Act await inMemoryChannel.registerToBus(bus); - await inMemoryChannel.once(eventName); + inMemoryChannel.once(eventName, () => {}); // Assert - expect(inMemoryChannel.bus.once).toHaveBeenCalledWith( + expect(inMemoryChannel.bus?.once).toHaveBeenCalledWith( event.key(), expect.any(Function), ); @@ -196,7 +166,7 @@ describe('InMemoryChannel Channel', () => { inMemoryChannel.publish(eventFullName); // Assert - expect(inMemoryChannel.bus.publish).toHaveBeenCalledWith( + expect(inMemoryChannel.bus?.publish).toHaveBeenCalledWith( event.key(), event.serialize(), ); @@ -208,10 +178,6 @@ describe('InMemoryChannel Channel', () => { describe('#invoke', () => { const actionName = 'action1'; - it('should throw TypeError when action name was not provided', () => { - return expect(inMemoryChannel.invoke()).rejects.toBeInstanceOf(TypeError); - }); - it('should execute the action straight away if the action module is equal to moduleAlias', async () => { // Arrange const actionFullName = `${inMemoryChannel.moduleAlias}:${actionName}`; @@ -232,7 +198,7 @@ describe('InMemoryChannel Channel', () => { await inMemoryChannel.invoke(actionFullName); // Assert - expect(inMemoryChannel.bus.invoke).toHaveBeenCalled(); + expect(inMemoryChannel.bus?.invoke).toHaveBeenCalled(); }); }); }); diff --git a/framework/test/jest/unit/specs/controller/controller.spec.js b/framework/test/jest/unit/specs/controller/controller.spec.js deleted file mode 100644 index 024388089dd..00000000000 --- a/framework/test/jest/unit/specs/controller/controller.spec.js +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright © 2019 Lisk Foundation - * - * See the LICENSE file at the top-level directory of this distribution - * for licensing information. - * - * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, - * no part of this software, including this file, may be copied, modified, - * propagated, or distributed except according to the terms contained in the - * LICENSE file. - * - * Removal or modification of this copyright notice is prohibited. - */ - -'use strict'; - -jest.mock('fs-extra'); -jest.mock('../../../../../src/controller/bus'); -jest.mock('../../../../../src/controller/channels/in_memory_channel'); - -const fs = require('fs-extra'); -const Controller = require('../../../../../src/controller/controller'); -const Bus = require('../../../../../src/controller/bus'); - -describe('Controller Class', () => { - // Arrange - const appLabel = '#LABEL'; - const logger = { - debug: jest.fn(), - info: jest.fn(), - error: jest.fn(), - }; - const storage = { - entities: { - Migration: { - applyAll: jest.fn(), - }, - }, - }; - const channel = { - registerToBus: jest.fn(), - }; - const config = { - components: '#CONFIG', - tempPath: '/tmp/lisk', - }; - const systemDirs = { - temp: `${config.tempPath}/${appLabel}/`, - sockets: `${config.tempPath}/${appLabel}/sockets`, - pids: `${config.tempPath}/${appLabel}/pids`, - }; - const configController = { - ...config, - dirs: systemDirs, - socketsPath: { - root: `unix://${systemDirs.sockets}`, - pub: `unix://${systemDirs.sockets}/lisk_pub.sock`, - sub: `unix://${systemDirs.sockets}/lisk_sub.sock`, - rpc: `unix://${systemDirs.sockets}/lisk_rpc.sock`, - }, - }; - - const params = { - appLabel, - config, - logger, - storage, - channel, - }; - - let controller = null; - - beforeEach(() => { - // Arrange - fs.readdirSync = jest.fn().mockReturnValue([]); - // Act - controller = new Controller(params); - }); - - afterEach(async () => { - // Act - controller.cleanup(); - }); - - describe('#constructor', () => { - it('should initialize the instance correctly when valid arguments were provided.', () => { - // Assert - expect(controller.logger).toEqual(logger); - expect(controller.appLabel).toEqual(appLabel); - expect(controller.config).toEqual(configController); - expect(controller.modules).toEqual({}); - expect(controller.channel).toBe(channel); - expect(controller.storage).toBe(storage); - expect(controller.bus).toBeNull(); - }); - }); - - describe('#load', () => { - it('should call initialization methods.', async () => { - // Arrange - const spies = { - _setupDirectories: jest.spyOn(controller, '_setupDirectories'), - _validatePidFile: jest.spyOn(controller, '_validatePidFile'), - _setupBus: jest.spyOn(controller, '_setupBus'), - _loadMigrations: jest - .spyOn(controller, '_loadMigrations') - .mockImplementation(), - _loadModules: jest.spyOn(controller, '_loadModules'), - }; - const modules = {}; - const moduleOptions = {}; - - // Act - await controller.load(modules, moduleOptions, {}, {}); - - // Assert - // Order of the functions matters in load method - expect(spies._setupDirectories).toHaveBeenCalled(); - expect(spies._validatePidFile).toHaveBeenCalledAfter( - spies._setupDirectories, - ); - expect(spies._setupBus).toHaveBeenCalledAfter(spies._validatePidFile); - expect(spies._loadMigrations).toHaveBeenCalledAfter(spies._setupBus); - expect(spies._loadModules).toHaveBeenCalledAfter(spies._loadMigrations); - expect(spies._loadModules).toHaveBeenCalledWith(modules, moduleOptions); - }); - - it('should log registered events and actions', async () => { - // Arrange - const modules = {}; - const moduleOptions = {}; - - // Act - await controller.load(modules, moduleOptions, {}, {}); - - // Assert - expect(logger.debug).toHaveBeenCalledWith( - undefined, - 'Bus listening to events', - ); - expect(logger.debug).toHaveBeenCalledWith( - undefined, - 'Bus ready for actions', - ); - }); - }); - - describe('#setupDirectories', () => { - it('should ensure directories exist', async () => { - // Act - await controller._setupDirectories(); - - // Assert - expect(fs.ensureDir).toHaveBeenCalledWith(systemDirs.temp); - expect(fs.ensureDir).toHaveBeenCalledWith(systemDirs.sockets); - expect(fs.ensureDir).toHaveBeenCalledWith(systemDirs.pids); - }); - }); - - describe('#_validatePidFile', () => { - it.todo( - 'should call `fs.writeFile` function with pidPath, process.pid arguments.', - ); - - it.todo( - 'should throw `DuplicateAppInstanceError` if an application is already running with the given label.', - ); - }); - - describe('#_setupBus', () => { - beforeEach(async () => { - // Act - controller._setupBus(); - }); - - it('should set created `Bus` instance to `controller.bus` property.', () => { - // Assert - expect(Bus).toHaveBeenCalledWith( - { - wildcard: true, - delimiter: ':', - maxListeners: 1000, - }, - logger, - configController, - ); - expect(controller.bus).toBeInstanceOf(Bus); - }); - - it('should call `controller.bus.setup()` method.', () => { - // Assert - expect(controller.bus.setup).toHaveBeenCalled(); - }); - - it('should call `controller.channel.registerToBus()` method.', () => { - // Assert - expect(controller.bus.setup).toHaveBeenCalled(); - }); - - it.todo('should log events if level is greater than info.'); - }); - - describe('#_loadMigrations', () => { - it.todo('should load migrations.'); - }); - - describe('#_loadModules', () => { - it('should load modules in sequence', async () => { - // Arrange - const spies = { - _loadInMemoryModule: jest - .spyOn(controller, '_loadInMemoryModule') - .mockResolvedValue(''), - }; - - const modules = { - dummyModule1: '#KLASS1', - dummyModule2: '#KLASS2', - dummyModule3: '#KLASS3', - }; - - const moduleOptions = { - dummyModule1: '#OPTIONS1', - dummyModule2: '#OPTIONS2', - dummyModule3: '#OPTIONS3', - }; - - // Act - await controller._loadModules(modules, moduleOptions); - - // Assert - Object.keys(modules).forEach((alias, index) => { - expect(spies._loadInMemoryModule).toHaveBeenNthCalledWith( - index + 1, - alias, - modules[alias], - moduleOptions[alias], - ); - }); - }); - }); - - describe('#_loadInMemoryModule', () => { - it.todo('should call validateModuleSpec function.'); - - describe('when creating channel', () => { - it.todo( - 'should add created channel to `controller.modulesChannel` object', - ); - - it.todo( - 'should call `channel.registerToBus` method to register channel to the Bus.', - ); - }); - - describe('when creating module', () => { - it.todo('should publish `loading:started` event before loading module.'); - it.todo('should call `module.load` method.'); - it.todo('should publish `loading:finished` event after loading module.'); - it.todo('should add module to `controller.modules` object.'); - }); - }); - describe('#unloadModules', () => { - let stubs = {}; - beforeEach(() => { - // Arrange - stubs = { - dummyModuleUnload1: jest.fn(), - dummyModuleUnload2: jest.fn(), - dummyModuleUnload3: jest.fn(), - }; - - controller.modules = { - dummyModule1: { - unload: stubs.dummyModuleUnload1, - }, - dummyModule2: { - unload: stubs.dummyModuleUnload2, - }, - dummyModule3: { - unload: stubs.dummyModuleUnload3, - }, - }; - }); - - it('should unload modules in sequence', async () => { - // Act - await controller.unloadModules(); - - // Assert - expect(stubs.dummyModuleUnload1).toHaveBeenCalled(); - expect(stubs.dummyModuleUnload2).toHaveBeenCalledAfter( - stubs.dummyModuleUnload1, - ); - expect(stubs.dummyModuleUnload3).toHaveBeenCalledAfter( - stubs.dummyModuleUnload2, - ); - }); - - it('should unload all modules if modules argument was not provided', async () => { - // Act - await controller.unloadModules(); - - // Assert - expect(controller.modules).toEqual({}); - }); - - it('should unload given modules if modules argument was provided', async () => { - // Act - await controller.unloadModules(['dummyModule1', 'dummyModule3']); - - // Assert - expect(controller.modules).toEqual({ - dummyModule2: { - unload: stubs.dummyModuleUnload2, - }, - }); - }); - }); -}); diff --git a/framework/test/jest/unit/specs/controller/controller.spec.ts b/framework/test/jest/unit/specs/controller/controller.spec.ts new file mode 100644 index 00000000000..5b12319b893 --- /dev/null +++ b/framework/test/jest/unit/specs/controller/controller.spec.ts @@ -0,0 +1,313 @@ +/* + * Copyright © 2019 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +jest.mock('fs-extra'); +jest.mock('../../../../../src/controller/bus'); +jest.mock('../../../../../src/controller/channels/in_memory_channel'); + +/* eslint-disable import/first */ + +import * as fs from 'fs-extra'; +import { Controller } from '../../../../../src/controller/controller'; +import { Bus } from '../../../../../src/controller/bus'; + +const createMockModule = (alias: string, loadStub?: any, unloadStub?: any) => { + function Module(this: any) { + this.load = loadStub ?? jest.fn(); + this.unload = unloadStub ?? jest.fn(); + this.defaults = {}; + this.events = []; + this.actions = {}; + } + + Module.info = { + name: alias || 'dummy', + version: 'dummy', + author: 'dummy', + }; + Module.alias = alias ?? 'dummy'; + + return Module; +}; +describe('Controller Class', () => { + // Arrange + const appLabel = '#LABEL'; + const logger = { + debug: jest.fn(), + info: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + fatal: jest.fn(), + warn: jest.fn(), + level: jest.fn(), + }; + const storage: any = { + entities: { + Migration: { + applyAll: jest.fn(), + }, + }, + }; + const channel: any = { + registerToBus: jest.fn(), + }; + const config = { + tempPath: '/tmp/lisk', + ipc: { + enabled: false, + }, + }; + const systemDirs = { + temp: `${config.tempPath}/${appLabel}/`, + sockets: `${config.tempPath}/${appLabel}/sockets`, + pids: `${config.tempPath}/${appLabel}/pids`, + }; + const configController = { + tempPath: '/tmp/lisk/#LABEL/', + ipc: { + enabled: false, + }, + dirs: systemDirs, + socketsPath: { + root: `unix://${systemDirs.sockets}`, + pub: `unix://${systemDirs.sockets}/lisk_pub.sock`, + sub: `unix://${systemDirs.sockets}/lisk_sub.sock`, + rpc: `unix://${systemDirs.sockets}/lisk_rpc.sock`, + }, + }; + + const params = { + appLabel, + config, + logger, + storage, + channel, + }; + + let controller: Controller; + + beforeEach(() => { + // Arrange + jest.spyOn(fs, 'readdirSync').mockReturnValue([]); + // Act + controller = new Controller(params); + }); + + afterEach(async () => { + // Act + await controller.cleanup(); + }); + + describe('#constructor', () => { + it('should initialize the instance correctly when valid arguments were provided.', () => { + // Assert + expect(controller.logger).toEqual(logger); + expect(controller.appLabel).toEqual(appLabel); + expect(controller.config).toEqual(configController); + expect(controller.modules).toEqual({}); + expect(controller.channel).toBe(channel); + expect(controller.storage).toBe(storage); + expect(controller.bus).toBeUndefined(); + }); + }); + + describe('#load', () => { + let modules: any; + let moduleOptions: any; + + beforeEach(async () => { + modules = { + dummyModule1: createMockModule('dummyModule1'), + dummyModule2: createMockModule('dummyModule2'), + dummyModule3: createMockModule('dummyModule3'), + }; + + moduleOptions = { + dummyModule1: '#OPTIONS1', + dummyModule2: '#OPTIONS2', + dummyModule3: '#OPTIONS3', + }; + + await controller.load(modules, moduleOptions, {}); + }); + + describe('_setupDirectories', () => { + it('should ensure directory exists', async () => { + // Arrange + jest.spyOn(fs, 'ensureDir'); + + // Assert + expect(fs.ensureDir).toHaveBeenCalledWith(controller.config.dirs.temp); + expect(fs.ensureDir).toHaveBeenCalledWith( + controller.config.dirs.sockets, + ); + expect(fs.ensureDir).toHaveBeenCalledWith(controller.config.dirs.pids); + }); + }); + + describe('_validatePidFile', () => { + it('should write process id to pid file if pid file not exits', async () => { + // eslint-disable-next-line + // @ts-ignore + jest.spyOn(fs, 'pathExists').mockResolvedValue(false); + jest.spyOn(fs, 'writeFile'); + + expect(fs.writeFile).toBeCalledWith( + `${controller.config.dirs.pids}/controller.pid`, + expect.toBeNumber(), + ); + }); + + it.todo('should check if process is running if pid file exists'); + it.todo('should throw error if process for same app is running already'); + }); + + describe('_setupBus', () => { + it('should set created `Bus` instance to `controller.bus` property.', () => { + // Assert + expect(Bus).toHaveBeenCalledWith( + { + wildcard: true, + delimiter: ':', + maxListeners: 1000, + }, + logger, + configController, + ); + expect(controller.bus).toBeInstanceOf(Bus); + }); + + it('should call `controller.bus.setup()` method.', () => { + // Assert + expect(controller.bus?.setup).toHaveBeenCalled(); + }); + + it('should call `controller.channel.registerToBus()` method.', () => { + // Assert + expect(controller.bus?.setup).toHaveBeenCalled(); + }); + + it.todo('should log events if level is greater than info.'); + }); + + describe('_loadMigrations', () => { + it.todo('should load migrations.'); + }); + + describe('_loadModules', () => { + it.todo('should load modules in sequence'); + it.todo('should call validateModuleSpec function.'); + + describe('when creating channel', () => { + it.todo( + 'should add created channel to `controller.modulesChannel` object', + ); + + it.todo( + 'should call `channel.registerToBus` method to register channel to the Bus.', + ); + }); + + describe('when creating module', () => { + it.todo( + 'should publish `loading:started` event before loading module.', + ); + it.todo('should call `module.load` method.'); + it.todo( + 'should publish `loading:finished` event after loading module.', + ); + it.todo('should add module to `controller.modules` object.'); + }); + }); + + it('should log registered events and actions', async () => { + // Assert + expect(logger.debug).toHaveBeenCalledWith( + undefined, + 'Bus listening to events', + ); + expect(logger.debug).toHaveBeenCalledWith( + undefined, + 'Bus ready for actions', + ); + }); + }); + + describe('#unloadModules', () => { + let loadStubs: any; + let unloadStubs: any; + + beforeEach(async () => { + // Arrange + loadStubs = { + module1: jest.fn(), + module2: jest.fn(), + }; + + unloadStubs = { + module1: jest.fn(), + module2: jest.fn(), + }; + + const modules: any = { + module1: createMockModule( + 'module1', + loadStubs.module1, + unloadStubs.module1, + ), + module2: createMockModule( + 'module2', + loadStubs.module2, + unloadStubs.module2, + ), + }; + const moduleOptions: any = { + module1: { + loadAsChildProcess: false, + }, + module2: { + loadAsChildProcess: false, + }, + }; + + await controller.load(modules, moduleOptions); + }); + + it('should unload modules in sequence', async () => { + // Act + await controller.unloadModules(); + + // Assert + expect(unloadStubs.module1).toHaveBeenCalled(); + expect(unloadStubs.module2).toHaveBeenCalled(); + expect(unloadStubs.module2).toHaveBeenCalledAfter(unloadStubs.module1); + }); + + it('should unload all modules if modules argument was not provided', async () => { + // Act + await controller.unloadModules(); + + // Assert + expect(controller.modules).toEqual({}); + }); + + it('should unload given modules if modules argument was provided', async () => { + // Act + await controller.unloadModules(['module2']); + + // Assert + expect(Object.keys(controller.modules)).toEqual(['module1']); + }); + }); +}); diff --git a/framework/test/jest/unit/specs/controller/channels/child_process/index.spec.js b/framework/test/jest/unit/specs/controller/event/constants.ts similarity index 65% rename from framework/test/jest/unit/specs/controller/channels/child_process/index.spec.js rename to framework/test/jest/unit/specs/controller/event/constants.ts index e045021afbf..0e5fc1f7b08 100644 --- a/framework/test/jest/unit/specs/controller/channels/child_process/index.spec.js +++ b/framework/test/jest/unit/specs/controller/event/constants.ts @@ -12,10 +12,8 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; - -describe('#setupProcessHandlers', () => { - // TODO: Figure out how to test this function in a future Issue - // eslint-disable-next-line jest/no-disabled-tests - it.skip('should register required events in process.once()', () => {}); -}); +export const MODULE_NAME = 'module'; +export const EVENT_NAME = 'event'; +export const VALID_EVENT_NAME_ARG = `${MODULE_NAME}:${EVENT_NAME}`; +export const INVALID_EVENT_NAME_ARG = `${MODULE_NAME}`; +export const DATA = '#data'; diff --git a/framework/test/jest/unit/specs/controller/event/event.spec.js b/framework/test/jest/unit/specs/controller/event/event.spec.ts similarity index 91% rename from framework/test/jest/unit/specs/controller/event/event.spec.js rename to framework/test/jest/unit/specs/controller/event/event.spec.ts index eb108549eeb..1291cec2eb7 100644 --- a/framework/test/jest/unit/specs/controller/event/event.spec.js +++ b/framework/test/jest/unit/specs/controller/event/event.spec.ts @@ -12,25 +12,18 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; +import { Event } from '../../../../../../src/controller/event'; -const Event = require('../../../../../../src/controller/event'); -const { +import { EVENT_NAME, MODULE_NAME, VALID_EVENT_NAME_ARG, INVALID_EVENT_NAME_ARG, DATA, -} = require('./constants'); +} from './constants'; describe('Event Class', () => { describe('#constructor', () => { - it('should throw error when no name argument was provided.', () => { - expect(() => new Event()).toThrow( - 'Event name "undefined" must be a valid name with module name.', - ); - }); - it('should throw error when invalid name argument was provided.', () => { // Act & Assert expect(() => new Event(INVALID_EVENT_NAME_ARG)).toThrow( @@ -58,7 +51,8 @@ describe('Event Class', () => { }); describe('methods', () => { - let event; + let event: Event; + beforeEach(() => { // Act event = new Event(VALID_EVENT_NAME_ARG, DATA); diff --git a/framework/test/jest/unit/specs/jest.config/__snapshots__/jest.config.unit.spec.js.snap b/framework/test/jest/unit/specs/jest.config/__snapshots__/jest.config.unit.spec.js.snap index 920c4a171c1..f33e9f78073 100644 --- a/framework/test/jest/unit/specs/jest.config/__snapshots__/jest.config.unit.spec.js.snap +++ b/framework/test/jest/unit/specs/jest.config/__snapshots__/jest.config.unit.spec.js.snap @@ -16,7 +16,7 @@ Object { ], "globals": Object { "ts-jest": Object { - "tsConfig": "./test/tsconfig.json", + "tsConfig": "/test/tsconfig.json", }, }, "resetModules": true, diff --git a/framework/types/pm2-axon/index.d.ts b/framework/types/pm2-axon/index.d.ts index 2b1b9133702..dd985c2f91c 100644 --- a/framework/types/pm2-axon/index.d.ts +++ b/framework/types/pm2-axon/index.d.ts @@ -134,6 +134,7 @@ declare module 'pm2-axon' { } export class RepSocket extends Socket { + public sock: Socket; public onmessage(sock: NetSocket): (args: Buffer | Buffer[]) => void; } From 35d75da1aa07f3a116609b2d4781e43a49157a4d Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 6 May 2020 17:26:33 +0200 Subject: [PATCH 26/35] Update config for collecting coverage for jest unit tests --- framework/test/jest/config/jest.config.base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/test/jest/config/jest.config.base.js b/framework/test/jest/config/jest.config.base.js index baa428bb295..1d5f8461e92 100644 --- a/framework/test/jest/config/jest.config.base.js +++ b/framework/test/jest/config/jest.config.base.js @@ -18,7 +18,7 @@ module.exports = { }, }, verbose: true, - collectCoverage: false, + collectCoverage: true, coverageReporters: ['json', 'lcov', 'cobertura'], rootDir: '../../../', setupFilesAfterEnv: ['/test/jest/config/setup.js'], From c52f9cd493b9d4b8cb2bdd6f0d1508ed6a708bd0 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 6 May 2020 23:28:14 +0200 Subject: [PATCH 27/35] Fix failing jest unit tests --- .../controller/channels/in_memory_channel.ts | 2 -- .../specs/application/application.spec.js | 20 +++++++++---------- .../fast_chain_switching_mechanism.spec.ts | 2 +- .../node/synchronizer/synchronizer.spec.ts | 2 +- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index 0a0217d34a3..6572f1ef8c6 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -59,8 +59,6 @@ export class InMemoryChannel extends BaseChannel { public async invoke(actionName: string, params?: object): Promise { const action = new Action(actionName, params, this.moduleAlias); - console.info(this.actions); - if (action.module === this.moduleAlias) { if (this.actions[action.name] === undefined) { throw new Error( diff --git a/framework/test/jest/unit/specs/application/application.spec.js b/framework/test/jest/unit/specs/application/application.spec.js index 053270d05a1..f3db381c7a2 100644 --- a/framework/test/jest/unit/specs/application/application.spec.js +++ b/framework/test/jest/unit/specs/application/application.spec.js @@ -339,53 +339,53 @@ describe('Application', () => { describe('#_initChannel', () => { let app; - let actions; + let actionsList; beforeEach(() => { // Arrange app = new Application(genesisBlock, config); app.channel = app._initChannel(); - actions = app.channel.actionsList.map(action => action.name); + actionsList = app.channel.actionsList; }); it('should create getAccount action', () => { // Assert - expect(actions).toContain('getAccount'); + expect(actionsList).toContain('getAccount'); }); it('should create getAccounts action', () => { // Assert - expect(actions).toContain('getAccounts'); + expect(actionsList).toContain('getAccounts'); }); it('should create getBlockByID action', () => { // Assert - expect(actions).toContain('getBlockByID'); + expect(actionsList).toContain('getBlockByID'); }); it('should create getBlocksByIDs action', () => { // Assert - expect(actions).toContain('getBlocksByIDs'); + expect(actionsList).toContain('getBlocksByIDs'); }); it('should create getBlockByHeight action', () => { // Assert - expect(actions).toContain('getBlockByHeight'); + expect(actionsList).toContain('getBlockByHeight'); }); it('should create getBlocksByHeightBetween action', () => { // Assert - expect(actions).toContain('getBlocksByHeightBetween'); + expect(actionsList).toContain('getBlocksByHeightBetween'); }); it('should create getTransactionByID action', () => { // Assert - expect(actions).toContain('getTransactionByID'); + expect(actionsList).toContain('getTransactionByID'); }); it('should create getTransactionsByIDs action', () => { // Assert - expect(actions).toContain('getTransactionsByIDs'); + expect(actionsList).toContain('getTransactionsByIDs'); }); }); }); diff --git a/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts b/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts index 27aada90939..684e7c3346e 100644 --- a/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts +++ b/framework/test/jest/unit/specs/application/node/synchronizer/fast_chain_switching_mechanism/fast_chain_switching_mechanism.spec.ts @@ -29,7 +29,7 @@ import { registeredTransactions } from '../../../../../../../utils/registered_tr import * as genesisBlockDevnet from '../../../../../../../fixtures/config/devnet/genesis_block.json'; -const ChannelMock: any = jest.genMockFromModule( +const { InMemoryChannel: ChannelMock } = jest.genMockFromModule( '../../../../../../../../src/controller/channels/in_memory_channel', ); diff --git a/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts b/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts index 150ef00a491..1ce26cb682b 100644 --- a/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts +++ b/framework/test/jest/unit/specs/application/node/synchronizer/synchronizer.spec.ts @@ -27,7 +27,7 @@ import { registeredTransactions } from '../../../../../../utils/registered_trans import * as genesisBlockDevnet from '../../../../../../fixtures/config/devnet/genesis_block.json'; -const ChannelMock: any = jest.genMockFromModule( +const { InMemoryChannel: ChannelMock } = jest.genMockFromModule( '../../../../../../../src/controller/channels/in_memory_channel', ); From 5b6f42802feb770029db9625e9384d56f5dc66d5 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 7 May 2020 11:11:28 +0200 Subject: [PATCH 28/35] Apply suggestions from code review Co-authored-by: Shu --- framework/src/controller/bus.ts | 4 ++-- framework/src/controller/channels/child_process_channel.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index b85cfd9e410..48fd55c6b85 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -170,7 +170,7 @@ export class Bus extends EventEmitter2 { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invoke(actionData: string | ActionInfoObject): Promise { + public async invoke(actionData: string | ActionInfoObject): Promise { const action = Action.deserialize(actionData); const actionModule = action.module; const actionFullName = action.key(); @@ -231,7 +231,7 @@ export class Bus extends EventEmitter2 { } // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types - public publish(eventName: string, eventValue: any): void | never { + public publish(eventName: string, eventValue: T): void { if (!this.getEvents().includes(eventName)) { throw new Error(`Event ${eventName} is not registered to bus.`); } diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index 90c21d60488..f5c903048a1 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -156,7 +156,7 @@ export class ChildProcessChannel extends BaseChannel { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invoke(actionName: string, params?: object): Promise { + public async invoke(actionName: string, params?: object): Promise { const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { @@ -186,7 +186,7 @@ export class ChildProcessChannel extends BaseChannel { remoteMethod: string, params: object, // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { + ): Promise { return this.invoke(`app:${remoteMethod}`, params); } From cfc39ae1bbe21a2319193daec72fd7716cf794bd Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 7 May 2020 13:32:03 +0200 Subject: [PATCH 29/35] Fix generic type and module klass type issues --- framework/src/controller/bus.ts | 45 +++++-------------- .../src/controller/channels/base_channel.ts | 19 ++++---- .../channels/child_process_channel.ts | 21 +++++---- .../controller/channels/in_memory_channel.ts | 11 +++-- framework/src/controller/channels/index.ts | 4 +- .../src/controller/child_process_loader.ts | 22 +++++---- framework/src/controller/controller.ts | 33 +++++++------- framework/src/controller/event.ts | 6 +-- framework/src/modules/base_module.ts | 14 +++++- 9 files changed, 84 insertions(+), 91 deletions(-) diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 48fd55c6b85..0812620be68 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -23,10 +23,10 @@ import { Client as RPCClient, Server as RPCServer } from 'pm2-axon-rpc'; import { EventEmitter2, Listener } from 'eventemitter2'; import { Action, ActionInfoObject, ActionsObject } from './action'; import { Logger } from '../types'; -import { BaseChannel } from './channels'; +import { BaseChannel } from './channels/base_channel'; import { EventsArray } from './event'; import { SocketPaths } from './types'; -import { CONTROLLER_IDENTIFIER, SOCKET_TIMEOUT_TIME } from './constants'; +import { SOCKET_TIMEOUT_TIME } from './constants'; interface BusConfiguration { ipc: { @@ -41,6 +41,8 @@ interface RegisterChannelOptions { readonly rpcSocketPath?: string; } +type NodeCallback = (error: Error | null, result?: unknown) => void; + interface ChannelInfo { readonly channel: BaseChannel; readonly actions: ActionsObject; @@ -96,20 +98,20 @@ export class Bus extends EventEmitter2 { this.rpcServer.expose( 'registerChannel', - (moduleAlias, events, actions, options, cb) => { + (moduleAlias, events, actions, options, cb: NodeCallback) => { this.registerChannel(moduleAlias, events, actions, options) .then(() => cb(null)) .catch(error => cb(error)); }, ); - this.rpcServer.expose('invoke', (action, cb) => { + this.rpcServer.expose('invoke', (action, cb: NodeCallback) => { this.invoke(action) .then(data => cb(null, data)) .catch(error => cb(error)); }); - this.rpcServer.expose('invokePublic', (action, cb) => { + this.rpcServer.expose('invokePublic', (action, cb: NodeCallback) => { this.invokePublic(action) .then(data => cb(null, data)) .catch(error => cb(error)); @@ -172,7 +174,6 @@ export class Bus extends EventEmitter2 { // eslint-disable-next-line @typescript-eslint/no-explicit-any public async invoke(actionData: string | ActionInfoObject): Promise { const action = Action.deserialize(actionData); - const actionModule = action.module; const actionFullName = action.key(); const actionParams = action.params; @@ -182,37 +183,13 @@ export class Bus extends EventEmitter2 { const channelInfo = this.channels[actionFullName]; - if (actionModule === CONTROLLER_IDENTIFIER) { - return channelInfo.channel.invoke(actionFullName, actionParams); - } - - if (channelInfo.type === 'inMemory') { - return channelInfo.channel.invoke(actionFullName, actionParams); - } - - return new Promise((resolve, reject) => { - // TODO: Fix when both channel types are converted to typescript - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - (channelInfo.channel as any).call( - 'invoke', - action.serialize(), - // eslint-disable-next-line - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (err?: string | object, data: any) => { - if (err) { - return reject(err); - } - return resolve(data); - }, - ); - }); + return channelInfo.channel.invoke(actionFullName, actionParams); } - public async invokePublic( + public async invokePublic( actionData: string | ActionInfoObject, // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { + ): Promise { const action = Action.deserialize(actionData); // Check if action exists @@ -231,7 +208,7 @@ export class Bus extends EventEmitter2 { } // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types - public publish(eventName: string, eventValue: T): void { + public publish(eventName: string, eventValue: object): void { if (!this.getEvents().includes(eventName)) { throw new Error(`Event ${eventName} is not registered to bus.`); } diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index e7dcee8c3de..85ef382a623 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -14,7 +14,7 @@ import { EventCallback } from '../event'; import { Action, ActionsDefinition, ActionsObject } from '../action'; -import { INTERNAL_EVENTS, eventWithModuleNameReg } from '../constants'; +import { eventWithModuleNameReg, INTERNAL_EVENTS } from '../constants'; export interface BaseChannelOptions { readonly skipInternalEvents?: boolean; @@ -37,12 +37,10 @@ export abstract class BaseChannel { this.moduleAlias = moduleAlias; this.options = options; - const eventList = options.skipInternalEvents + this.eventsList = options.skipInternalEvents ? events : [...events, ...INTERNAL_EVENTS]; - this.eventsList = eventList; - this.actions = {}; for (const actionName of Object.keys(actions)) { const actionData = actions[actionName]; @@ -52,14 +50,13 @@ export abstract class BaseChannel { const isPublic = typeof actionData === 'object' ? actionData.isPublic ?? true : true; - const action = new Action( + this.actions[actionName] = new Action( `${this.moduleAlias}:${actionName}`, undefined, undefined, isPublic, handler, ); - this.actions[actionName] = action; } this.actionsList = Object.keys(this.actions); } @@ -100,20 +97,20 @@ export abstract class BaseChannel { // Call action of any moduleAlias through controller // Specified as moduleName:actionName - abstract async invoke(actionName: string, params?: object): Promise; + abstract async invoke(actionName: string, params?: object): Promise; // Call action network module when requesting from the network or a specific peer // Specified as actionName for request available on the network - abstract async invokeFromNetwork( + abstract async invokeFromNetwork( actionName: string, params?: object, - ): Promise; + ): Promise; // Call action of any moduleAlias through controller // Specified as moduleName:actionName // Specified action must be defined as publicly callable - abstract async invokePublic( + abstract async invokePublic( actionName: string, params?: object, - ): Promise; + ): Promise; } diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index f5c903048a1..2f9abb524fd 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -33,7 +33,7 @@ export const setupProcessHandlers = (channel: ChildProcessChannel): void => { process.once('exit', code => channel.cleanup(code)); }; -type NodeCallback = (error: Error | null, result?: number) => void; +type NodeCallback = (error: Error | null, result?: unknown) => void; const SOCKET_TIMEOUT_TIME = 2000; @@ -172,7 +172,7 @@ export class ChildProcessChannel extends BaseChannel { action.serialize(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (err: Error, data: any) => { - if (err) { + if (err !== undefined) { return reject(err); } @@ -182,7 +182,7 @@ export class ChildProcessChannel extends BaseChannel { }); } - public async invokeFromNetwork( + public async invokeFromNetwork( remoteMethod: string, params: object, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -199,7 +199,10 @@ export class ChildProcessChannel extends BaseChannel { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invokePublic(actionName: string, params?: object): Promise { + public async invokePublic( + actionName: string, + params?: object, + ): Promise { const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { @@ -221,7 +224,7 @@ export class ChildProcessChannel extends BaseChannel { action.serialize(), // eslint-disable-next-line @typescript-eslint/no-explicit-any (err: Error, data: any) => { - if (err) { + if (err !== undefined) { return reject(err); } @@ -292,7 +295,7 @@ export class ChildProcessChannel extends BaseChannel { { type: 'ipcSocket', rpcSocketPath: this.rpcSocketPath }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (err: Error, result: any) => { - if (err) { + if (err !== undefined) { reject(err); } resolve(result); @@ -306,7 +309,7 @@ export class ChildProcessChannel extends BaseChannel { await Promise.all(promises); } - private async _rejectWhenAnySocketFailsToBind() { + private async _rejectWhenAnySocketFailsToBind(): Promise { const promises = []; if (this.pubSocket) { @@ -339,11 +342,11 @@ export class ChildProcessChannel extends BaseChannel { ); } - return Promise.race(promises); + await Promise.race(promises); } // eslint-disable-next-line class-methods-use-this - private async _rejectWhenTimeout(timeInMillis: number) { + private async _rejectWhenTimeout(timeInMillis: number): Promise { return new Promise((_, reject) => { setTimeout(() => { reject(new Error('ChildProcessChannel sockets setup timeout')); diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index 6572f1ef8c6..0e15bd5ce27 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -56,7 +56,7 @@ export class InMemoryChannel extends BaseChannel { } // eslint-disable-next-line @typescript-eslint/require-await,@typescript-eslint/no-explicit-any - public async invoke(actionName: string, params?: object): Promise { + public async invoke(actionName: string, params?: object): Promise { const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { @@ -75,11 +75,11 @@ export class InMemoryChannel extends BaseChannel { return (this.bus as Bus).invoke(action.serialize()); } - public async invokeFromNetwork( + public async invokeFromNetwork( remoteMethod: string, params?: object, // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { + ): Promise { return this.invoke(`app:${remoteMethod}`, params); } @@ -92,7 +92,10 @@ export class InMemoryChannel extends BaseChannel { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public async invokePublic(actionName: string, params?: object): Promise { + public async invokePublic( + actionName: string, + params?: object, + ): Promise { const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { diff --git a/framework/src/controller/channels/index.ts b/framework/src/controller/channels/index.ts index 698847869fa..17aaffeeabe 100644 --- a/framework/src/controller/channels/index.ts +++ b/framework/src/controller/channels/index.ts @@ -13,7 +13,5 @@ */ export { BaseChannel } from './base_channel'; -// TODO: Fix cycle dependencies -// eslint-disable-next-line import/no-cycle -export { InMemoryChannel } from './in_memory_channel'; export { ChildProcessChannel } from './child_process_channel'; +export { InMemoryChannel } from './in_memory_channel'; diff --git a/framework/src/controller/child_process_loader.ts b/framework/src/controller/child_process_loader.ts index 73eea94e98c..fe4f39a7f18 100644 --- a/framework/src/controller/child_process_loader.ts +++ b/framework/src/controller/child_process_loader.ts @@ -15,15 +15,22 @@ // Parameters passed by `child_process.fork(_, parameters)` import { ChildProcessChannel } from './channels'; +import { InstantiableModule, BaseModule } from '../modules/base_module'; import { SocketPaths } from './types'; const modulePath: string = process.argv[2]; -// eslint-disable-next-line import/no-dynamic-require -const Klass = require(modulePath); - -const _loadModule = async (config: object, moduleOptions: object) => { - const module = new Klass(moduleOptions); - const moduleAlias = module.constructor.alias; +// eslint-disable-next-line import/no-dynamic-require,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires +const Klass: InstantiableModule = require(modulePath); + +const _loadModule = async ( + config: { + [key: string]: unknown; + socketsPath: SocketPaths; + }, + moduleOptions: object, +): Promise => { + const moduleAlias = Klass.alias; + const module: BaseModule = new Klass(moduleOptions); const channel = new ChildProcessChannel( moduleAlias, @@ -31,8 +38,7 @@ const _loadModule = async (config: object, moduleOptions: object) => { module.actions, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await channel.registerToBus((config as any).socketsPath as SocketPaths); + await channel.registerToBus(config.socketsPath); channel.publish(`${moduleAlias}:registeredToBus`); channel.publish(`${moduleAlias}:loading:started`); diff --git a/framework/src/controller/controller.ts b/framework/src/controller/controller.ts index 00a62006c45..5e65d00fe76 100644 --- a/framework/src/controller/controller.ts +++ b/framework/src/controller/controller.ts @@ -24,9 +24,9 @@ import { DuplicateAppInstanceError } from '../errors'; import { validateModuleSpec } from '../application/validator'; import { Logger, Storage } from '../types'; import { SocketPaths } from './types'; -import { BaseModule } from '../modules/base_module'; +import { BaseModule, InstantiableModule } from '../modules/base_module'; -const isPidRunning = async (pid: number) => +const isPidRunning = async (pid: number): Promise => psList().then(list => list.some(x => x.pid === pid)); export interface ControllerOptions { @@ -56,7 +56,7 @@ interface ControllerConfig { } interface ModulesObject { - readonly [key: string]: typeof BaseModule; + readonly [key: string]: InstantiableModule; } interface ModuleOptions { @@ -156,7 +156,7 @@ export class Controller { await this.unloadModules(); this.logger.info('Unload completed'); } catch (err) { - this.logger.error({ err }, 'Caused error during modules cleanup'); + this.logger.error(err, 'Caused error during modules cleanup'); } } @@ -180,7 +180,7 @@ export class Controller { if (pidRunning && pid !== process.pid) { this.logger.error( - { app_name: this.appLabel }, + { appLabel: this.appLabel }, 'An instance of application is already running, please change application name to run another instance', ); throw new DuplicateAppInstanceError(this.appLabel, pidPath); @@ -207,7 +207,10 @@ export class Controller { // If log level is greater than info if (this.logger.level !== undefined && this.logger.level() < 30) { this.bus.onAny(event => { - this.logger.trace(`eventName: ${event},`, 'Monitor Bus Channel'); + this.logger.trace( + `eventName: ${event as string},`, + 'Monitor Bus Channel', + ); }); } } @@ -243,15 +246,13 @@ export class Controller { private async _loadInMemoryModule( alias: string, - Klass: typeof BaseModule, + Klass: InstantiableModule, options: ModuleOptions, ): Promise { const moduleAlias = alias || Klass.alias; const { name, version } = Klass.info; - // eslint-disable-next-line - // @ts-ignore - const module = new Klass(options); + const module: BaseModule = new Klass(options); validateModuleSpec(module); this.logger.info( @@ -281,16 +282,14 @@ export class Controller { private async _loadChildProcessModule( alias: string, - Klass: typeof BaseModule, + Klass: InstantiableModule, options: ModuleOptions, ): Promise { - // eslint-disable-next-line - // @ts-ignore - const module = new Klass(options); - validateModuleSpec(module); + const moduleAlias = alias || Klass.alias; + const { name, version } = Klass.info; - const moduleAlias = alias || module.constructor.alias; - const { name, version } = module.constructor.info; + const module: BaseModule = new Klass(options); + validateModuleSpec(module); this.logger.info( { name, version, moduleAlias }, diff --git a/framework/src/controller/event.ts b/framework/src/controller/event.ts index 9151703ddb5..69667d1bb10 100644 --- a/framework/src/controller/event.ts +++ b/framework/src/controller/event.ts @@ -19,7 +19,7 @@ import { eventWithModuleNameReg } from './constants'; export interface EventInfoObject { readonly module: string; readonly name: string; - readonly data?: object | string; + readonly data?: object; } export type EventCallback = (action: EventInfoObject) => void; @@ -29,9 +29,9 @@ export type EventsArray = ReadonlyArray; export class Event { public module: string; public name: string; - public data?: object | string; + public data?: object; - public constructor(name: string, data?: object | string) { + public constructor(name: string, data?: object) { assert( eventWithModuleNameReg.test(name), `Event name "${name}" must be a valid name with module name.`, diff --git a/framework/src/modules/base_module.ts b/framework/src/modules/base_module.ts index cae4de0195e..fa215e74fcf 100644 --- a/framework/src/modules/base_module.ts +++ b/framework/src/modules/base_module.ts @@ -15,7 +15,7 @@ import { ImplementationMissingError } from '../errors'; import { EventsArray } from '../controller/event'; import { ActionsDefinition } from '../controller/action'; -import { BaseChannel } from '../controller/channels/base_channel'; +import { BaseChannel } from '../controller/channels'; export interface ModuleInfo { readonly author: string; @@ -23,10 +23,20 @@ export interface ModuleInfo { readonly name: string; } +export interface InstantiableModule { + alias: string; + info: ModuleInfo; + migrations: ReadonlyArray; + defaults: object; + load: () => Promise; + unload: () => Promise; + new (...args: U[]): T; +} + export abstract class BaseModule { public readonly options: object; - public constructor(options: object) { + protected constructor(options: object) { this.options = options; } From 3fceda62d94d57aee7893de95f72955e66c17292 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 7 May 2020 14:35:45 +0200 Subject: [PATCH 30/35] Update the access specifier for classes --- framework/src/controller/bus.ts | 23 ++++++++----------- .../src/controller/channels/base_channel.ts | 8 +++---- .../channels/child_process_channel.ts | 7 +----- .../controller/channels/in_memory_channel.ts | 2 +- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 0812620be68..02d9d0520a5 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -52,18 +52,18 @@ interface ChannelInfo { export class Bus extends EventEmitter2 { public logger: Logger; - public config: BusConfiguration; - public actions: { [key: string]: Action }; - public events: { [key: string]: boolean }; - public channels: { + + private readonly config: BusConfiguration; + private readonly actions: { [key: string]: Action }; + private readonly events: { [key: string]: boolean }; + private readonly channels: { [key: string]: ChannelInfo; }; - public rpcClients: { [key: string]: ReqSocket }; - public pubSocket?: PubEmitterSocket; - public subSocket?: SubEmitterSocket; - public rpcSocket?: RepSocket; - public rpcServer?: RPCServer; - public channel?: BaseChannel; + private readonly rpcClients: { [key: string]: ReqSocket }; + private pubSocket?: PubEmitterSocket; + private subSocket?: SubEmitterSocket; + private rpcSocket?: RepSocket; + private rpcServer?: RPCServer; public constructor( options: object, @@ -171,7 +171,6 @@ export class Bus extends EventEmitter2 { }; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public async invoke(actionData: string | ActionInfoObject): Promise { const action = Action.deserialize(actionData); const actionFullName = action.key(); @@ -188,7 +187,6 @@ export class Bus extends EventEmitter2 { public async invokePublic( actionData: string | ActionInfoObject, - // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { const action = Action.deserialize(actionData); @@ -207,7 +205,6 @@ export class Bus extends EventEmitter2 { return this.invoke(actionData); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types public publish(eventName: string, eventValue: object): void { if (!this.getEvents().includes(eventName)) { throw new Error(`Event ${eventName} is not registered to bus.`); diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index 85ef382a623..a8eb638ef69 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -21,12 +21,12 @@ export interface BaseChannelOptions { } export abstract class BaseChannel { - public readonly moduleAlias: string; - public readonly options: object; - public readonly eventsList: ReadonlyArray; public readonly actionsList: ReadonlyArray; - public readonly actions: ActionsObject; + + protected readonly actions: ActionsObject; + protected readonly moduleAlias: string; + protected readonly options: object; public constructor( moduleAlias: string, diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index 2f9abb524fd..8f34d19ca0a 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -155,7 +155,6 @@ export class ChildProcessChannel extends BaseChannel { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public async invoke(actionName: string, params?: object): Promise { const action = new Action(actionName, params, this.moduleAlias); @@ -185,7 +184,6 @@ export class ChildProcessChannel extends BaseChannel { public async invokeFromNetwork( remoteMethod: string, params: object, - // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { return this.invoke(`app:${remoteMethod}`, params); } @@ -193,12 +191,10 @@ export class ChildProcessChannel extends BaseChannel { public async publishToNetwork( actionName: string, data: object, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { + ): Promise { return this.invoke(`app:${actionName}`, data); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public async invokePublic( actionName: string, params?: object, @@ -234,7 +230,6 @@ export class ChildProcessChannel extends BaseChannel { }); } - // eslint-disable-next-line @typescript-eslint/require-await public cleanup(_status?: number, _message?: string): void { if (this.pubSocket) { this.pubSocket.close(); diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index 0e15bd5ce27..e79bb3106d7 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -18,7 +18,7 @@ import { BaseChannel } from './base_channel'; import { Bus } from '../bus'; export class InMemoryChannel extends BaseChannel { - public bus: Bus | undefined; + private bus: Bus | undefined; public async registerToBus(bus: Bus): Promise { this.bus = bus; From 1101e4051c839a07cb1a88abd764d973ba236819 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 7 May 2020 16:17:01 +0200 Subject: [PATCH 31/35] Fix failing integration test --- framework/src/controller/bus.ts | 4 +- .../controller/channels/in_memory_channel.ts | 5 +- ...nnel.spec.js => in_memory_channel.spec.ts} | 70 ++++++++++--------- 3 files changed, 41 insertions(+), 38 deletions(-) rename framework/test/jest/integration/specs/controller/{in_memory_channel.spec.js => in_memory_channel.spec.ts} (77%) diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 02d9d0520a5..626a4d5af8c 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -180,7 +180,7 @@ export class Bus extends EventEmitter2 { throw new Error(`Action '${action.key()}' is not registered to bus.`); } - const channelInfo = this.channels[actionFullName]; + const channelInfo = this.channels[action.module]; return channelInfo.channel.invoke(actionFullName, actionParams); } @@ -244,7 +244,7 @@ export class Bus extends EventEmitter2 { } // Communicate through event emitter - super.once([eventName], cb); + super.once(eventName, cb); // TODO: make it `once` instead of `on` // Communicate through unix socket diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index e79bb3106d7..139b31fc297 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -32,13 +32,13 @@ export class InMemoryChannel extends BaseChannel { } public subscribe(eventName: string, cb: EventCallback): void { - (this.bus as Bus).subscribe(new Event(eventName).key(), data => + (this.bus as Bus).subscribe(eventName, data => setImmediate(cb, Event.deserialize(data)), ); } public once(eventName: string, cb: EventCallback): void { - (this.bus as Bus).once(new Event(eventName).key(), data => + (this.bus as Bus).once(eventName, data => setImmediate(cb, Event.deserialize(data)), ); } @@ -51,7 +51,6 @@ export class InMemoryChannel extends BaseChannel { `Event "${eventName}" not registered in "${this.moduleAlias}" module.`, ); } - (this.bus as Bus).publish(event.key(), event.serialize()); } diff --git a/framework/test/jest/integration/specs/controller/in_memory_channel.spec.js b/framework/test/jest/integration/specs/controller/in_memory_channel.spec.ts similarity index 77% rename from framework/test/jest/integration/specs/controller/in_memory_channel.spec.js rename to framework/test/jest/integration/specs/controller/in_memory_channel.spec.ts index 9946fe9baf2..447b125d802 100644 --- a/framework/test/jest/integration/specs/controller/in_memory_channel.spec.js +++ b/framework/test/jest/integration/specs/controller/in_memory_channel.spec.ts @@ -12,17 +12,15 @@ * Removal or modification of this copyright notice is prohibited. */ -'use strict'; +import { InMemoryChannel } from '../../../../../src/controller/channels'; +import { Bus } from '../../../../../src/controller/bus'; +import { Event } from '../../../../../src/controller/event'; -const InMemoryChannel = require('../../../../../src/controller/channels/in_memory_channel'); -const Bus = require('../../../../../src/controller/bus'); -const Event = require('../../../../../src/controller/event'); - -const logger = { +const logger: any = { info: jest.fn(), }; -const config = { +const config: any = { ipc: { enabled: false, }, @@ -30,16 +28,14 @@ const config = { const alpha = { moduleAlias: 'alphaAlias', - events: ['alpha1', 'alpha2'].map( - event => new Event(`${'alphaAlias'}:${event}`), - ), + events: ['alpha1', 'alpha2'], actions: { multiplyByTwo: { - handler: action => action.params * 2, + handler: (action: any) => action.params.val * 2, isPublic: true, }, multiplyByThree: { - handler: action => action.params * 3, + handler: (action: any) => action.params.val * 3, isPublic: true, }, }, @@ -47,16 +43,14 @@ const alpha = { const beta = { moduleAlias: 'betaAlias', - events: ['beta1', 'beta2'].map( - event => new Event(`${alpha.moduleAlias}:${event}`), - ), + events: ['beta1', 'beta2'], actions: { divideByTwo: { - handler: action => action.params / 2, + handler: (action: any) => action.params.val / 2, isPublic: true, }, divideByThree: { - handler: action => action.params / 3, + handler: (action: any) => action.params.val / 3, isPublic: true, }, }, @@ -64,9 +58,9 @@ const beta = { describe('InMemoryChannel', () => { describe('after registering itself to the bus', () => { - let inMemoryChannelAlpha; - let inMemoryChannelBeta; - let bus; + let inMemoryChannelAlpha: InMemoryChannel; + let inMemoryChannelBeta: InMemoryChannel; + let bus: Bus; beforeEach(async () => { // Arrange @@ -98,8 +92,8 @@ describe('InMemoryChannel', () => { describe('#subscribe', () => { it('should be able to subscribe to an event.', async () => { // Arrange - const betaEventData = '#DATA'; - const eventName = beta.events[0].key(); + const betaEventData = { data: '#DATA' }; + const eventName = beta.events[0]; const donePromise = new Promise(resolve => { // Act @@ -123,9 +117,8 @@ describe('InMemoryChannel', () => { it('should be able to subscribe to an event once.', async () => { // Arrange - const betaEventData = '#DATA'; - const eventName = beta.events[0].key(); - + const betaEventData = { data: '#DATA' }; + const eventName = beta.events[0]; const donePromise = new Promise(resolve => { // Act inMemoryChannelAlpha.once( @@ -150,7 +143,7 @@ describe('InMemoryChannel', () => { // Arrange const omegaEventName = 'omegaEventName'; const omegaAlias = 'omegaAlias'; - const dummyData = '#DATA'; + const dummyData = { data: '#DATA' }; const inMemoryChannelOmega = new InMemoryChannel( omegaAlias, [omegaEventName], @@ -183,8 +176,8 @@ describe('InMemoryChannel', () => { describe('#publish', () => { it('should be able to publish an event.', async () => { // Arrange - const alphaEventData = '#DATA'; - const eventName = alpha.events[0].key(); + const alphaEventData = { data: '#DATA' }; + const eventName = alpha.events[0]; const donePromise = new Promise(done => { // Act @@ -211,13 +204,18 @@ describe('InMemoryChannel', () => { it('should be able to invoke its own actions.', async () => { // Act && Assert await expect( - inMemoryChannelAlpha.invoke(`${alpha.moduleAlias}:multiplyByTwo`, 2), + inMemoryChannelAlpha.invoke( + `${alpha.moduleAlias}:multiplyByTwo`, + { val: 2 }, + ), ).resolves.toBe(4); await expect( - inMemoryChannelAlpha.invoke( + inMemoryChannelAlpha.invoke( `${alpha.moduleAlias}:multiplyByThree`, - 4, + { + val: 4, + }, ), ).resolves.toBe(12); }); @@ -225,11 +223,17 @@ describe('InMemoryChannel', () => { it("should be able to invoke other channels' actions.", async () => { // Act && Assert await expect( - inMemoryChannelAlpha.invoke(`${beta.moduleAlias}:divideByTwo`, 4), + inMemoryChannelAlpha.invoke( + `${beta.moduleAlias}:divideByTwo`, + { val: 4 }, + ), ).resolves.toBe(2); await expect( - inMemoryChannelAlpha.invoke(`${beta.moduleAlias}:divideByThree`, 9), + inMemoryChannelAlpha.invoke( + `${beta.moduleAlias}:divideByThree`, + { val: 9 }, + ), ).resolves.toBe(3); }); From 3ba25e34bb38e76d777d8e59aa9f405d1df17920 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 7 May 2020 16:49:27 +0200 Subject: [PATCH 32/35] Fix http api controller loading issue --- framework/src/modules/http_api/init_steps/bootstrap_swagger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/modules/http_api/init_steps/bootstrap_swagger.js b/framework/src/modules/http_api/init_steps/bootstrap_swagger.js index c572093fbd4..7592364519e 100644 --- a/framework/src/modules/http_api/init_steps/bootstrap_swagger.js +++ b/framework/src/modules/http_api/init_steps/bootstrap_swagger.js @@ -204,7 +204,7 @@ function bootstrapSwagger(app, config, logger, scope, cb) { // Load Swagger controllers and bind the scope const controllerFolder = '/controllers/'; fs.readdirSync(config.root + controllerFolder).forEach(file => { - if (path.extname(file) === 'js' && path.basename(file) !== 'index.js') { + if (path.extname(file) === '.js' && path.basename(file) !== 'index.js') { // eslint-disable-next-line import/no-dynamic-require,global-require require(config.root + controllerFolder + file)(scope); } From c89da60adcd8d38e899255333ed64f8dc0326509 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Thu, 7 May 2020 17:05:33 +0200 Subject: [PATCH 33/35] Fix failing unit test --- framework/src/controller/channels/base_channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/controller/channels/base_channel.ts b/framework/src/controller/channels/base_channel.ts index a8eb638ef69..219b7ce5752 100644 --- a/framework/src/controller/channels/base_channel.ts +++ b/framework/src/controller/channels/base_channel.ts @@ -23,9 +23,9 @@ export interface BaseChannelOptions { export abstract class BaseChannel { public readonly eventsList: ReadonlyArray; public readonly actionsList: ReadonlyArray; + public readonly moduleAlias: string; protected readonly actions: ActionsObject; - protected readonly moduleAlias: string; protected readonly options: object; public constructor( From 4f80f77f23a6ba123ff11444978c7a7fe69b69ff Mon Sep 17 00:00:00 2001 From: shuse2 Date: Thu, 7 May 2020 21:38:56 +0200 Subject: [PATCH 34/35] :recycle: Fix unit test and eslint --- framework/src/controller/bus.ts | 5 +- .../channels/child_process_channel.ts | 18 +++---- .../controller/channels/in_memory_channel.ts | 22 +++++---- framework/src/controller/controller.ts | 4 +- .../jest/unit/specs/controller/bus.spec.ts | 17 ++++--- .../controller/channels/base_channel.spec.ts | 41 ++++++++++++---- .../channels/child_process_channel.spec.ts | 47 +++++++++++-------- .../channels/in_memory_channel.spec.ts | 24 +++++----- .../unit/specs/controller/controller.spec.ts | 16 +++---- .../unit/specs/controller/event/constants.ts | 2 +- .../unit/specs/controller/event/event.spec.ts | 2 +- 11 files changed, 116 insertions(+), 82 deletions(-) diff --git a/framework/src/controller/bus.ts b/framework/src/controller/bus.ts index 626a4d5af8c..a2944a1c2eb 100644 --- a/framework/src/controller/bus.ts +++ b/framework/src/controller/bus.ts @@ -157,9 +157,7 @@ export class Bus extends EventEmitter2 { rpcSocket.connect(options.rpcSocketPath); // TODO: Fix this override - // eslint-disable-next-line - // @ts-ignore - channel = new RPCClient(rpcSocket); + channel = (new RPCClient(rpcSocket) as unknown) as BaseChannel; this.rpcClients[moduleAlias] = rpcSocket; } @@ -234,6 +232,7 @@ export class Bus extends EventEmitter2 { } } + // FIXME: Function signature is different that typescript compains // eslint-disable-next-line // @ts-ignore public once(eventName: string, cb: Listener): void { diff --git a/framework/src/controller/channels/child_process_channel.ts b/framework/src/controller/channels/child_process_channel.ts index 8f34d19ca0a..b2c5d76f07e 100644 --- a/framework/src/controller/channels/child_process_channel.ts +++ b/framework/src/controller/channels/child_process_channel.ts @@ -159,10 +159,11 @@ export class ChildProcessChannel extends BaseChannel { const action = new Action(actionName, params, this.moduleAlias); if (action.module === this.moduleAlias) { - // eslint-disable-next-line - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.actions[action.name].handler(action.serialize()); + const handler = this.actions[action.name]?.handler; + if (!handler) { + throw new Error('Handler does not exist.'); + } + return handler(action.serialize()) as T; } return new Promise((resolve, reject) => { @@ -207,11 +208,12 @@ export class ChildProcessChannel extends BaseChannel { `Action ${action.name} is not allowed because it's not public.`, ); } + const handler = this.actions[action.name]?.handler; + if (!handler) { + throw new Error('Handler does not exist.'); + } - // eslint-disable-next-line - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.actions[action.name].handler(action); + return handler(action.serialize()) as T; } return new Promise((resolve, reject) => { diff --git a/framework/src/controller/channels/in_memory_channel.ts b/framework/src/controller/channels/in_memory_channel.ts index 139b31fc297..1f561a6922e 100644 --- a/framework/src/controller/channels/in_memory_channel.ts +++ b/framework/src/controller/channels/in_memory_channel.ts @@ -54,7 +54,6 @@ export class InMemoryChannel extends BaseChannel { (this.bus as Bus).publish(event.key(), event.serialize()); } - // eslint-disable-next-line @typescript-eslint/require-await,@typescript-eslint/no-explicit-any public async invoke(actionName: string, params?: object): Promise { const action = new Action(actionName, params, this.moduleAlias); @@ -65,10 +64,12 @@ export class InMemoryChannel extends BaseChannel { ); } - // eslint-disable-next-line - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.actions[action.name].handler(action.serialize()); + const handler = this.actions[action.name]?.handler; + if (!handler) { + throw new Error('Handler does not exist.'); + } + + return handler(action.serialize()) as T; } return (this.bus as Bus).invoke(action.serialize()); @@ -90,7 +91,6 @@ export class InMemoryChannel extends BaseChannel { return this.invoke(`app:${actionName}`, data); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public async invokePublic( actionName: string, params?: object, @@ -104,10 +104,12 @@ export class InMemoryChannel extends BaseChannel { ); } - // eslint-disable-next-line - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return this.actions[action.name].handler(action.serialize()); + const handler = this.actions[action.name]?.handler; + if (!handler) { + throw new Error('Handler does not exist.'); + } + + return handler(action.serialize()) as T; } return (this.bus as Bus).invokePublic(action.serialize()); diff --git a/framework/src/controller/controller.ts b/framework/src/controller/controller.ts index 5e65d00fe76..9070ef5a8c1 100644 --- a/framework/src/controller/controller.ts +++ b/framework/src/controller/controller.ts @@ -307,14 +307,12 @@ export class Controller { const parameters = [modulePath]; // Avoid child processes and the main process sharing the same debugging ports causing a conflict - const forkedProcessOptions = { + const forkedProcessOptions: { execArgv: string[] | undefined } = { execArgv: undefined, }; const maxPort = 20000; const minPort = 10000; if (process.env.NODE_DEBUG) { - // eslint-disable-next-line - // @ts-ignore forkedProcessOptions.execArgv = [ `--inspect=${Math.floor( Math.random() * (maxPort - minPort) + minPort, diff --git a/framework/test/jest/unit/specs/controller/bus.spec.ts b/framework/test/jest/unit/specs/controller/bus.spec.ts index e6190cd76d9..22df886fc23 100644 --- a/framework/test/jest/unit/specs/controller/bus.spec.ts +++ b/framework/test/jest/unit/specs/controller/bus.spec.ts @@ -50,13 +50,13 @@ describe('Bus', () => { describe('#constructor', () => { it('should create the Bus instance with given arguments.', () => { // Assert - expect(bus.actions).toEqual({}); - expect(bus.events).toEqual({}); + expect(bus['actions']).toEqual({}); + expect(bus['events']).toEqual({}); }); }); describe('#setup', () => { - it('should resolve with true.', () => { + it('should resolve with true.', async () => { return expect(bus.setup()).resolves.toBe(true); }); }); @@ -71,10 +71,9 @@ describe('Bus', () => { await bus.registerChannel(moduleAlias, events, {}, channelOptions); // Assert - expect(Object.keys(bus.events)).toHaveLength(2); - console.info(bus.events); + expect(Object.keys(bus['events'])).toHaveLength(2); events.forEach(eventName => { - expect(bus.events[`${moduleAlias}:${eventName}`]).toBe(true); + expect(bus['events'][`${moduleAlias}:${eventName}`]).toBe(true); }); }); @@ -101,9 +100,9 @@ describe('Bus', () => { await bus.registerChannel(moduleAlias, [], actions, channelOptions); // Assert - expect(Object.keys(bus.actions)).toHaveLength(2); + expect(Object.keys(bus['actions'])).toHaveLength(2); Object.keys(actions).forEach(actionName => { - expect(bus.actions[`${moduleAlias}:${actionName}`]).toBe( + expect(bus['actions'][`${moduleAlias}:${actionName}`]).toBe( actions[actionName], ); }); @@ -224,7 +223,7 @@ describe('Bus', () => { await bus.registerChannel(moduleAlias, events, {}, channelOptions); // Act - bus.publish(eventName, eventData); + bus.publish(eventName, eventData as any); // Assert expect(EventEmitter2.prototype.emit).toHaveBeenCalledWith( diff --git a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.ts b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.ts index 9567c9fceb9..46efc8dd121 100644 --- a/framework/test/jest/unit/specs/controller/channels/base_channel.spec.ts +++ b/framework/test/jest/unit/specs/controller/channels/base_channel.spec.ts @@ -14,6 +14,8 @@ jest.mock('../../../../../../src/controller/action'); +// eslint-disable-next-line import/first +import { EventCallback } from '../../../../../../src/controller/event'; // eslint-disable-next-line import/first import { BaseChannel } from '../../../../../../src/controller/channels'; // eslint-disable-next-line import/first @@ -21,9 +23,32 @@ import { INTERNAL_EVENTS } from '../../../../../../src/controller/constants'; // eslint-disable-next-line import/first import { Action } from '../../../../../../src/controller/action'; -// eslint-disable-next-line -// @ts-ignore -class MyChannel extends BaseChannel {} +class MyChannel extends BaseChannel { + // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function + public subscribe(_eventName: string, _cb: EventCallback): void {} + // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function + public publish(_eventName: string, _data: object): void {} + // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function + public publishToNetwork(_actionName: string, _data: object): void {} + // eslint-disable-next-line class-methods-use-this,@typescript-eslint/require-await + public async invoke(_actionName: string, _params?: object): Promise { + return {} as T; + } + // eslint-disable-next-line class-methods-use-this,@typescript-eslint/require-await + public async invokeFromNetwork( + _actionName: string, + _params?: object, + ): Promise { + return {} as T; + } + // eslint-disable-next-line class-methods-use-this,@typescript-eslint/require-await + public async invokePublic( + _actionName: string, + _params?: object, + ): Promise { + return {} as T; + } +} describe('Base Channel', () => { // Arrange @@ -53,8 +78,8 @@ describe('Base Channel', () => { describe('#constructor', () => { it('should create the instance with given arguments.', () => { // Assert - expect(baseChannel.moduleAlias).toBe(params.moduleAlias); - expect(baseChannel.options).toBe(params.options); + expect(baseChannel['moduleAlias']).toBe(params.moduleAlias); + expect(baseChannel['options']).toBe(params.options); Object.keys(params.actions).forEach(action => { expect(Action).toHaveBeenCalledWith( @@ -71,9 +96,9 @@ describe('Base Channel', () => { describe('getters', () => { it('base.actions should contain list of Action Objects', () => { // Assert - expect(Object.keys(baseChannel.actions)).toHaveLength(3); - Object.keys(baseChannel.actions).forEach(action => { - expect(baseChannel.actions[action]).toBeInstanceOf(Action); + expect(Object.keys(baseChannel['actions'])).toHaveLength(3); + Object.keys(baseChannel['actions']).forEach(action => { + expect(baseChannel['actions'][action]).toBeInstanceOf(Action); }); }); diff --git a/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.ts b/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.ts index 704899c3835..45bcd2f7346 100644 --- a/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.ts +++ b/framework/test/jest/unit/specs/controller/channels/child_process_channel.spec.ts @@ -81,7 +81,7 @@ describe('ChildProcessChannel Channel', () => { }; }); - afterEach(async () => { + afterEach(() => { childProcessChannel.cleanup(); }); @@ -102,7 +102,7 @@ describe('ChildProcessChannel Channel', () => { describe('#registerToBus', () => { beforeEach(async () => childProcessChannel.registerToBus(socketsPath)); - it('should connect pubSocket', async () => { + it('should connect pubSocket', () => { // Assert expect(childProcessChannel.pubSocket?.connect).toHaveBeenCalledWith( socketsPath.sub, @@ -150,8 +150,9 @@ describe('ChildProcessChannel Channel', () => { await childProcessChannel.registerToBus(socketsPath); }); - it('should call localBus.on when the module is the same', async () => { + it('should call localBus.on when the module is the same', () => { // Act + // eslint-disable-next-line @typescript-eslint/no-empty-function childProcessChannel.subscribe(validEventName, () => {}); // Assert expect(childProcessChannel.localBus.on).toHaveBeenCalledWith( @@ -160,11 +161,12 @@ describe('ChildProcessChannel Channel', () => { ); }); - it('should call subSocket.on when the module is not the same', async () => { + it('should call subSocket.on when the module is not the same', () => { // Arrange const invalidEventName = 'invalidModule:anEventName'; // Act + // eslint-disable-next-line @typescript-eslint/no-empty-function childProcessChannel.subscribe(invalidEventName, () => {}); // Assert @@ -178,10 +180,11 @@ describe('ChildProcessChannel Channel', () => { describe('#once', () => { const validEventName = `${params.moduleAlias}:${params.events[0]}`; - beforeEach(() => childProcessChannel.registerToBus(socketsPath)); + beforeEach(async () => childProcessChannel.registerToBus(socketsPath)); - it('should call localBus.once when the module is the same', async () => { + it('should call localBus.once when the module is the same', () => { // Act + // eslint-disable-next-line @typescript-eslint/no-empty-function childProcessChannel.once(validEventName, () => {}); // Assert expect(childProcessChannel.localBus.once).toHaveBeenCalledWith( @@ -190,11 +193,12 @@ describe('ChildProcessChannel Channel', () => { ); }); - it('should call subSocket.on when the module is not the same', async () => { + it('should call subSocket.on when the module is not the same', () => { // Arrange const invalidEventName = 'invalidModule:anEventName'; // Act + // eslint-disable-next-line @typescript-eslint/no-empty-function childProcessChannel.once(invalidEventName, () => {}); // Assert @@ -209,20 +213,21 @@ describe('ChildProcessChannel Channel', () => { const validEventName = `${params.moduleAlias}:${params.events[0]}`; const invalidEventName = 'invalidModule:anEventName'; - beforeEach(() => + beforeEach(async () => { // Arrange - childProcessChannel.registerToBus(socketsPath), - ); + await childProcessChannel.registerToBus(socketsPath); + }); - it('should throw new Error when the module is not the same', async () => { + it('should throw new Error when the module is not the same', () => { expect(() => + // eslint-disable-next-line @typescript-eslint/no-empty-function childProcessChannel.publish(invalidEventName, () => {}), ).toThrow( `Event "${invalidEventName}" not registered in "${params.moduleAlias}" module.`, ); }); - it('should call localBus.emit with proper arguments', async () => { + it('should call localBus.emit with proper arguments', () => { // Arrange const data = { data: '#DATA' }; const event = new Event(validEventName, data); @@ -237,7 +242,7 @@ describe('ChildProcessChannel Channel', () => { ); }); - it('should call pubSocket.emit with proper arguments', async () => { + it('should call pubSocket.emit with proper arguments', () => { // Arrange const data = { data: '#DATA' }; const event = new Event(validEventName, data); @@ -367,11 +372,13 @@ describe('ChildProcessChannel Channel', () => { }); describe('#_rejectWhenAnySocketFailsToBind', () => { - beforeEach(() => childProcessChannel.registerToBus(socketsPath)); + beforeEach(async () => { + await childProcessChannel.registerToBus(socketsPath); + }); - it('should reject if any of the sockets receive an "error" event', () => { + it('should reject if any of the sockets receive an "error" event', async () => { // Assert - return expect( + await expect( (childProcessChannel as any)._rejectWhenAnySocketFailsToBind(), ).rejects.toBe('#MOCKED_ONCE'); }); @@ -414,11 +421,13 @@ describe('ChildProcessChannel Channel', () => { }); describe('#_rejectWhenTimeout', () => { - beforeEach(() => childProcessChannel.registerToBus(socketsPath)); + beforeEach(async () => { + await childProcessChannel.registerToBus(socketsPath); + }); - it('should reject with an Error object with proper message', () => { + it('should reject with an Error object with proper message', async () => { // Assert - return expect( + await expect( (childProcessChannel as any)._rejectWhenTimeout(1), ).rejects.toThrow('ChildProcessChannel sockets setup timeout'); }); diff --git a/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.ts b/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.ts index 6cb953100b9..2a383963670 100644 --- a/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.ts +++ b/framework/test/jest/unit/specs/controller/channels/in_memory_channel.spec.ts @@ -80,13 +80,13 @@ describe('InMemoryChannel Channel', () => { await inMemoryChannel.registerToBus(bus); // Assert - expect(inMemoryChannel.bus).toBe(bus); + expect(inMemoryChannel['bus']).toBe(bus); expect( - inMemoryChannel.bus?.registerChannel, + inMemoryChannel['bus']?.registerChannel, ).toHaveBeenCalledWith( - inMemoryChannel.moduleAlias, + inMemoryChannel['moduleAlias'], inMemoryChannel.eventsList, - inMemoryChannel.actions, + inMemoryChannel['actions'], { type: 'inMemory', channel: inMemoryChannel }, ); }); @@ -104,10 +104,11 @@ describe('InMemoryChannel Channel', () => { await inMemoryChannel.registerToBus(bus); // Act + // eslint-disable-next-line @typescript-eslint/no-empty-function inMemoryChannel.once(eventName, () => {}); // Assert - expect(inMemoryChannel.bus?.once).toHaveBeenCalledWith( + expect(inMemoryChannel['bus']?.once).toHaveBeenCalledWith( event.key(), expect.any(Function), ); @@ -127,10 +128,11 @@ describe('InMemoryChannel Channel', () => { // Act await inMemoryChannel.registerToBus(bus); + // eslint-disable-next-line @typescript-eslint/no-empty-function inMemoryChannel.once(eventName, () => {}); // Assert - expect(inMemoryChannel.bus?.once).toHaveBeenCalledWith( + expect(inMemoryChannel['bus']?.once).toHaveBeenCalledWith( event.key(), expect.any(Function), ); @@ -152,13 +154,13 @@ describe('InMemoryChannel Channel', () => { expect(() => { inMemoryChannel.publish(eventName); }).toThrow( - `Event "${eventName}" not registered in "${inMemoryChannel.moduleAlias}" module.`, + `Event "${eventName}" not registered in "${inMemoryChannel['moduleAlias']}" module.`, ); }); it('should call bus.publish if the event module is equal to moduleAlias', async () => { // Arrange - const eventFullName = `${inMemoryChannel.moduleAlias}:eventName`; + const eventFullName = `${inMemoryChannel['moduleAlias']}:eventName`; const event = new Event(eventFullName); // Act @@ -166,7 +168,7 @@ describe('InMemoryChannel Channel', () => { inMemoryChannel.publish(eventFullName); // Assert - expect(inMemoryChannel.bus?.publish).toHaveBeenCalledWith( + expect(inMemoryChannel['bus']?.publish).toHaveBeenCalledWith( event.key(), event.serialize(), ); @@ -180,7 +182,7 @@ describe('InMemoryChannel Channel', () => { it('should execute the action straight away if the action module is equal to moduleAlias', async () => { // Arrange - const actionFullName = `${inMemoryChannel.moduleAlias}:${actionName}`; + const actionFullName = `${inMemoryChannel['moduleAlias']}:${actionName}`; // Act await inMemoryChannel.invoke(actionFullName); @@ -198,7 +200,7 @@ describe('InMemoryChannel Channel', () => { await inMemoryChannel.invoke(actionFullName); // Assert - expect(inMemoryChannel.bus?.invoke).toHaveBeenCalled(); + expect(inMemoryChannel['bus']?.invoke).toHaveBeenCalled(); }); }); }); diff --git a/framework/test/jest/unit/specs/controller/controller.spec.ts b/framework/test/jest/unit/specs/controller/controller.spec.ts index 5b12319b893..05888c9bbde 100644 --- a/framework/test/jest/unit/specs/controller/controller.spec.ts +++ b/framework/test/jest/unit/specs/controller/controller.spec.ts @@ -22,7 +22,7 @@ import * as fs from 'fs-extra'; import { Controller } from '../../../../../src/controller/controller'; import { Bus } from '../../../../../src/controller/bus'; -const createMockModule = (alias: string, loadStub?: any, unloadStub?: any) => { +const createMockModule = (alias?: string, loadStub?: any, unloadStub?: any) => { function Module(this: any) { this.load = loadStub ?? jest.fn(); this.unload = unloadStub ?? jest.fn(); @@ -32,7 +32,7 @@ const createMockModule = (alias: string, loadStub?: any, unloadStub?: any) => { } Module.info = { - name: alias || 'dummy', + name: alias ?? 'dummy', version: 'dummy', author: 'dummy', }; @@ -143,7 +143,7 @@ describe('Controller Class', () => { }); describe('_setupDirectories', () => { - it('should ensure directory exists', async () => { + it('should ensure directory exists', () => { // Arrange jest.spyOn(fs, 'ensureDir'); @@ -157,13 +157,11 @@ describe('Controller Class', () => { }); describe('_validatePidFile', () => { - it('should write process id to pid file if pid file not exits', async () => { - // eslint-disable-next-line - // @ts-ignore - jest.spyOn(fs, 'pathExists').mockResolvedValue(false); + it('should write process id to pid file if pid file not exits', () => { + jest.spyOn(fs, 'pathExists').mockResolvedValue(false as never); jest.spyOn(fs, 'writeFile'); - expect(fs.writeFile).toBeCalledWith( + expect(fs.writeFile).toHaveBeenCalledWith( `${controller.config.dirs.pids}/controller.pid`, expect.toBeNumber(), ); @@ -231,7 +229,7 @@ describe('Controller Class', () => { }); }); - it('should log registered events and actions', async () => { + it('should log registered events and actions', () => { // Assert expect(logger.debug).toHaveBeenCalledWith( undefined, diff --git a/framework/test/jest/unit/specs/controller/event/constants.ts b/framework/test/jest/unit/specs/controller/event/constants.ts index 0e5fc1f7b08..0abc35f749c 100644 --- a/framework/test/jest/unit/specs/controller/event/constants.ts +++ b/framework/test/jest/unit/specs/controller/event/constants.ts @@ -16,4 +16,4 @@ export const MODULE_NAME = 'module'; export const EVENT_NAME = 'event'; export const VALID_EVENT_NAME_ARG = `${MODULE_NAME}:${EVENT_NAME}`; export const INVALID_EVENT_NAME_ARG = `${MODULE_NAME}`; -export const DATA = '#data'; +export const DATA = { data: '#data' }; diff --git a/framework/test/jest/unit/specs/controller/event/event.spec.ts b/framework/test/jest/unit/specs/controller/event/event.spec.ts index 1291cec2eb7..7fa19a0322c 100644 --- a/framework/test/jest/unit/specs/controller/event/event.spec.ts +++ b/framework/test/jest/unit/specs/controller/event/event.spec.ts @@ -119,7 +119,7 @@ describe('Event Class', () => { expect(event).toBeInstanceOf(Event); expect(event.module).toBe(MODULE_NAME); expect(event.name).toBe(EVENT_NAME); - expect(event.data).toBe(DATA); + expect(event.data).toStrictEqual(DATA); }); it('should return event instance with given object config.', () => { From bb5bd804cf4e38caf1eec38ea0094fc52585589f Mon Sep 17 00:00:00 2001 From: shuse2 Date: Fri, 8 May 2020 19:17:50 +0200 Subject: [PATCH 35/35] :recycle: Add mocha ts-node setup --- framework/test/mocha/mocha.opts | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/test/mocha/mocha.opts b/framework/test/mocha/mocha.opts index 81684a175d2..5a75733c933 100644 --- a/framework/test/mocha/mocha.opts +++ b/framework/test/mocha/mocha.opts @@ -2,4 +2,5 @@ --exit --colors --require test/mocha/setup.js +--require ts-node/register --recursive