message-relay-services
Version:
A simple server to route message to other services, such as email, admin, etc.
244 lines (211 loc) • 8.26 kB
JavaScript
'use strict';
// an intermediate endpoint implementation
//
const Server = require('./message_endpoint')
const EventEmitter = require('events')
const dgram = require('dgram')
const UDP_TYPE = 'udp4'
const NOISY = false
const BIG_TIMEOUT_INTERVAL = 1000*60*30
class NOOPWriter extends EventEmitter {
constructor() {
super()
}
write(message) {}
}
/**
*
*/
class UDPWriter extends EventEmitter {
constructor(address,port,connection) {
super()
// this is a child process sending generically to the parent the process.send
this.readyState = (typeof process.send === "function") ? 'open' : 'closed'
this.con = connection
this.address = address
this.port = port
}
//
write(message) { // passed in as a buffer
if ( !this.con ) return
this.con.send(message,0,message.length,this.port,this.address) // send back to this one
}
}
//
/**
* Creates an endpoint server for connected sockets.
* This differs from other servers in that it can be configured to respond or not respond to messages.
* Also, instances can be configured to keep client information or disgard it.
* The storage of the client information for such things as handling publications and subscriptions is under the
* control of the client by setting `_no_resp` and `no_keeps`. When `_no_resp` the server will not respond to messages.
* If `no_keeps` is false, the default, then this will store connections by keys made from the port and address of the remote in a
* timeout map.
*/
class ServerUDP extends Server {
//
constructor(conf) {
super(conf)
this.time_out_table = {}
this.time_out_period = conf.time_out_period ? conf.time_out_period : BIG_TIMEOUT_INTERVAL
this.no_op_writer = new NOOPWriter()
this.no_keeps = conf.no_keeps ? true : false
this.use_broadcast = conf.use_broadcast ? true : false
this.broadcast_address = conf.broadcast_address
}
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
/**
*
* @param {boolean} bval
*/
set_use_broadcast(bval) {
if ( this.connection !== undefined ) {
this.use_broadcast = bval
this.connection.setBroadcast(bval) // bval
} else {
this.use_broadcast = false // no connectino to set
}
}
/**
*
* @returns {boolean} - if the broadcast option is in use
*/
get_use_broadcast() {
return this.use_broadcast
}
// send_to_all
// send to all connections on the topic except the one that published
/**
* Adds to the parent implementation and uses some of its code for the default case.
*
* @param {*} topic
* @param {*} msg_obj
* @param {*} ignore
* @returns
*/
async send_to_all(topic,msg_obj,ignore,replier) { // publish to all conncetions (fan out)
if ( this.app_can_block_and_respond ) {
let blocked = await this.app_publication_pre_fan_response(topic,msg_obj,ignore)
if ( blocked ) return
}
let tset = this.all_topics[topic] // topic map -- all subscription clients
if ( tset ) {
// USE BROADCAST
if ( this.use_broadcast && this.connection ) {
let str_msg = replier.encode_message(msg_obj) // BROADCAST ADDRESS
this.connection.send(str_msg,0,str_msg.length,this.port,this.broadcast_address)
} else {
// USE INDIVIDUAL KEPT CONNECTIONS
for ( let client_name in tset ) { // all clients
let relayer = tset[client_name]
this.send_to_one(relayer,msg_obj)
if ( relayer && (relayer.writer !== ignore) ) {
this.send_to_one(relayer,msg_obj)
}
}
//
}
if ( this.app_handles_subscriptions ) { // (override) this application acts as an endpoint to the topic
this.app_subscription_handler(topic,msg_obj)
}
}
}
/**
*
* @param {object} conf
*/
_init_members(conf) {
super._init_members(conf)
this.udp_type = conf ? conf.udp4or6 || UDP_TYPE : UDP_TYPE
this.time_out_interval = setInterval(() => {
let t = Date.now()
let removals = []
for ( let [client_name,access_time] of Object.entries(this.time_out_table) ) {
if ( t - access_time > this.time_out_period*3 ) {
this.remove_connection(client_name)
removals.push(client_name)
}
}
for ( let cname of removals ) {
this.remove_connection(cname)
}
},this.time_out_period)
}
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
/**
*
* @param {object} sock - the expected UDP socket returned by node.js dgram.createSocket
*/
onListenting_func(sock) {
// //
//
sock.on('message', async (data, rinfo) => {
//
//
try {
let dstr = data.toString()
let mobj = JSON.parse(dstr)
//
if ( mobj._no_resp ) { // keep one permanent client that never receives a response
let no_client_name = "NO:RESPONSE"
let prev_writer = this.has_connection(no_client_name) // returns a wrapper of the UDPWriter
if ( prev_writer === false ) {
this.add_connection(no_client_name,this.no_op_writer)
}
await this.add_object_and_react(no_client_name,mobj) // this is usually supposed to be a response to a previous send
} else {
let client_name = `${rinfo.address}:${rinfo.port}`;
if ( NOISY ) console.log(`new client connected: ${client_name}`);
//
let prev_writer = this.has_connection(client_name) // returns a wrapper of the UDPWriter
if ( prev_writer === false ) {
let a_writer = new UDPWriter(rinfo.address,rinfo.port,sock);
this.add_connection(client_name,a_writer)
}
//
await this.add_object_and_react(client_name,mobj) // this is usually supposed to be a response to a previous send
if ( this.no_keeps && (mobj._ps_op && ( mobj._ps_op == 'sub' || mobj._ps_op == 'pub' )) ) {
this.time_out_table[client_name] = Date.now()
} else if ( !this.no_keeps ) { // anything not having to do with publication and we are keeping connections
this.time_out_table[client_name] = Date.now()
} else {
this.remove_connection(client_name)
}
}
} catch (e) {}
});
//
sock.on('close', () => {
//sock.disconnect()
});
//
sock.on('error', (err) => {
console.error(`Connection ${client_name} error: ${err.message}`);
});
//
}
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
/**
*
*/
_create_connection() {
//
this.connection = dgram.createSocket(this.udp_type)
this.connection.on('listening',() => {
const address = this.connection.address();
console.log(`server listening ${address.address}:${address.port}`);
this.onListenting_func(this.connection)
//
this.set_use_broadcast(this.use_broadcast) // if it was already set by config
//
this.emit('server-ready')
})
if ( this.address !== undefined ) {
this.connection.bind(this.port,this.address)
} else {
this.connection.bind(this.port) // listen on all interface
}
//
}
}
//
module.exports = ServerUDP;