senecajs/seneca

View on GitHub
lib/common.ts

Summary

Maintainability
D
2 days
Test Coverage
/* Copyright © 2010-2022 Richard Rodger and other contributors, MIT License. */
'use strict'


import Util from 'util'

import Stringify from 'fast-safe-stringify'
import Jsonic from '@jsonic/jsonic-next'
import Nid from 'nid'

const Eraro = require('eraro')

const DefaultsDeep = require('lodash.defaultsdeep')
const { Print } = require('./print')

import Errors from './errors'



const error =
  (exports.error =
    exports.eraro =
    Eraro({
      package: 'seneca',
      msgmap: Errors,
      override: true,
    }))


function pins(inpin: any): Record<string, any>[] {
  return (Array.isArray(inpin) ? inpin : [inpin])
    .reduce((a: any, p: any) => (a.push(
      'string' === typeof p ? p.split(';').map(pp => Jsonic(pp)) : p
    ), a), [])
    .filter((n: any) => null != n)
    .flat()
}


function promiser(context: any, callback?: any) {
  if ('function' === typeof context && null == callback) {
    callback = context
  } else {
    callback = callback.bind(context)
  }

  return new Promise((resolve, reject) => {
    callback((err: any, out: any) => {
      return err ? reject(err) : resolve(out)
    })
  })
}


function stringify() {
  return (Stringify as any)(...arguments)
}


function wrap_error(err: any) {
  if (err.seneca) {
    throw err
  } else {
    throw error.call(null, ...arguments)
  }
}


function make_plugin_key(plugin: any, origtag: any) {
  if (null == plugin) {
    throw error('missing_plugin_name')
  }

  let name = null == plugin.name ? plugin : plugin.name
  let tag = null == plugin.tag ? (null == origtag ? '' : origtag) : plugin.tag

  if ('number' === typeof name) {
    name = '' + name
  }

  if ('number' === typeof tag) {
    tag = '' + tag
  }

  if ('' == name || 'string' !== typeof name) {
    throw error('bad_plugin_name', { name: name })
  }

  let m = name.match(/^([a-zA-Z@][a-zA-Z0-9.~_\-/]*)\$([a-zA-Z0-9.~_-]+)$/)
  if (m) {
    name = m[1]
    tag = m[2]
  }

  // Allow file paths, but ...
  if (!name.match(/^(\.|\/|\\|\w:)/)) {
    // ... anything else should be well-formed
    if (!name.match(/^[a-zA-Z@][a-zA-Z0-9.~_\-/]*$/) || 1024 < name.length) {
      throw error('bad_plugin_name', { name: name })
    }
  }

  if ('' != tag && (!tag.match(/^[a-zA-Z0-9.~_-]+$/) || 1024 < tag.length)) {
    throw error('bad_plugin_tag', { tag: tag })
  }

  let key = name + (tag ? '$' + tag : '')

  return key
}


