keymetrics/pm2-io-apm

View on GitHub
src/services/actions.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import { ServiceManager } from '../serviceManager'
import { Transport } from './transport'
import * as Debug from 'debug'

export class Action {
  handler: Function
  name: string
  type: string
  isScoped: boolean
  callback?: Function
  arity: number
  opts: Object | null | undefined
}

export class ActionService {

  private timer: NodeJS.Timer | undefined = undefined
  private transport: Transport | undefined = undefined
  private actions: Map<string, Action> = new Map<string, Action>()
  private logger: Function = Debug('axm:services:actions')

  private listener (data) {
    this.logger(`Received new message from reverse`)
    if (!data) return false

    const actionName = data.msg ? data.msg : data.action_name ? data.action_name : data
    let action = this.actions.get(actionName)
    if (typeof action !== 'object') {
      return this.logger(`Received action ${actionName} but failed to find the implementation`)
    }

    // handle normal custom action
    if (!action.isScoped) {
      this.logger(`Succesfully called custom action ${action.name} with arity ${action.handler.length}`)
      // In case 2 arguments has been set but no options has been transmitted
      if (action.handler.length === 2) {
        let params = {}
        if (typeof data === 'object') {
          params = data.opts
        }
        return action.handler(params, action.callback)
      }
      return action.handler(action.callback)
    }

    // handle scoped actions
    if (data.uuid === undefined) {
      return this.logger(`Received scoped action ${action.name} but without uuid`)
    }

    // create a simple object that represent a stream
    const stream = {
      send : (dt) => {
        // @ts-ignore thanks mr typescript but i already checked above
        this.transport.send('axm:scoped_action:stream', {
          data: dt,
          uuid: data.uuid,
          action_name: actionName
        })
      },
      error : (dt) => {
        // @ts-ignore thanks mr typescript but i already checked above
        this.transport.send('axm:scoped_action:error', {
          data: dt,
          uuid: data.uuid,
          action_name: actionName
        })
      },
      end : (dt) => {
        // @ts-ignore thanks mr typescript but i already checked above
        this.transport.send('axm:scoped_action:end', {
          data: dt,
          uuid: data.uuid,
          action_name: actionName
        })
      }
    }

    this.logger(`Succesfully called scoped action ${action.name}`)
    return action.handler(data.opts || {}, stream)
  }

  init (): void {
    this.transport = ServiceManager.get('transport')
    // tslint:disable-next-line
    if (this.transport === undefined) {
      return this.logger(`Failed to load transport service`)
    }
    this.actions.clear()
    this.transport.on('data', this.listener.bind(this))
  }

  destroy (): void {
    if (this.timer !== undefined) {
      clearInterval(this.timer)
    }
    // tslint:disable-next-line
    if (this.transport !== undefined) {
      this.transport.removeListener('data', this.listener.bind(this))
    }
  }

  /**
   * Register a custom action that will be called when we receive a call for this actionName
   */
  registerAction (actionName?: string, opts?: Object | undefined | Function, handler?: Function): void {
    if (typeof opts === 'function') {
      handler = opts
      opts = undefined
    }

    if (typeof actionName !== 'string') {
      console.error(`You must define an name when registering an action`)
      return
    }
    if (typeof handler !== 'function') {
      console.error(`You must define an callback when registering an action`)
      return
    }
    if (this.transport === undefined) {
      return this.logger(`Failed to load transport service`)
    }

    let type = 'custom'

    if (actionName.indexOf('km:') === 0 || actionName.indexOf('internal:') === 0) {
      type = 'internal'
    }

    const reply = (data) => {
      // @ts-ignore thanks mr typescript but i already checked above
      this.transport.send('axm:reply', {
        at: new Date().getTime(),
        action_name: actionName,
        return: data
      })
    }

    const action: Action = {
      name: actionName,
      callback: reply,
      handler,
      type,
      isScoped: false,
      arity: handler.length,
      opts
    }
    this.logger(`Succesfully registered custom action ${action.name}`)
    this.actions.set(actionName, action)
    this.transport.addAction(action)
  }

  /**
   * Register a scoped action that will be called when we receive a call for this actionName
   */
  scopedAction (actionName?: string, handler?: Function) {
    if (typeof actionName !== 'string') {
      console.error(`You must define an name when registering an action`)
      return -1
    }
    if (typeof handler !== 'function') {
      console.error(`You must define an callback when registering an action`)
      return -1
    }
    if (this.transport === undefined) {
      return this.logger(`Failed to load transport service`)
    }

    const action: Action = {
      name: actionName,
      handler,
      type: 'scoped',
      isScoped: true,
      arity: handler.length,
      opts: null
    }
    this.logger(`Succesfully registered scoped action ${action.name}`)
    this.actions.set(actionName, action)
    this.transport.addAction(action)
  }
}