UNPKG

ampjs

Version:

A Javascript(NodeJS) framework for handling the AMP protocol

558 lines (511 loc) 19.6 kB
/* AmpJS - is a library/framework to send/receive data over a TCP network using the Amp Protocol Written by Matthew Grant @ 2019. It is free to use and modify under the l Structure of Library: Ampbox - is an object that holds data that is sent/receive over the network senderbox.js - handles sending ampboxes over the amp network receiverbox.js - handles receiving ampboxes over the amp network controller.js - Holds the controller class which take/receive data from the network process it. It acts like the middle man bewteen Ampbox and the custom command classes. AmpBox <--> Controller <--> Command command.js - Hold two classes: commandlocator and command. Commandlocator receives data from the Controller and initializes the correct custom command classes and connects the callback function for the custom class. The command class holds the basic structure for commands. All custom commands needs to be derived from this class server.js - is responsible for opening a server and sending/receiving data from other nodes mapext.js - is an extention of the map class that provides extra functionality over the standard map class. ampdate.js - is an extention of the date class. It provides a function called toAmpDate() that is used to output a string in ISO 8061 format that the amp network uses There is also a minification, compressed version that holds all the above files in one small file (about 5kb in size) called AmpJS.js Example usage of this library can be seen in the amptest.py file. It demostrates how to derive custom command class. It shows how to start the network up so one can send/receive data over the network. To run the example: node amptest.js serverport clientport serverport - is the port that this program will open the server and listen for connects clientport - is the port of the client program that you want to connect to and send commands Example: Open two terminals First terminal type 'node amptest.js 2000 3000' Second terminal type 'node amptest.js 3000 2000' Then go to either window and it will prompt you for a command. In this case (Add, Sub, Mul, Div, exit) Then it will prompt you for variable A and B. Once it got this data it will send it over to the other node To get an amp network up and running. You must do these five items. 1) Define a custom command class derived from the Command Class which holds the basic structure for the custom command: Its variables(a map that holds variable name: variable type) and the structure of the responds the structure of the reply message (a map that holds variable name: variable type). See the Add, Sub, Mul, and Div command class below to see how it should be structured. 2) Define a callback function for each custom command class. This is where the data received from the wire will be sent to and process. See OnAdd, OnSub, OnMul, and OnDiv functions for example. (Command design pattern) 3) Define a factorycommands object. That connects the command sent over the wire to its custom command class. Its works like a factory method design pattern. See example below. 4) Define a factorycallbacks object. That connects the custom command class to its callback function. See example below 5) The last things is to define a port and start the server. (Server.listen) See example below. To send data to a client node one simply calls the senddata command senddata(command, a, b, clientaddress, clientport) */ const prompt = require('prompt'); var controller = require('../lib/controller'); var command = require('../lib/command'); var server = require('../lib/server'); class _Error extends command.Command { constructor(receiveargs, funcallback) { var arguments1 = new Map(); var maparg = new Map(); var mapres = new Map([['_error', 'string'], ['_error_code', 'string'], ['_error_description', 'string']]); super(maparg, mapres, receiveargs, _OnError); this.responder = _OnError; } } // All custom amp command must be derived from the command class and have the structure shown below class Add extends command.Command { //Custom command Add that adds two variables a + b and returns the total = a + b constructor(receiveargs, funcallback) { /* argumentMap - will consist of variable name and type stored in an Array respondsMap - will be consist of variable and type stored in an array receiveargs - Holds a map of variables that was received from the wire responder property holds the callback function when this custom command is invoke funcallback - is the callback function that will trigger when this command is called */ //Map(['a', 'array[string]']) => Command a = ['matt', 'monroe', 'grant'] // over the wire _command; Add, a; 'matt', a; 'monroe', a; 'grant' var argumentMap = new Map([['a', 'integer'], ['b', 'integer']]); var respondsMap = new Map([['total', 'integer']]); super(argumentMap, respondsMap, receiveargs, funcallback); this.responder = funcallback; } } class Sub extends command.Command { //Custom command Sub that subtracks two variables a - b and returns the total = a - b constructor(arguments1, fn) { var argArray = [['a', 'integer'], ['b', 'integer']]; var resArray = [['total', 'integer']] var maparg = new Map(argArray); var mapres = new Map(resArray); super(maparg, mapres, arguments1, fn); this.responder = fn; } } class Mul extends command.Command { //Custom command Mul that multiply two variables a * b and returns the total = a * b constructor(arguments1, fn) { var argArray = [['a', 'integer'], ['b', 'integer']]; var resArray = [['total', 'integer']] var maparg = new Map(argArray); var mapres = new Map(resArray); super(maparg, mapres, arguments1, fn); //this.arguments = new Map(argArray); //this.responds = new Map(resArray); this.responder = fn; } } class Div extends command.Command { constructor(arguments1, fn) { //Custom command Div that divides two variables a / b and returns the total = a / b var argArray = [['a', 'integer'], ['b', 'integer']]; var resArray = [['total', 'integer']] var maparg = new Map(argArray); var mapres = new Map(resArray); super(maparg, mapres, arguments1, fn); //this.arguments = new Map(argArray); //this.responds = new Map(resArray); this.responder = fn; } } var _OnError = function(maparg){ // Error method that happens in the amp protocol console.log('Error event has happened'); var error = maparg.get('_error'); var error_code = maparg.get('_error_code'); var error_description = maparg.get('_error_description'); const reply = new Map([['_error', error], ['_error_code', error_code], ['_error_description', error_description]]); console.log(reply); return reply; } /* For every custom command that you create you must create a callback function that will trigger when data is received for this custom class. This callback function will handle processing data and sending back a reply. The basic structure for the callback function is shown below */ var OnAdd = function(maparg){ /* User defined method that does addition on two passed variables Callback function that is invoke when the Add command has be sent. The variables that you defined in the custom class will have to be grabbed from the maparg, processed and return. The return map must match the one you defined in the custom class */ console.log("OnAdd event has been trigger"); var a = maparg.get('a'); var b = maparg.get('b'); var answer = a + b; const reply = new Map([['total', answer]]); return reply; }; var OnSub = function(maparg){ /* User defined method that does subtraction on two passed variables Callback function that is invoke when the Sub command has be sent */ console.log("OnSub event has been trigger"); var a = maparg.get('a'); var b = maparg.get('b'); var answer = a - b; const reply = new Map([['total', answer]]); return reply; }; var OnMul = function(maparg){ /* User defined method that does multiplication on two passed variables Callback function that is invoke when the Mul command has be sent */ console.log("OnMul event has been trigger"); var a = maparg.get('a'); var b = maparg.get('b'); var answer = a * b; const reply = new Map([['total', answer]]); return reply; }; var OnDiv = function(maparg){ /* User defined method that does division on two passed variables Callback function that is invoke when the Div command has be sent */ console.log("OnDiv event has been trigger"); var a = maparg.get('a'); var b = maparg.get('b'); var answer = a / b; const reply = new Map([['total', answer]]); return reply; }; /* The factorycommands object must be defined as shown below and sent to the controller class. This object connects the command sent over the file to the custom command class that defines its arguments and response structure */ var factorycommands = { 'Add': Add, 'Sub': Sub, 'Mul': Mul, 'Div': Div }; controller.factory.factorycommands = factorycommands; /* The factorycallbacks object must be defined as shown below and sent to the controller class. This object connects the custom command to its callback function when it receives data from the wire. */ var factorycallbacks = { 'Add': OnAdd, 'Sub': OnSub, 'Mul': OnMul, 'Div': OnDiv }; controller.factory.factorycallbacks = factorycallbacks; /* All client calls can be handle using the pattern shown below: Set server.client.clientaddress to the host Ip address of the peer you want to connect to Set server.client.clientport to the port of the host that you want to connect to Set server.client.connected to the function that will be callback when you make a connection Set server.client.received to the callback function that will be ran when data is received from the host The connected callback function you need to call server.client.remoteCall to send the command to the host The received callback function you need to call ResolveCommand to resolve the promised set when this class is initailized. Make sure you set ResolveCommand to resolve as shown above. */ class doadd{ constructor(){ this.clientconn = server.client; this.clientconn.clientaddress = clientaddress; this.clientconn.clientport = clientport; this.clientconn.connected = this.connected; this.clientconn.received = this.received; } connected(){ console.log('A connection has been established'); //['command','Add'], ['var1',10], ['var2',20] => ['_ask',ask], ['_command','Add'], ['a', aVar], ['b', bVar] //var data = new Map([['_ask',ask], ['_command','Add'], ['a', aVar], ['b', bVar]]); var data = new Map([['command','Add'], ['a', aVar], ['b', bVar]]); server.client.remoteCall(data); } received(data){ console.log('Data has been received back from the server'); const entries = Object.entries(data); const keys = Object.keys(data); //console.log("key - " + keys[0]); if(keys[0] == "_answer"){ if(data['total'] == NaN){ server.errorObj.set_code("151"); server.errorObj.set_description("The response data is not right"); } else{ //console.log("data - " + data['total']); ResolveCommand(data['total']); } } else{ RejectCommand(data); } } } class dosub{ constructor(){ this.clientconn = server.client; this.clientconn.clientaddress = '127.0.0.1'; this.clientconn.clientport = 2000 this.clientconn.connected = this.connected; this.clientconn.received = this.received; } connected(){ console.log('A connection has been established'); //console.log("a - " + aVar + "b - " + bVar); var data = new Map([['_ask','1'], ['_command','Sub'], ['a', aVar], ['b', bVar]]); //var data = new Map([['_command','Sub'], ['a', aVar], ['b', bVar]]); server.client.remoteCall(data); } received(data){ console.log('Data has been received back from the server'); //const entries = Object.entries(data) //console.log("data - " + data['total']); ResolveCommand(data['total']); } } class domul{ constructor(){ this.clientconn = server.client; this.clientconn.clientaddress = '127.0.0.1'; this.clientconn.clientport = 2000 this.clientconn.connected = this.connected; this.clientconn.received = this.received; } connected(){ console.log('A connection has been established'); //console.log("a - " + aVar + "b - " + bVar); var data = new Map([['_ask','1'], ['_command','Mul'], ['a', aVar], ['b', bVar]]); server.client.remoteCall(data); } received(data){ console.log('Data has been received back from the server'); //const entries = Object.entries(data) //console.log("data - " + data['total']); ResolveCommand(data['total']); } } class dodiv{ constructor(){ this.clientconn = server.client; this.clientconn.clientaddress = '127.0.0.1'; this.clientconn.clientport = 2000 this.clientconn.connected = this.connected; this.clientconn.received = this.received; } connected(){ console.log('A connection has been established'); //console.log("a - " + aVar + "b - " + bVar); var data = new Map([['_ask','1'], ['_command','Div'], ['a', aVar], ['b', bVar]]); server.client.remoteCall(data); } received(data){ console.log('Data has been received back from the server'); //const entries = Object.entries(data) //console.log("data - " + data['total']); ResolveCommand(data['total']); } } function sendcommand(command){ if(command=="Add"){ var promise = new Promise(function(resolve, reject) { com1 = new doadd(); com1.clientconn.start(); ResolveCommand = resolve; RejectCommand = reject; }) promise. then(function (data) { console.log("Data received from Command => " + data); }). catch(function (data) { console.log('Some error has occured'); const values = Object.values(data); console.log("[Error] - " + values) server.errorObj.reset_error(); }). finally(function (){ // This block has no effect on the return value. console.log('All done!') getinput(); }); } else if(command=="Sub"){ var promise = new Promise(function(resolve, reject) { com1 = new dosub(); com1.clientconn.start(); ResolveCommand = resolve; }) promise. then(function (data) { console.log("Data received from Command => " + data); }). catch(function () { console.log('Some error has occured'); }). finally(function (){ // This block has no effect on the return value. console.log('All done!') getinput(); }); } else if(command=="Mul"){ var promise = new Promise(function(resolve, reject) { com1 = new domul(); com1.clientconn.start(); ResolveCommand = resolve; }) promise. then(function (data) { console.log("Data received from Command => " + data); }). catch(function () { console.log('Some error has occured'); }). finally(function (){ // This block has no effect on the return value. console.log('All done!') getinput(); }); } else if(command=="Div"){ var promise = new Promise(function(resolve, reject) { com1 = new dodiv(); com1.clientconn.start(); ResolveCommand = resolve; }) promise. then(function (data) { console.log("Data received from Command => " + data); }). catch(function () { console.log('Some error has occured'); }). finally(function (){ // This block has no effect on the return value. console.log('All done!') getinput(); }); } else{ console.log("Not a valid command!!"); } } /* The port must be define, the initial callback function must be define and the listening method from the server class must be defined as shown below to start the amp server on the supplied port */ function processarg(){ var myArgs = process.argv.slice(2); if(myArgs.length != 2){ console.log("Invalid arguments, to run this app -> 'node ampserver serverport clientport'"); process.exit(); } try{ serverport = parseInt(myArgs[0]); clientport = parseInt(myArgs[1]); } catch(err){ console.log("The serverport and clientport must be a number bewteen 0 and 65535"); process.exit(); } } var ResolveCommand = undefined; var RejectCommand = undefined; var serverport = 1234; var clientport = 4321; var clientaddress = '127.0.0.1'; var aVar = 9; processarg(); console.log("Client port is set to " + clientport); /////// Start the server //////// server.callback.setcallback(controller.CreateController); server.server.listen(serverport, function() { console.log('The AMP server is running on port ' + serverport); getinput(); }); /* Below is used to get user input for demostration */ var schema = { properties: { command: { description: 'Enter the Command', type: 'string', pattern: /Add|Sub|Mul|Div|exit/, message: 'Command must be Add, Sub, Mul, Div or exit', default: 'Add', required: true }, A: { description: 'Enter the Value for Variable A', type: 'integer', pattern: /[0-9]*/, default: '1', ask: function() { // only ask if command is not exit return prompt.history('command').value != 'exit'; } }, B: { description: 'Enter the Value for Variable B', type: 'integer', pattern: /[0-9]*/, default: '2', ask: function() { // only ask if command is not exit return prompt.history('command').value != 'exit'; } } } }; prompt.message = "AMP"; prompt.delimiter = ">"; prompt.start(); function getinput(){ commandarray = ['Add', 'Sub', 'Mul', 'Div']; console.log("Commands (Add, Sub, Mul, Div, exit)"); prompt.get(schema, function (err, result) { //['command', 'A', 'B'] if (err) { return onErr(err); } if (result.command.toLowerCase() == "exit"){ console.log("Shutting down, exiting the app"); process.exit(); } else if(commandarray.includes(result.command)){ x = isNaN(result.A); y = isNaN(result.B); if(x == true || y == true){ console.log("The variable A and B must be a integer"); } else{ console.log("Sending Command => " + result.command + "(a=" + result.A + ", b=" + result.B + ")"); aVar = parseInt(result.A); bVar = parseInt(result.B); sendcommand(result.command); } } else{ console.log("Invalid command!"); } }); //prompt.stop(); } function onErr(err) { console.log(err); return 1; }