const tagnid = Nid({ length: 3, alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' })


function parse_jsonic(str: any, code: any) {
  code = code || 'bad_jsonic'

  try {
    return null == str ? null : Jsonic(str)
  }
  catch (e: any) {
    throw error(code, {
      argstr: str,
      syntax: e.message,
      line: e.lineNumber,
      col: e.columnNumber,
    })
  }
}


// Convert pattern object into a normalized jsonic String.
function pattern(patobj: any) {
  if ('string' === typeof patobj) {
    return patobj
  }

  patobj = patobj || {}
  let sb: any = []

  Object.keys(patobj).forEach((k) => {
    let v = patobj[k]
    if (!~k.indexOf('$') && 'function' != typeof v && 'object' != typeof v) {
      sb.push(k + ':' + v)
    }
  })

  sb.sort()

  return sb.join(',')
}


function pincanon(inpin: any) {
  if ('string' == typeof inpin) {
    return pattern(Jsonic(inpin))
  } else if (Array.isArray(inpin)) {
    let pin: any = inpin.map(pincanon)
    pin.sort()
    return pin.join(';')
  } else {
    return pattern(inpin)
  }
}


function noop() { }


// remove any props containing $
function clean(obj: any, opts?: any) {
  if (null == obj) return obj

  let out: any = Array.isArray(obj) ? [] : {}

  let pn = Object.getOwnPropertyNames(obj)
  for (let i = 0; i < pn.length; i++) {
    let p = pn[i]

    if ('$' != p[p.length - 1]) {
      out[p] = obj[p]
    }
  }

  return out
}


// rightmost wins
function deep(...argsarr: any) {
  // Lodash uses the reverse order to apply defaults than the deep API.
  argsarr = argsarr.reverse()

  // Add an empty object to the front of the args.  Defaults will be written
  // to this empty object.
  argsarr.unshift({})

  return DefaultsDeep.apply(null, argsarr)
}


// Print action result
const print = Print.print


// Iterate over arrays or objects
function each(collect: any, func: any) {
  if (null == collect || null == func) {
    return null
  }

  if (Array.isArray(collect)) {
    return collect.forEach(func)
  } else {
    Object.keys(collect).forEach((k) => func(collect[k], k))
  }
}


function makedie(instance: any, ctxt: any) {
  ctxt = Object.assign(ctxt, instance.die ? instance.die.context : {})

  let diecount = 0

  let die = function(this: any, err: any) {
    let so = instance.options()
    let test = so.test

    // undead is only for testing, do not use in production
    let undead = (so.debug && so.debug.undead) || (err && err.undead)
    let full =
      (so.debug && so.debug.print && 'full' === so.debug.print.fatal) || false
    let print_env = (so.debug && so.debug.print.env) || false

    if (0 < diecount) {
      if (!undead) {
        throw error(err, '[DEATH LOOP] die count: ' + diecount)
      }
      return
    } else {
      diecount++
    }

    try {
      if (!err) {
        err = new Error('unknown')
      } else if (!so.error.identify(err)) {
        err = new Error('string' === typeof err ? err : inspect(err))
      }

      err.fatal$ = true

      let logdesc = {
        kind: ctxt.txt || 'fatal',
        level: ctxt.level || 'fatal',
        plugin: ctxt.plugin,
        tag: ctxt.tag,
        id: ctxt.id,
        code: err.code || 'fatal',
        notice: err.message,
        err: err,
        callpoint: ctxt.callpoint && ctxt.callpoint(),
      }

      instance.log.fatal(logdesc)

      let stack = err.stack || ''
      stack = stack
        .substring(stack.indexOf('\n') + 5)
        .replace(/\n\s+/g, '\n               ')

      let procdesc =
        'pid=' +
        process.pid +
        ', arch=' +
        process.arch +
        ', platform=' +
        process.platform +
        (!full ? '' : ', path=' + process.execPath) +
        ', argv=' +
        inspect(process.argv).replace(/\n/g, '') +
        (!full
          ? ''
          : !print_env
            ? ''
            : ', env=' + inspect(process.env).replace(/\n/g, ''))

      let when = new Date()

      let clean_details = null

      let stderrmsg =
        '\n\n' +
        '=== SENECA FATAL ERROR ===' +
        '\nMESSAGE   :::  ' +
        err.message +
        '\nCODE      :::  ' +
        err.code +
        '\nINSTANCE  :::  ' +
        instance.toString() +
        '\nDETAILS   :::  ' +
        inspect(
          full
            ? err.details
            : ((clean_details = clean(err.details) || {}),
              delete clean_details.instance,
              clean_details),
          { depth: so.debug.print.depth }
        ).replace(/\n/g, '\n               ') +
        '\nSTACK     :::  ' +
        stack +
        '\nWHEN      :::  ' +
        when.toISOString() +
        ', ' +
        when.getTime() +
        '\nLOG       :::  ' +
        jsonic_stringify(logdesc) +
        '\nNODE      :::  ' +
        process.version +
        ', ' +
        process.title +
        (!full
          ? ''
          : ', ' +
          inspect(process.versions).replace(/\s+/g, ' ') +
          ', ' +
          inspect(process.features).replace(/\s+/g, ' ') +
          ', ' +
          inspect((process as any).moduleLoadList).replace(/\s+/g, ' ')) +
        '\nPROCESS   :::  ' +
        procdesc +
        '\nFOLDER    :::  ' +
        process.env.PWD

      if (so.errhandler) {
        so.errhandler.call(instance, err)
      }

      if (instance.flags.closed) {
        return
      }

      if (!undead) {
        instance.act('role:seneca,info:fatal,closing$:true', { err: err })

        instance.close(
          // terminate process, err (if defined) is from seneca.close
          function(close_err: any) {
            if (!undead) {
              process.nextTick(function() {
                if (close_err) {
                  instance.log.fatal({
                    kind: 'close',
                    err: inspect(close_err),
                  })
                }

                if (test) {
                  if (close_err) {
                    Print.internal_err(close_err)
                  }

                  Print.internal_err(stderrmsg)
                  Print.internal_err(
                    '\nSENECA TERMINATED at ' +
                    new Date().toISOString() +
                    '. See above for error report.\n'
                  )
                }

                so.system.exit(1)
              })
            }
          }
        )
      }

      // make sure we close down within options.death_delay seconds
      if (!undead) {
        let killtimer = setTimeout(function() {
          instance.log.fatal({ kind: 'close', timeout: true })

          if (so.test) {
            Print.internal_err(stderrmsg)
            Print.internal_err(
              '\n\nSENECA TERMINATED (on timeout) at ' +
              new Date().toISOString() +
              '.\n\n'
            )
          }

          so.system.exit(2)
        }, so.death_delay)

        if (killtimer.unref) {
          killtimer.unref()
        }
      }
    } catch (panic: any) {
      this.log.fatal({
        kind: 'panic',
        panic: inspect(panic),
        orig: arguments[0],
      })

      if (so.test) {
        let msg =
          '\n\n' +
          'Seneca Panic\n' +
          '============\n\n' +
          panic.stack +
          '\n\nOriginal Error:\n' +
          (arguments[0] && arguments[0].stack
            ? arguments[0].stack
            : arguments[0])
        Print.internal_err(msg)
      }
    }
  }

    ; (die as any).context = ctxt

  return die
}

function make_standard_act_log_entry(
  actdef: any,
  msg: any,
  meta: any,
  origmsg: any,
  ctxt: any
) {
  let transport = origmsg.transport$ || {}
  let callmeta = meta || msg.meta$ || {}
  let prior = callmeta.prior || {}
  actdef = actdef || {}

  return Object.assign(
    {
      actid: callmeta.id,
      msg: msg,
      meta: meta,
      entry: prior.entry,
      prior: prior.chain,
      gate: origmsg.gate$,
      caller: origmsg.caller$,
      actdef: actdef,

      // these are transitional as need to be updated
      // to standard transport metadata
      client: actdef.client,
      listen: !!transport.origin,
      transport: transport,
    },
    ctxt
  )
}

function make_standard_err_log_entry(err: any, ctxt: any) {
  if (!err) return ctxt

  if (err.details && ctxt && ctxt.caller) {
    err.details.caller = ctxt.caller
  }

  let entry = Object.assign(
    {
      notice: err.message,
      code: err.code,
      err: err,
    },
    ctxt
  )

  return entry
}


function resolve_option(value: any, options: any) {
  return 'function' === typeof value ? value(options) : value
}


function autoincr() {
  let counter = 0
  return function() {
    return counter++
  }
}


function inspect(val: any, opts?: any) {
  return Util.inspect(val, opts)
}


// Callpoint resolver. Indicates location in calling code.
function make_callpoint(active: any) {
  return function callpoint(override: any) {
    if (active || override) {
      return error.callpoint(new Error(), [
        '/ordu.js',
        '/seneca/seneca.js',
        '/seneca/lib/',
        '/lodash.js',
      ])
    } else {
      return void 0
    }
  }
}


function make_trace_desc(meta: any) {
  return [
    meta.pattern,
    meta.id,
    meta.instance,
    meta.tag,
    meta.version,
    meta.start,
    meta.end,
    meta.sync,
    meta.action,
  ]
}


// Stringify message for logs, debugging and errors. 
function msgstr(msg: any, len: number = 111): string {
  let str =
    inspect(clean(msg))
      .replace(/\n/g, '')

  str = str.substring(0, len) +
    (len < str.length ? '...' : '')

  return str
}


function jsonic_strify(val: any, opts: any, depth: number) {
  depth++
  if (null == val) return 'null'

  var type = Object.prototype.toString.call(val).charAt(8)
  if ('F' === type && !opts.showfunc) return null

  // WARNING: output may not be jsonically parsable!
  if (opts.custom) {
    if (Object.prototype.hasOwnProperty.call(val, 'toString')) {
      return val.toString()
    }
    else if (Object.prototype.hasOwnProperty.call(val, 'inspect')) {
      return val.inspect()
    }
  }


  var out, i = 0, j, k

  if ('N' === type) {
    return isNaN(val) ? 'null' : val.toString()
  }
  else if ('O' === type) {
    out = []
    if (depth <= opts.depth) {
      j = 0
      for (let i in val) {
        if (j >= opts.maxitems) break

        var pass = true
        for (k = 0; k < opts.exclude.length && pass; k++) {
          pass = !~i.indexOf(opts.exclude[k])
        }
        pass = pass && !opts.omit[i]

        let str: string = jsonic_strify(val[i], opts, depth)

        if (null != str && pass) {
          var n = i.match(/^[a-zA-Z0-9_$]+$/) ? i : JSON.stringify(i)
          out.push(n + ':' + str)
          j++
        }
      }
    }
    return '{' + out.join(',') + '}'
  }
  else if ('A' === type) {
    out = []
    if (depth <= opts.depth) {
      for (; i < val.length && i < opts.maxitems; i++) {
        let str: string = jsonic_strify(val[i], opts, depth)
        if (null != str) {
          out.push(str)
        }
      }
    }
    return '[' + out.join(',') + ']'
  }
  else {
    var valstr = val.toString()

    if (~" \"'\r\n\t,}]".indexOf(valstr[0]) ||
      !~valstr.match(/,}]/) ||
      ~" \r\n\t".indexOf(valstr[valstr.length - 1])) {
      valstr = "'" + valstr.replace(/'/g, "\\'") + "'"
    }

    return valstr
  }
}



// Legacy Jsonic stringify
function jsonic_stringify(val: any, callopts?: any) {
  try {
    callopts = callopts || {}
    var opts: any = {}

    opts.showfunc = callopts.showfunc || callopts.f || false
    opts.custom = callopts.custom || callopts.c || false
    opts.depth = callopts.depth || callopts.d || 3
    opts.maxitems = callopts.maxitems || callopts.mi || 11
    opts.maxchars = callopts.maxchars || callopts.mc || 111
    opts.exclude = callopts.exclude || callopts.x || ['$']
    var omit = callopts.omit || callopts.o || []

    opts.omit = {}
    for (var i = 0; i < omit.length; i++) {
      opts.omit[omit[i]] = true;
    }

    var str: string = jsonic_strify(val, opts, 0)
    str = null == str ? '' : str.substring(0, opts.maxchars)
    return str
  }
  catch (e) {
    return 'ERROR: jsonic_stringify: ' + e + ' input was: ' + JSON.stringify(val)
  }
}


const TRACE_PATTERN = 0
const TRACE_ID = 1
const TRACE_INSTANCE = 2
const TRACE_TAG = 3
const TRACE_VERSION = 4
const TRACE_START = 5
const TRACE_END = 6
const TRACE_SYNC = 7
const TRACE_ACTION = 8


function history(opts: any) {
  return new ActHistory(opts)
}


class ActHistory {

  _total: number = 0
  _list: any = []
  _map: any = {}
  _prune_interval: any


  constructor(opts: any) {
    let self = this
    opts = opts || {}

    if (opts.prune) {
      this._prune_interval = setInterval(function() {
        self.prune(Date.now())
      }, opts.interval || 100)
      if (this._prune_interval.unref) {
        this._prune_interval.unref()
      }
    }
  }

  stats(this: any) {
    return {
      total: this._total,
    }
  }

  add(this: any, obj: any) {
    this._map[obj.id] = obj

    let i = this._list.length - 1

    if (i < 0 || this._list[i].timelimit <= obj.timelimit) {
      this._list.push(obj)
    } else {
      i = this.place(obj.timelimit)
      this._list.splice(i, 0, obj)
    }
  }

  place(this: any, timelimit: any) {
    let i = this._list.length
    let s = 0
    let e = i

    if (0 === this._list.length) {
      return 0
    }

    do {
      i = Math.floor((s + e) / 2)

      if (timelimit > this._list[i].timelimit) {
        s = i + 1
        i = s
      } else if (timelimit < this._list[i].timelimit) {
        e = i
      } else {
        i++
        break
      }
    } while (s < e)

    return i
  }

  prune(this: any, timelimit: any) {
    let i = this.place(timelimit)
    if (0 <= i && i <= this._list.length) {
      for (let j = 0; j < i; j++) {
        delete this._map[this._list[j].id]
      }
      this._list = this._list.slice(i)
    }
  }

  get(this: any, id: any) {
    return this._map[id] || null
  }

  list(this: any) {
    return this._list
  }

  close(this: any) {
    if (this._prune_interval) {
      clearInterval(this._prune_interval)
    }
  }

  toString(this: any) {
    return inspect({
      total: this._total,
      map: this._map,
      list: this._list,
    })
  }

  [Util.inspect.custom](this: any) {
    return this.toString()
  }
}


export {
  pins,
  promiser,
  stringify,
  wrap_error,
  make_plugin_key,
  parse_jsonic,
  pattern,
  pincanon,
  noop,
  clean,
  deep,
  each,
  makedie,
  make_standard_act_log_entry,
  make_standard_err_log_entry,
  resolve_option,
  autoincr,
  make_callpoint,
  make_trace_desc,
  history,
  print,
  tagnid,
  inspect,
  error,
  msgstr,
  jsonic_stringify,
  TRACE_PATTERN,
  TRACE_ID,
  TRACE_INSTANCE,
  TRACE_TAG,
  TRACE_VERSION,
  TRACE_START,
  TRACE_END,
  TRACE_SYNC,
  TRACE_ACTION,
}