2021-03-11 16:24:56 -08:00
/ * *
* UCI Base Module
* @ module UCI - Base
* @ description This is the primary module to use in the UCI ecosystem
* /
2020-01-06 23:39:29 -08:00
// TODO add automated duplex (for consumer will create corresponding socket and send connection info to remote socket)
2018-05-16 11:17:38 -07:00
2020-01-06 23:39:29 -08:00
// --------------- UCI dependencies -------------------
// UCI communication transport communication modules
2021-04-05 17:35:57 -07:00
// TODO change to dynamic import so loads only if that connector type is requested
2019-01-01 16:39:08 -08:00
import Socket from '@uci/socket' // tcp or named pipe
2021-04-05 17:35:57 -07:00
2019-01-01 16:39:08 -08:00
import MQTT from '@uci/mqtt' // requires broker
import WebSocket from '@uci/websocket' // server only - client is for web browser only
2021-04-05 17:35:57 -07:00
2020-01-06 23:39:29 -08:00
// UCI helpers
import { Ready , changed , map } from '@uci-utils/ready'
2021-04-05 17:35:57 -07:00
// import { isClass } from '@uci-utils/type'
2019-02-14 14:01:21 -08:00
import { bindFuncs } from '@uci-utils/bind-funcs'
2020-01-06 23:39:29 -08:00
// UCI logger
2019-02-14 14:01:21 -08:00
import logger from '@uci-utils/logger'
2019-01-01 16:39:08 -08:00
let log = { } // declare module wide log to be set during construction
2020-01-06 23:39:29 -08:00
// -------------------------------------------------
2019-01-01 16:39:08 -08:00
// Community dependencies
2019-03-17 13:45:19 -07:00
import to from 'await-to-js'
2020-01-06 23:39:29 -08:00
import isPlainObject from 'is-plain-object'
2021-04-05 17:35:57 -07:00
import loadYaml from 'load-yaml-file'
2020-03-15 15:15:43 -07:00
import { merge } from 'merge-anything'
2020-01-06 23:39:29 -08:00
// Nodejs dependencies
2018-01-18 21:32:07 -08:00
import EventEmitter from 'events'
2018-01-27 23:20:33 -08:00
2019-01-01 16:39:08 -08:00
// Internal dependencies
2020-01-06 23:39:29 -08:00
import { cmdProcessor , defaultCmds , namespaces } from './processing'
2018-02-05 22:05:38 -08:00
2019-09-16 18:05:03 -07:00
// Constants
2021-04-05 17:35:57 -07:00
// TODO transport will become "connector"
// type will be input or output, duplex
// TODO make sure reach registered connector has unique name
2020-03-15 15:15:43 -07:00
// const SOCKET_INFO_KEYS = ['name', 'type', 'transport']
2021-04-05 17:35:57 -07:00
// TODO remove this hard-code and get these from plugin
2019-01-01 16:39:08 -08:00
const TRANSLATE = {
n : 'Named Pipe' ,
t : 'TCP' ,
s : 'Socket' ,
c : 'Consumer' ,
m : 'MQTT' ,
2021-04-05 17:35:57 -07:00
w : 'Web Socket' ,
2019-01-01 16:39:08 -08:00
}
/ * *
* @ class Base
* @ description
* An inter - process inter - machine multi socket communication class . < / b r >
* It is extended to derive many other UCI classes thus the name "Base" < / b r >
* The class itself is extended from the { @ link https : //nodejs.org/api/events.html#events_class_eventemitter nodejs EventEmitter } to support in process communication as well
*
* @ extends EventEmitter
2021-03-11 16:24:56 -08:00
* @ param options { object } - object hash of options
* @ param [ options . id = ucit - base + < timestamp > ] { string } and id for this process / instance used for logging
* @ param { String } [ options . desc ] additional description for humans , can be logged .
2019-01-01 16:39:08 -08:00
* @ param { String | Boolean } [ opts . path = either full path to where socket should be created or 'true' ( which uses default path ]
* @ param { string } [ opts . sockets ] comma delimited strong of sockets to be created each socket of the form [ name ] # [ c or s ] > [ n , t , m or w ]
2021-03-11 16:24:56 -08:00
* @ param [ opts . useRootNS = false ] { boolean } - include the "root" of the created base instance as part of the namespace for a packet cmd - use not recommended
2019-01-01 16:39:08 -08:00
* @ param { Object } [ opts . ( name of socket ) ] options for a particular socket created by opts . socket . see UCI guide { @ link ? content = guide # sockets | creating sockets }
* @ property { array } socket collection of all sockets created and used by base instance as per opts . sockets or addSocket method
*
* @ example
* import Base from '@uci/base'
* // options object example, credated is a socket of each transport using their defaults
* // and a tcp consumer socket that connects to a tcp socket on another host 'somehost' at port 8888
* const opts = id : mybase , sockets : 'us#s>n,tc#c>t,ts#s>t,mqtts#s>m,webs#s>w' , tc : { host : 'somehost' , port : 8888 } }
* let mybaseprocess = new Base ( opts )
* /
class Base extends EventEmitter {
2021-04-05 17:35:57 -07:00
constructor ( opts = { } ) {
if ( typeof opts === 'string' ) opts = loadYaml . sync ( opts )
2018-01-18 21:32:07 -08:00
super ( )
2020-01-14 09:20:44 -08:00
this . name = opts . name || opts . appName || 'a base class instance'
this . id = opts . id || 'uci-base:' + new Date ( ) . getTime ( )
2019-01-01 16:39:08 -08:00
log = logger ( { name : 'base' , id : this . id } )
2018-01-18 21:32:07 -08:00
this . desc = opts . desc // additional details for humans
2021-04-05 17:35:57 -07:00
this . opts = opts // make original options available
2019-04-26 11:05:10 -07:00
this . _socket = { } // holds all the various communication sockets
2019-09-13 19:05:22 -07:00
// these two if passed will get applied to all consumer sockets, otherwise socket defaults will be used
2021-03-11 16:24:56 -08:00
/** @type {string} - timeout for connecting to a consumer socket */
2019-09-13 19:05:22 -07:00
this . initTimeout = opts . initTimeout
this . retryWait = opts . retryWait
this . defaultReturnCmd = opts . defaultReturnCmd
2020-01-06 23:39:29 -08:00
this . _cmdProcessors = { _default : cmdProcessor }
2021-04-05 17:35:57 -07:00
this . ready = new Ready ( { emitter : this } )
2020-01-16 17:21:58 -08:00
// ready packet to be sent when process is "ready"
2021-04-05 17:35:57 -07:00
this . _readyPacket = {
cmd : 'ready' ,
event : ` ${ this . name } :process ` ,
name : this . name ,
id : this . id ,
ready : false ,
}
2019-04-26 11:05:10 -07:00
// _c and _s are the default namespaces
2021-04-05 17:35:57 -07:00
this . _namespaces = Object . assign ( { } , namespaces )
this . _c = Object . assign ( { } , defaultCmds . c )
this . _s = Object . assign ( { } , defaultCmds . s )
2020-01-14 09:20:44 -08:00
// make available a method that will bind a whole object tree of functions
// Note: functions called via a command namespace are called by base connext by default
// if called directlly/locally they should be bound to base context if desired
this . bindFuncs = bindFuncs
2019-01-01 16:39:08 -08:00
if ( opts . useRootNS ) {
// add root of instance to checking for command functions - not recommended!
this . _namespaces . s . splice ( - 1 , 0 , null )
this . _namespaces . c . splice ( - 1 , 0 , null )
2018-05-25 14:39:48 -07:00
}
2020-01-14 09:20:44 -08:00
// this.bindFuncs = bindFuncs // make available a method that will bind a whole object tree of functions
2021-04-05 17:35:57 -07:00
this . initSockets = opts . sockets // sockets to be registered at init
2020-01-14 09:20:44 -08:00
this . _socket = { } // where all sockets are stored
// at creation defined sockets:
2021-04-05 17:35:57 -07:00
// generic sockets
opts . sockets = Array . isArray ( opts . sockets ) ? opts . sockets : [ opts . sockets ]
if ( opts . port )
opts . sockets . push ( {
name : ` ${ this . name } :tcp ` ,
type : 's' ,
transport : 'tcp' ,
options : { port : opts . port } ,
} )
if ( opts . path )
opts . sockets . push ( {
name : ` ${ this . name } :named ` ,
type : 's' ,
transport : 'named' ,
options : { path : opts . path } ,
} )
// specific sockets
// if (opts.sockets) {
// let sockets = opts.sockets
// sockets = Array.isArray(sockets) ? sockets : [sockets]
// sockets.forEach((socket) => {
// socket.name = socket.name || `${this.name}:${socket.transport}`
// this.registerSocket(socket)
// })
// }
2018-01-18 21:32:07 -08:00
} // end constructor
2019-01-01 16:39:08 -08:00
/ *
2021-04-05 17:35:57 -07:00
* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
* CLASS METHODS
* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
* /
2019-01-01 16:39:08 -08:00
// PUBLIC METHODS
2018-05-16 11:17:38 -07:00
2019-01-01 16:39:08 -08:00
/ * *
2021-03-11 16:24:56 -08:00
* initialize the instance with the set options .
* This must be called to initialize all sockets and connections
2019-01-01 16:39:08 -08:00
* @ async
* @ public
* @ required
2021-04-05 17:35:57 -07:00
* @ param { array } sockets string of one or array of names to initialize , if none , then all current added sockets will be initialized
*
2019-01-01 16:39:08 -08:00
* /
2019-11-21 09:30:12 -08:00
async init ( sockets ) {
2021-04-05 17:35:57 -07:00
// register all sockets requested in constructor
for ( const socket of this . opts . sockets ) {
socket . name = socket . name || ` ${ this . name } : ${ socket . transport } ` // check for unique name
await this . registerSocket ( socket )
}
2020-02-10 21:33:54 -08:00
const res = await this . socketsInit ( sockets )
2021-04-05 17:35:57 -07:00
// will update ready packet and push/send that changed packet on ready state change
// on can add more observers to the ready
this . ready . all . subscribe ( async ( ready ) => {
this . _readyPacket . ready = ready
delete this . _readyPacket . _header
if ( ! ready ) {
// make a list of the failures to send
2020-02-10 21:33:54 -08:00
// await new Promise(res=>setTimeout(()=>res(),1000))
this . _readyPacket . failures = this . ready . failed
} else delete this . _readyPacket . failures
2021-04-05 17:35:57 -07:00
let packet = Object . assign ( { } , this . _readyPacket )
this . emit ( 'log' , {
level : 'debug' ,
packet : packet ,
msg : ` ${ this . name } has an updated ready state: broadcasting: event>state = ${ this . _readyPacket . event } > ${ this . _readyPacket . ready } ` ,
} )
2020-03-15 15:15:43 -07:00
// console.log('ready packet to broadcast',packet)
2021-04-05 17:35:57 -07:00
// (re)sets the connection packet for each socket
this . getSocketsFilter ( { type : 's' } ) . forEach (
( socket ) => ( this . getSocket ( socket ) . conPackets [ 0 ] = packet )
)
2020-03-24 14:22:32 -07:00
// announce changed ready state
2020-03-15 15:15:43 -07:00
setTimeout ( async ( ) => {
this . send ( packet ) // to any socket that this instance is connected to
2021-04-05 17:35:57 -07:00
this . push ( packet ) // to any remote consumer connected to an instance socket
} , 100 ) // delay 100ms, fixes issue so won't be sent during a disconnect which causes socket write error
2020-01-16 17:21:58 -08:00
} )
2020-02-10 21:33:54 -08:00
return res
2019-11-21 09:30:12 -08:00
}
async socketsInit ( sockets ) {
2019-08-29 13:41:32 -07:00
let results = { }
let errors = { }
2019-11-21 09:30:12 -08:00
// single socket intialize mapper
2021-04-05 17:35:57 -07:00
const initialize = async ( socket ) => {
return new Promise (
async function ( resolve ) {
try {
const value = await socket . init ( )
this . emit ( 'log' , {
level : 'info' ,
socketName : socket . name ,
msg : 'socket successfully initialized' ,
message : value ,
} )
resolve ( value )
} catch ( error ) {
this . emit ( 'log' , {
level : 'fatal' ,
socketName : socket . name ,
msg : 'socket init error' ,
error : error ,
} ) // emit an error here, remove socket
// let res = await this.removeSocket(socket.name)
errors [ socket . name ] = { error : error }
resolve ( error )
}
} . bind ( this )
)
2019-08-29 13:41:32 -07:00
}
2019-11-21 09:30:12 -08:00
let inits = [ ]
2021-04-05 17:35:57 -07:00
if ( ! sockets ) {
sockets = Object . keys ( this . _socket ) . filter ( ( name ) => {
return ! this . _socket [ name ] . active // only intialize (connect) inactive sockets
2019-11-21 09:30:12 -08:00
} )
2018-01-27 23:20:33 -08:00
}
2021-04-05 17:35:57 -07:00
if ( typeof sockets === 'string' ) sockets = [ sockets ]
2020-01-16 17:21:58 -08:00
2021-04-05 17:35:57 -07:00
sockets . forEach ( ( name ) => {
2019-12-05 15:32:14 -08:00
if ( this . _socket [ name ] ) {
2021-04-05 17:35:57 -07:00
inits . push ( { name : name , init : this . getSocketInit ( name ) } )
} else
log . warn ( {
msg : ` no socket registered by name of ${ name } to initialize ` ,
} )
2019-11-21 09:30:12 -08:00
} )
let [ err ] = await to ( Promise . all ( inits . map ( initialize ) ) )
if ( err ) {
2021-04-05 17:35:57 -07:00
this . emit ( 'log' , {
level : 'fatal' ,
msg : 'initialize of socket errors was NOT caught --- bad bad' ,
error : err ,
} )
return { errors : [ err ] }
2019-11-21 09:30:12 -08:00
}
2021-04-05 17:35:57 -07:00
if ( Object . keys ( errors ) . length === 0 ) errors = false
return { results : results , errors : errors }
2019-08-29 13:41:32 -07:00
}
2018-01-27 23:20:33 -08:00
2019-11-21 09:30:12 -08:00
// support old name for now
2021-04-05 17:35:57 -07:00
// async addSocket(name, type, transport, options) {
// return this.registerSocket(name, type, transport, options);
// }
2019-11-21 09:30:12 -08:00
2021-04-05 17:35:57 -07:00
// TODO use plugins for transports instead
2019-01-01 16:39:08 -08:00
/ * *
2021-04-05 17:35:57 -07:00
* registerSocket - register a socket for use
2019-11-21 09:30:12 -08:00
* This is not async and will NOT initialize the socket , that must be done with a call to init or socketInit
2019-01-01 16:39:08 -08:00
* @ param { type } name Name of socket ( usually something short but unique )
* @ param { string } [ type = c ] consumer / client 'c' or socket / server 's'
* @ param { string } [ transport = n ] transport : ( n ) named pipe / unix socket , ( t ) tcp socket , ( m ) mqtt subscribe , ( w ) websocket
* @ param { object } [ options = { } ] options for that particular type / transport of socket ( i . e . path , host , port , etc )
*
2019-11-21 09:30:12 -08:00
* @ returns { function } if called before base initialzation it can be ignored as all added sockets will be initialized . After through it be called to initialize that socket
2019-01-01 16:39:08 -08:00
* /
2021-04-05 17:35:57 -07:00
async registerSocket ( name , type = 'c' , transport , options = { } ) {
if ( isPlainObject ( name ) )
( { name , type = 'c' , transport , options = { } } = name )
if ( typeof name !== 'string' ) return null
transport = transport || ( options . port ? 'tcp' : 'named' )
2019-11-21 09:30:12 -08:00
transport = this . _validateTransport ( transport )
2021-04-05 17:35:57 -07:00
if ( ! transport ) {
log . error ( {
socketName : name ,
type : type ,
transport : transport ,
options : options ,
method : 'registerSocket' ,
msg : ` invalid transport ${ transport } ` ,
} )
return null
}
log . info ( {
socketName : name ,
type : type ,
tranport : transport ,
options : options ,
method : 'registerSocket' ,
line : 198 ,
msg : ` registering socket ${ name } ` ,
} )
2020-01-14 09:20:44 -08:00
options . id = options . id || name
options . name = options . name || name
// TODO add a 'd' type for duplex which creates an 's' first and waits on connect to make a 'c'
2021-04-05 17:35:57 -07:00
if ( type === 'c' )
options = Object . assign (
{
initTimeout : this . initTimeout ,
retryWait : this . retryWait ,
} ,
options
) // outbound
if ( type === 's' ) {
2020-02-10 21:33:54 -08:00
let conPackets = [ ] // [this._readyPacket]
2021-04-05 17:35:57 -07:00
conPackets = options . conPackets
? conPackets . concat ( options . conPackets )
: conPackets
conPackets = options . conPacket
? conPackets . push ( options . conPacket )
: conPackets
options = Object . assign (
{
defaultReturnCmd : this . defaultReturnCmd ,
conPackets : conPackets ,
} ,
options
) // inbound
2020-01-16 17:21:58 -08:00
}
2020-01-14 09:20:44 -08:00
// TODO get rid of hard coded transports and use registered transports (t and n being default)
2021-04-05 17:35:57 -07:00
// TODO dynamically import only requested connector plugins
// Base will pass options to plugin
// plugin will pass a unique transport identifier to base
// all plugins will use a function to generate a class instance
2018-05-20 15:44:31 -07:00
switch ( transport ) {
case 'n' :
options . path = options . path || true
2019-01-01 16:39:08 -08:00
// falls through
2018-05-20 15:44:31 -07:00
case 't' :
2019-04-26 11:05:10 -07:00
this . _socket [ name ] = new Socket [ TRANSLATE [ type ] ] ( options )
2018-05-20 15:44:31 -07:00
break
case 'm' :
2019-01-01 16:39:08 -08:00
if ( type === 'p' ) type = 'c'
2021-04-05 17:35:57 -07:00
options . client = type === 'c' ? true : false
2018-05-20 15:44:31 -07:00
options . connect = options . connect || { }
2019-04-26 11:05:10 -07:00
if ( options . host ) options . connect . host = options . host
if ( options . port ) options . connect . port = options . port
2018-05-20 15:44:31 -07:00
options . connect . connectTimeout = options . connect . connectTimeout || 5000
2019-04-26 11:05:10 -07:00
this . _socket [ name ] = new MQTT ( options )
2018-05-20 15:44:31 -07:00
break
case 'w' :
2021-04-05 17:35:57 -07:00
if ( type === 's' ) this . _socket [ name ] = await WebSocket ( options )
2018-05-20 15:44:31 -07:00
}
2019-09-13 19:05:22 -07:00
2021-04-05 17:35:57 -07:00
if ( this . _socket [ name ] ) {
// in case of invalid transport
2019-11-21 09:30:12 -08:00
this . _socket [ name ] . name = name
this . _socket [ name ] . type = type
this . _socket [ name ] . transport = transport
this . _socket [ name ] . _packetProcess = this . _packetProcess . bind ( this , name )
2020-02-10 21:33:54 -08:00
// bubble up events from inidivual sockets to base instance,
// connection:consumer is a socket emitting when a consumer is connecting
// connection:socket is a consumer emiting when connecting to a socket
2021-04-05 17:35:57 -07:00
const EVENTS = [
'log' ,
'socket' ,
'connection' ,
'connection:consumer' ,
'connection:socket' ,
] // that should emit up from each socket to instance
EVENTS . forEach ( ( event ) => {
this . _socket [ name ] . on ( event , ( obj ) => {
2019-11-21 09:30:12 -08:00
if ( Object . prototype . toString . call ( obj ) !== '[object Object]' ) {
2021-04-05 17:35:57 -07:00
let data = obj
2019-11-21 09:30:12 -08:00
obj = { }
obj . data = data
}
obj . socketName = name
2021-04-05 17:35:57 -07:00
this . emit ( event , obj )
2019-11-21 09:30:12 -08:00
} )
2019-09-11 21:35:08 -07:00
} )
2020-02-10 21:33:54 -08:00
2021-04-05 17:35:57 -07:00
if ( type === 'c' ) {
2020-02-10 21:33:54 -08:00
// when consumer has sucessfully connected to a socket
2021-04-05 17:35:57 -07:00
this . _socket [ name ] . obsName = ` ${ name } : ${
options . path ? options . path : ` ${ options . host } : ${ options . port } `
} < outbound > `
this . ready . addObserver ( this . _socket [ name ] . obsName , this . _socket [ name ] , {
event : 'connection:socket' ,
condition : ( ev ) => ev . state === 'connected' ,
} )
2020-02-10 21:33:54 -08:00
// set up listner for any pushed packets and emit locally
2021-04-05 17:35:57 -07:00
this . _socket [ name ] . on ( 'pushed' , ( packet ) => {
packet . _header . socketName = name
2019-11-21 09:30:12 -08:00
this . emit ( 'pushed' , packet )
} )
}
2020-01-14 09:20:44 -08:00
2021-04-05 17:35:57 -07:00
if ( type === 's' ) {
2020-02-10 21:33:54 -08:00
// when socket is listnening
2021-04-05 17:35:57 -07:00
this . ready . addObserver (
` ${ name } :socket<listening> ` ,
this . _socket [ name ] ,
{
event : 'socket' ,
condition : ( ev ) => ( ev || { } ) . state === 'listening' ,
}
)
2020-03-15 15:15:43 -07:00
// initially set conPackets, ready packets is ALWAYS the first
this . _socket [ name ] . conPackets . unshift ( this . _readyPacket )
2021-04-05 17:35:57 -07:00
if ( options . duplex )
this . consumerConnected ( this . _socket [ name ] , {
consumer : options . duplex ,
add : true ,
} )
2020-02-10 21:33:54 -08:00
}
2021-04-05 17:35:57 -07:00
return this . _socket [ name ] // return handle to newly registered socket
2020-07-04 10:14:18 -07:00
// return this.getSocketInit(name) // returns the init function (e.g. connect or create) for the socket
2019-11-21 09:30:12 -08:00
}
2018-05-16 11:17:38 -07:00
}
2019-01-01 16:39:08 -08:00
/ * *
2020-07-26 16:51:37 -07:00
* removeSocket
2019-01-01 16:39:08 -08:00
*
2019-11-21 09:30:12 -08:00
* @ param { string } name name of socket as created
2019-01-01 16:39:08 -08:00
* @ returns { String | Object } success string or error object
* /
2019-11-21 09:30:12 -08:00
2019-01-01 16:39:08 -08:00
async removeSocket ( name ) {
2019-03-17 13:45:19 -07:00
// NOTE: uci consumers have .end renamed as .close to match socket method for convenience
2021-04-05 17:35:57 -07:00
if ( typeof name !== 'string' )
return 'no socket name passed, nothing to remove'
2020-07-26 16:51:37 -07:00
const socket = this . getSocket ( name )
if ( ! socket ) return 'no socket by that name, nothing to remove'
2019-08-29 13:41:32 -07:00
let closeError
2021-04-05 17:35:57 -07:00
if ( typeof socket . close !== 'function' )
return 'bad socket no close function, nothing to remove'
2020-07-26 16:51:37 -07:00
let [ err ] = await to ( socket . close ( ) )
2021-04-05 17:35:57 -07:00
if ( err )
if ( err . code !== 'ERR_SERVER_NOT_RUNNING' ) {
closeError = {
socket : this . _socket [ name ] . name ,
error : err ,
msg : 'socket/consumer closed with errors, but removed' ,
}
}
this . emit ( 'log' , {
level : 'warn' ,
msg : ` socket ${ name } has been removed ` ,
socket : this . _socket [ name ] . opts ,
} )
2020-07-26 16:51:37 -07:00
socket . removeAllListeners ( )
2021-04-05 17:35:57 -07:00
this . ready . removeObserver (
socket . type === 'c'
? this . _socket [ name ] . obsName
: ` ${ name } :socket<listening> `
)
2019-04-26 11:05:10 -07:00
delete this . _socket [ name ]
2019-08-29 13:41:32 -07:00
return closeError ? closeError : 'success'
2018-05-16 11:17:38 -07:00
}
2018-03-02 08:35:25 -08:00
2019-04-26 11:05:10 -07:00
getSocket ( name ) {
2020-07-26 16:51:37 -07:00
if ( name ) return this . _socket [ name ] || null
2019-04-26 11:05:10 -07:00
else return this . _socket
}
2019-11-21 09:30:12 -08:00
// returns array of names of sockets that pass filter
2021-04-05 17:35:57 -07:00
getSocketsFilter ( { type , trans , active } = { } ) {
2019-11-21 09:30:12 -08:00
if ( trans ) trans = this . _validateTransport ( trans )
let filtered = [ ]
2021-04-05 17:35:57 -07:00
Object . keys ( this . _socket ) . forEach ( ( name ) => {
if (
( type == null || this . _socket [ name ] . type === type ) &&
( trans == null || this . _socket [ name ] . transport === trans ) &&
( active == null || this . _socket [ name ] . active === active )
)
filtered . push ( name )
2019-11-21 09:30:12 -08:00
} )
return filtered
}
2021-04-05 17:35:57 -07:00
getConsumers ( filter = { } ) {
filter . type = 'c'
2019-11-21 09:30:12 -08:00
return this . getSocketsFilter ( filter )
}
getSocketInit ( name ) {
let socket = this . _socket [ name ]
2021-04-05 17:35:57 -07:00
if ( ! socket ) {
log . warn ( {
msg : ` can't fetch create/connect function, no socket registered by name of ${ name } ` ,
} )
2019-12-05 15:32:14 -08:00
return null
}
2021-04-05 17:35:57 -07:00
if (
this . _socket [ name ] . type === 's' &&
this . _socket [ name ] . transport !== 'm'
) {
2019-11-21 09:30:12 -08:00
return socket . create
} else {
return socket . connect
}
}
2019-04-26 11:05:10 -07:00
2019-01-01 16:39:08 -08:00
/ * *
* send - Description
*
* @ param { String } name name of socket for send ( must be a consumer otherwise use push for server )
* @ param { Object } packet
*
* @ returns { type } Description
* /
async send ( name , packet ) {
2020-03-24 14:22:32 -07:00
let names = [ ]
2018-02-13 22:31:02 -08:00
if ( typeof name !== 'string' ) {
packet = name
2020-03-24 14:22:32 -07:00
names = this . getConsumers ( )
2019-01-01 16:39:08 -08:00
} else {
2020-03-24 14:22:32 -07:00
const con = this . getSocket ( name )
if ( ! con ) return ` no consumer ${ name } for sending `
names = [ name ]
2018-01-27 23:20:33 -08:00
}
2021-04-05 17:35:57 -07:00
if ( ! packet || ! Object . keys ( packet ) . length )
return 'no packet to send - aborted'
2020-03-24 14:22:32 -07:00
let sends = [ ]
if ( ! names . length ) return 'no consumers available for send, aborting'
for ( let name of names ) {
2021-04-05 17:35:57 -07:00
const consumer = this . getSocket ( name )
2020-03-24 14:22:32 -07:00
let hookedPacket = { }
2021-04-05 17:35:57 -07:00
hookedPacket = consumer . beforeSend
? await consumer . beforeSend . call ( this , Object . assign ( { } , packet ) )
: packet
log . debug ( {
msg : 'after hook, sending packet' ,
name : consumer . name ,
packet : hookedPacket ,
method : 'send' ,
} )
sends . push ( consumer . send . bind ( consumer , hookedPacket ) )
2020-03-24 14:22:32 -07:00
}
if ( sends . length === 1 ) return await sends [ 0 ] ( )
return Promise . all (
2021-04-05 17:35:57 -07:00
sends . map ( ( send ) => {
2020-03-24 14:22:32 -07:00
return send ( )
} )
)
2018-01-18 21:32:07 -08:00
}
2021-04-05 17:35:57 -07:00
async push ( packet , opts = { } ) {
if ( ! packet || ! Object . keys ( packet ) . length )
return Promise . resolve ( 'no packet to push - aborted' )
let sockets = this . getSocketsFilter ( { type : 's' } )
if ( ! sockets . length ) return Promise . resolve ( 'no sockets on which to push' )
opts . sockets = opts . sockets
? opts . sockets
: opts . socket
? [ opts . socket ]
: [ ]
if ( opts . sockets . length )
sockets = sockets . filter ( ( name ) => opts . sockets . includes ( name ) )
2020-02-10 21:33:54 -08:00
sockets = sockets
2021-04-05 17:35:57 -07:00
. map ( ( name ) => this . getSocket ( name ) )
. filter ( ( sock ) =>
opts . transport && opts . transport !== 'all'
? sock . transport === this . _validateTransport ( opts . transport )
: true
)
2020-02-10 21:33:54 -08:00
// console.log(sockets.length, 'sockets for push', sockets.map(socket=>socket.name))
if ( ! sockets . length ) return Promise . resolve ( 'no sockets on which to push' )
2019-11-21 09:30:12 -08:00
let broadcast = [ ]
2020-02-10 21:33:54 -08:00
// TODO use map and reflect
2019-11-21 09:30:12 -08:00
for ( let socket of sockets ) {
let hookedPacket = { }
2021-04-05 17:35:57 -07:00
hookedPacket = socket . beforeSend
? await socket . beforeSend . call ( this , Object . assign ( { } , packet ) , true )
: packet
broadcast . push ( socket . push . bind ( socket , hookedPacket , opts ) )
2019-11-21 09:30:12 -08:00
}
2019-01-01 16:39:08 -08:00
return Promise . all (
2021-04-05 17:35:57 -07:00
broadcast . map ( ( push ) => {
2019-04-26 11:05:10 -07:00
return push ( )
2019-01-01 16:39:08 -08:00
} )
)
2020-02-10 21:33:54 -08:00
}
2019-05-01 15:37:52 -07:00
// TODO accept alt transport string i.e. t or TCP
2019-01-01 16:39:08 -08:00
async sendTransport ( packet , transport ) {
2018-02-13 14:19:18 -08:00
let sends = [ ]
2019-04-26 11:05:10 -07:00
for ( let name of Object . keys ( this . _socket ) ) {
if ( this . _socket [ name ] . type === 'c' ) {
if ( this . _socket [ name ] . transport === transport ) {
sends . push ( this . _socket [ name ] . send . bind ( this . _socket [ name ] ) )
2018-02-13 14:19:18 -08:00
}
2018-02-06 18:30:00 -08:00
}
2018-02-05 22:05:38 -08:00
}
2018-02-13 14:19:18 -08:00
if ( sends . length === 1 ) return sends [ 0 ] ( packet )
2019-01-01 16:39:08 -08:00
return Promise . all (
2021-04-05 17:35:57 -07:00
sends . map ( ( send ) => {
2019-01-01 16:39:08 -08:00
return send ( packet )
} )
)
2018-02-05 22:05:38 -08:00
}
2019-04-26 11:05:10 -07:00
// async sendMQTT(topic, packet) {return this._socket.mqtt.send(topic, packet)}
2019-01-01 16:39:08 -08:00
async sendTCP ( packet ) {
return this . sendTransport ( packet , 't' )
}
2019-05-01 15:37:52 -07:00
// TODO change this to PIPE
2019-01-01 16:39:08 -08:00
async sendIPC ( packet ) {
return this . sendTransport ( packet , 'n' )
}
2018-02-05 22:05:38 -08:00
2019-05-01 15:37:52 -07:00
// TODO add sendMQTT, sendWS
2021-04-05 17:35:57 -07:00
socketsListen ( event , fn ) {
this . _eventListen ( 's' , event , fn )
2019-04-26 11:05:10 -07:00
}
2021-04-05 17:35:57 -07:00
consumersListen ( event , fn ) {
this . _eventListen ( 'c' , event , fn )
2019-01-01 16:39:08 -08:00
}
2018-02-04 14:18:21 -08:00
2018-02-13 14:19:18 -08:00
getPacketByName ( name , packets ) {
if ( ! packets . length ) packets = [ packets ]
let found = { }
2019-01-01 16:39:08 -08:00
packets . some ( ( packet , index , packets ) => {
2018-02-13 14:19:18 -08:00
if ( packet . _header . sender . name === name ) {
found = packets [ index ]
return true
}
} )
return found
}
2019-04-26 11:05:10 -07:00
// add set of functions to class prop/space and then register with this
addNamespace ( space , type , trans ) {
2021-04-05 17:35:57 -07:00
if ( type !== 'c' && type !== 's' ) {
2019-08-27 20:10:44 -07:00
trans = type
2021-04-05 17:35:57 -07:00
type = 's'
}
2019-08-27 20:10:44 -07:00
trans = this . _validateTransport ( trans )
2019-04-26 11:05:10 -07:00
if ( trans ) return this . _namespaces [ type + trans ] . unshift ( space )
else return this . _namespaces [ type ] . unshift ( space )
2019-04-20 16:57:44 -07:00
}
2020-01-06 23:39:29 -08:00
// object of functions, key is cmd name
2019-08-27 20:10:44 -07:00
amendCommands ( funcs , trans , type ) {
if ( ! trans && ! type ) type = 's'
2021-04-05 17:35:57 -07:00
if ( trans === 'c' || trans === 's' ) {
2019-08-27 20:10:44 -07:00
type = trans
trans = ''
2018-02-05 22:05:38 -08:00
}
2019-08-27 20:10:44 -07:00
trans = this . _validateTransport ( trans )
2021-04-05 17:35:57 -07:00
if ( ! this [ '_' + type + trans ] ) this [ '_' + type + trans ] = { }
Object . assign ( this [ '_' + type + trans ] , funcs ) // trans is type here
log . debug ( {
msg : 'amended namespace' ,
id : this . id ,
default _key : '_' + type + trans ,
functions : this [ '_' + type + trans ] ,
} )
2019-08-27 20:10:44 -07:00
}
amendConsumerCommands ( funcs , trans ) {
2021-04-05 17:35:57 -07:00
this . amendCommands ( funcs , trans , 'c' )
2018-01-29 21:51:13 -08:00
}
2019-08-27 20:10:44 -07:00
2019-04-26 11:05:10 -07:00
amendSocketCommands ( funcs , trans ) {
2021-04-05 17:35:57 -07:00
this . amendCommands ( funcs , trans )
2018-01-29 21:51:13 -08:00
}
2018-05-20 15:44:31 -07:00
2019-04-26 11:05:10 -07:00
// func should take and return a packet. if type
2021-04-05 17:35:57 -07:00
beforeSendHook ( func , opts ) {
this . _packetHook ( 'beforeSend' , func , opts )
2018-01-27 23:20:33 -08:00
}
2018-01-18 21:32:07 -08:00
2021-04-05 17:35:57 -07:00
beforeProcessHook ( func , opts ) {
this . _packetHook ( 'beforeProcess' , func , opts )
2019-04-26 11:05:10 -07:00
}
2021-04-05 17:35:57 -07:00
afterProcessHook ( func , opts ) {
this . _packetHook ( 'afterProcess' , func , opts )
2019-04-26 11:05:10 -07:00
}
// A Big Hammer - use only if necessary - default with hooks should suffice
// these three will pre-empt default processor to be called in ._packetProcess
2019-03-17 13:45:19 -07:00
2019-04-26 11:05:10 -07:00
// add and override default processor for ALL consumers, i.e. packets returning form a send or arrived from a push
2018-02-05 22:05:38 -08:00
consumersProcessor ( func ) {
2019-04-26 11:05:10 -07:00
for ( let name of Object . keys ( this . _socket ) ) {
if ( this . _socket [ name ] . type === 'c' ) {
this . altProcessor ( func , name )
2018-02-05 22:05:38 -08:00
}
}
}
2018-02-04 14:18:21 -08:00
2019-04-26 11:05:10 -07:00
// add and override default processor for ALL sockets, i.e. packets coming in from consumer send
2018-02-05 22:05:38 -08:00
socketsProcessor ( func ) {
2019-04-26 11:05:10 -07:00
for ( let name of Object . keys ( this . _socket ) ) {
if ( this . _socket [ name ] . type === 's' ) {
this . altProcessor ( func , name )
2018-02-05 22:05:38 -08:00
}
}
2018-01-30 21:12:38 -08:00
}
2019-04-26 11:05:10 -07:00
// add and override a processor for a particular socket/consumer to list of processors
// if no socket name given it will replace the default processor in _processors from processing.js
altProcessor ( func , socket _name ) {
2018-02-05 22:05:38 -08:00
socket _name = socket _name || '_default'
2020-01-06 23:39:29 -08:00
this . _cmdProcessors [ socket _name ] = func
2018-02-06 18:30:00 -08:00
}
2020-01-16 17:21:58 -08:00
// a call to this method will (make or add) return and or subscribe a ready observer for incoming consumer connections
2021-04-05 17:35:57 -07:00
consumerConnected ( socket , opts = { } ) {
let { subscribe , consumer , name , add } = opts
const conditionHandler = async ( ev ) => {
if ( ( ev || { } ) . state === 'connected' ) {
let data = ev . data || { }
if ( consumer ) {
// specific consumer check
if (
data . name === consumer ||
[ ev . name , ev . id , data . name , data . id ] . some ( ( name ) =>
( name || '' ) . includes ( consumer )
)
)
return true
2020-01-16 17:21:58 -08:00
} else return true
}
return false
}
2021-04-05 17:35:57 -07:00
if ( typeof socket === 'string' ) socket = this . getSocket ( socket )
2020-01-16 17:21:58 -08:00
2020-01-21 18:38:11 -08:00
name = name || consumer
add = add && consumer
2021-04-05 17:35:57 -07:00
const options = {
event : 'connection:consumer' ,
condition : conditionHandler ,
}
2020-03-15 15:15:43 -07:00
let oname = ` ${ name } :consumer> ${ socket . name } :socket<inbound> `
2021-04-05 17:35:57 -07:00
const obs = add
? this . ready . addObserver ( oname , socket , options )
: this . ready . makeObserver ( socket , options )
if ( typeof subscribe === 'function' ) return obs . subscribe ( subscribe )
2020-01-16 17:21:58 -08:00
return obs
} // end consumerConnected
2020-01-06 23:39:29 -08:00
//=============PRIVATE METHODS =========================================
2018-02-04 14:18:21 -08:00
/ *
2021-04-05 17:35:57 -07:00
*
* Assigns a Hook Function to a Socket , Type or Transport
*
* /
2020-01-06 23:39:29 -08:00
// options allow applying hook function to specific socket or type or transport, default is all type 's' sockets
2021-04-05 17:35:57 -07:00
_packetHook ( hook , func , opts ) {
log . debug ( {
msg : 'hooking a socket(s)' ,
method : '_packetHook' ,
line : 334 ,
hook : hook ,
function : func ,
options : opts ,
} )
let { name , type , trans , all } = opts
if ( opts == null ) type = 's' // default is all type 's' sockets
2019-08-27 20:10:44 -07:00
if ( name ) this . _socket [ name ] [ hook ] = func
else {
2021-04-05 17:35:57 -07:00
log . debug ( {
msg : 'sockets available to hook' ,
method : '_packetHook' ,
line : 338 ,
sockets : Object . keys ( this . _socket ) ,
} )
2019-08-27 20:10:44 -07:00
for ( let name of Object . keys ( this . _socket ) ) {
if ( this . _socket [ name ] . type === type ) this . _socket [ name ] [ hook ] = func
2021-04-05 17:35:57 -07:00
if ( this . _socket [ name ] . transport === trans )
this . _socket [ name ] [ hook ] = func
2019-08-27 20:10:44 -07:00
if ( all ) this . _socket [ name ] [ hook ] = func
2021-04-05 17:35:57 -07:00
if ( this . _socket [ name ] [ hook ] )
log . debug ( {
msg : 'hooked socket' ,
method : '_packetHook' ,
line : 343 ,
name : name ,
type : this . _socket [ name ] . type ,
trans : this . _socket [ name ] . transport ,
} )
2019-08-27 20:10:44 -07:00
}
}
}
2019-04-26 11:05:10 -07:00
/ *
2021-04-05 17:35:57 -07:00
* * * * * * * * * * main packet processor for all sockets
* supports per socket before and after hook processors
* supports additonal registered processors called via packet or socket name , with default processor ,
* /
2019-04-26 11:05:10 -07:00
async _packetProcess ( socket _name , packet ) {
2021-04-05 17:35:57 -07:00
if ( ! packet || ! Object . keys ( packet ) . length )
packet = { error : 'no packet to process' }
if ( ! socket _name || ! this . getSocket ( socket _name ) )
packet . error = 'no socket name passed for packet processing'
if ( ! this . getSocket ( socket _name ) )
packet . error = ` socket by name of ${ socket _name } `
2020-03-24 14:22:32 -07:00
if ( packet . error ) {
2021-04-05 17:35:57 -07:00
this . emit ( 'log' , {
level : 'error' ,
error : packet . error ,
packet : packet ,
msg : 'an error occured before processing an incoming packet' ,
} )
return packet // don't process a packet with an error
2020-03-24 14:22:32 -07:00
}
2020-01-06 23:39:29 -08:00
// TODO allow adding to or altering the process map
let processors = new Map ( [
2021-04-05 17:35:57 -07:00
[ 'before' , this . getSocket ( socket _name ) . beforeProcess ] ,
[
'command' ,
this . _cmdProcessors [
packet . cmdProcessor || this . _cmdProcessors [ socket _name ]
? socket _name
: '_default'
] ,
] ,
[ 'after' , this . getSocket ( socket _name ) . afterProcess ] ,
2020-01-06 23:39:29 -08:00
] )
let err
2021-04-05 17:35:57 -07:00
for ( let [ name , func ] of processors ) {
// the same as of recipeMap.entries()
[ err , packet ] = await to ( this . _process ( socket _name , packet , name , func ) )
2020-01-06 23:39:29 -08:00
if ( err ) packet . error = err
}
return packet
}
2021-04-05 17:35:57 -07:00
async _process ( socket _name , packet , name , func ) {
if ( packet . error ) return packet // if an error occurs skip any further processing
2019-09-11 21:35:08 -07:00
let err , res
2020-01-06 23:39:29 -08:00
if ( func ) {
2021-04-05 17:35:57 -07:00
[ err , res ] = await to ( func . call ( this , packet , socket _name ) )
if ( err ) {
// forced an abort to processing
2019-09-11 21:35:08 -07:00
packet . error = err
2020-01-06 23:39:29 -08:00
} else {
2021-04-05 17:35:57 -07:00
if ( ! isPlainObject ( res ) )
packet . processResult
? ( packet . processResult [ name ] = res )
: ( packet . processResult = { [ name ] : res } )
2020-01-06 23:39:29 -08:00
else {
2021-04-05 17:35:57 -07:00
let method =
( packet . processMethod || { } ) [ name ] || packet . processMethod
2020-03-15 15:15:43 -07:00
// TODO could support other specialized merge methods
2020-01-06 23:39:29 -08:00
if ( method === 'merge' ) {
2021-04-05 17:35:57 -07:00
packet = merge ( packet , res )
} else {
packet = res
2020-02-10 21:33:54 -08:00
}
2020-01-06 23:39:29 -08:00
}
2019-09-11 21:35:08 -07:00
}
}
2020-01-06 23:39:29 -08:00
return packet
2019-04-26 11:05:10 -07:00
}
// all sockets are emitters. Adds a listener to all sockets of a type with given event.
// now sockets can emit locally processed events
2021-04-05 17:35:57 -07:00
_eventListen ( type , event , fn ) {
2019-04-26 11:05:10 -07:00
for ( let name of Object . keys ( this . _socket ) ) {
if ( this . _socket [ name ] . type === type ) {
2021-04-05 17:35:57 -07:00
if ( fn === 'stop' ) this . _socket [ name ] . removeAllListeners ( event )
2019-04-20 16:57:44 -07:00
else {
2021-04-05 17:35:57 -07:00
log . debug ( {
socket : name ,
type : type ,
event : event ,
msg : 'adding listener to socket' ,
} )
2019-04-26 11:05:10 -07:00
this . _socket [ name ] . on ( event , fn )
2019-04-20 16:57:44 -07:00
}
}
}
}
2021-04-05 17:35:57 -07:00
_validateTransport ( trans , type = 's' ) {
2019-08-27 20:10:44 -07:00
const valids = {
2021-04-05 17:35:57 -07:00
w : 'w' ,
web : 'w' ,
n : 'n' ,
named : 'n' ,
unix : 'n' ,
pipe : 'n' ,
t : 't' ,
tcp : 't' ,
net : 't' ,
network : 't' ,
m : 'm' ,
mqtt : 'm' ,
2019-08-27 20:10:44 -07:00
}
2021-04-05 17:35:57 -07:00
trans = valids [ trans ] || null
if ( type !== 's' && trans === 'w' ) {
log . warn ( {
type : type ,
transport : trans ,
msg :
'Invalid type/transport - Consumer/Client Web Socket not supported use TCP' ,
} )
trans = null
2019-11-21 09:30:12 -08:00
}
2019-08-27 20:10:44 -07:00
return trans
}
2019-01-01 16:39:08 -08:00
_transport ( name ) {
2019-04-26 11:05:10 -07:00
return this . _socket [ name ] . transport
2019-01-01 16:39:08 -08:00
} //getter for socket transport
_type ( name ) {
2019-04-26 11:05:10 -07:00
return this . _socket [ name ] . type
2019-01-01 16:39:08 -08:00
} //getter for socket type
2018-02-04 14:18:21 -08:00
2018-02-06 18:30:00 -08:00
_getTransportNamespaces ( socket ) {
2019-01-01 16:39:08 -08:00
return this . _namespace [ this . _type ( socket ) + this . _transport ( socket ) ]
2018-02-06 18:30:00 -08:00
}
2019-01-01 16:39:08 -08:00
_getCmdFuncNamespace ( cmd , namespaces ) {
2018-02-06 18:30:00 -08:00
let cmd _func = null
2021-04-05 17:35:57 -07:00
namespaces . some ( ( namespace ) => {
2019-01-01 16:39:08 -08:00
namespace = namespace ? namespace + '.' + cmd : cmd
2018-02-06 18:30:00 -08:00
cmd _func = this . _getCmdFunc ( namespace )
if ( cmd _func ) return true
} )
return cmd _func
}
2019-08-27 20:10:44 -07:00
// takes command and returns corresponding function in a hash, recurisve walk
2019-01-01 16:39:08 -08:00
_getCmdFunc ( cmd , obj ) {
if ( typeof cmd === 'string' ) {
2018-02-06 18:30:00 -08:00
if ( ! obj ) obj = this
2019-01-01 16:39:08 -08:00
cmd = cmd . split ( /[.:/]+/ )
2018-02-06 18:30:00 -08:00
}
2019-01-01 16:39:08 -08:00
var prop = cmd . shift ( )
2018-02-06 18:30:00 -08:00
if ( cmd . length === 0 ) return obj [ prop ]
2019-01-01 16:39:08 -08:00
if ( ! obj [ prop ] ) return null
2021-04-05 17:35:57 -07:00
log . debug ( {
length : cmd . length ,
cmd : cmd ,
prop : prop ,
objprop : obj [ prop ] ,
method : '_getCmdFunc' ,
msg : 'command to corresponding function in a hash' ,
} )
2018-02-06 18:30:00 -08:00
return this . _getCmdFunc ( cmd , obj [ prop ] )
}
2019-04-26 11:05:10 -07:00
// primary function to find a function to call based on packet cmd
2019-01-01 16:39:08 -08:00
async _callCmdFunc ( packet , socket ) {
let cmd _func = this . _getCmdFuncNamespace (
packet . cmd ,
this . _namespaces [ this . _type ( socket ) + this . _transport ( socket ) ]
)
2021-04-05 17:35:57 -07:00
if ( cmd _func ) return await cmd _func . call ( this , packet ) // todo try .call
2019-01-01 16:39:08 -08:00
cmd _func = this . _getCmdFuncNamespace (
packet . cmd ,
this . _namespaces [ this . _type ( socket ) ]
)
2021-04-05 17:35:57 -07:00
if ( cmd _func ) return await cmd _func . call ( this , packet )
2018-02-06 18:30:00 -08:00
return 'failed'
}
2019-01-01 16:39:08 -08:00
} // end Base Class
2018-02-04 14:18:21 -08:00
2019-01-01 16:39:08 -08:00
export default Base
2021-04-05 17:35:57 -07:00
export { Base , Ready , map , changed , isPlainObject , to , merge , loadYaml } // todo share rxjs